mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 03:59:58 +08:00
fix: what the batch C review found
- A redirect followed for a custom base URL (quota on) no longer carries
the user's key or cookies to another origin, as fetch itself does, and
a private address may redirect to another private one (already counted).
- The admin Test of an Ollama or Vertex AI entry without a URL goes where
chat sends that entry's key: the environment's own URL variable, for
Ollama else the local default. The Test of an Ollama Cloud key without
a URL went to the cloud while chat went to local Ollama.
- Chat saves: each save notes the chat on screen and the order of the
reads before reading its data. A save read before switching chats no
longer writes into the chat switched to, and a copy that waited for its
thumbnail no longer replaces a newer one.
- "Continue without saving" keeps its button when a later auto-save fails,
and goes away when a new message is sent.
- A screenshot check that was waiting for its image when the user pressed
Stop stays skipped after the next message.
- MCP History: draw.io's own copy of a diagram (after get_diagram) no
longer adds an entry without a picture; a change of background is still
its own version. The tab ignores an edit's answer that arrives after a
newer AI write loaded.
- Desktop: a deleted preset is not brought back by a failed switch, and a
request naming no preset does not stop a rollback. An origin keeping
an access code counts as having settings.
- .env: a quoted value ending in a backslash ("C:\dir\") is read as dotenv
reads it.
- The tool card no longer crashes on an id that does not turn into text;
an older Test's success timer no longer ends a newer Test's spinner.
- Tests that passed without their fix now check it.
This commit is contained in:
+15
-5
@@ -21,6 +21,7 @@ import {
|
||||
adminProvidersToConfig,
|
||||
loadAdminProviders,
|
||||
} from "@/lib/admin/providers"
|
||||
import { getEnvFallback } from "@/lib/admin/settings"
|
||||
import { getApiEndpoint } from "@/lib/base-path"
|
||||
import { redirectGuardedFetch } from "@/lib/ssrf-protection"
|
||||
import {
|
||||
@@ -803,8 +804,11 @@ export function getAIModel(clientOverrides?: ClientOverrides): ModelConfig {
|
||||
// Exception: EdgeOne doesn't require API keys.
|
||||
// Ollama is exempt only when no server OLLAMA_API_KEY is configured;
|
||||
// when it IS configured, the outer guard also enforces client apiKey for custom baseUrls.
|
||||
// A trusted URL is the server's own (the admin Test of an entry without
|
||||
// one), not a user's
|
||||
if (
|
||||
overrides?.baseUrl &&
|
||||
!overrides?.trustedBaseUrl &&
|
||||
!overrides?.apiKey &&
|
||||
!(overrides?.provider === "vertexai" && overrides?.vertexApiKey) &&
|
||||
overrides?.provider !== "edgeone" &&
|
||||
@@ -1118,14 +1122,20 @@ export function edgeOneEndpoint(req: Request): string {
|
||||
/**
|
||||
* The server's <P>_BASE_URL for a provider, which getAIModel uses for a
|
||||
* server model without a URL variable of its own (an admin panel entry
|
||||
* without a URL). None for Bedrock and EdgeOne, and none for Ollama and
|
||||
* Vertex AI, whose variables the panel writes itself (before a save they
|
||||
* still hold the entry's previous URL).
|
||||
* without a URL). None for Bedrock and EdgeOne. Ollama and Vertex AI share
|
||||
* one variable with the panel, which writes an entry's URL into it: an
|
||||
* entry without a URL gets the environment's value once saved (before a
|
||||
* save the variable may still hold the entry's previous URL), and Ollama
|
||||
* without one goes to the SDK's local default.
|
||||
*/
|
||||
export function globalBaseUrl(provider: ProviderName): string | undefined {
|
||||
if (["bedrock", "edgeone", "ollama", "vertexai"].includes(provider)) {
|
||||
return undefined
|
||||
if (provider === "ollama") {
|
||||
return getEnvFallback("OLLAMA_BASE_URL") || "http://127.0.0.1:11434/api"
|
||||
}
|
||||
if (provider === "vertexai") {
|
||||
return getEnvFallback("GOOGLE_VERTEX_BASE_URL") || undefined
|
||||
}
|
||||
if (provider === "bedrock" || provider === "edgeone") return undefined
|
||||
const name =
|
||||
provider === "gateway"
|
||||
? "AI_GATEWAY_BASE_URL"
|
||||
|
||||
+28
-6
@@ -131,8 +131,8 @@ const MAX_REDIRECTS = 5
|
||||
* blocked, a public URL could still redirect the request to an internal
|
||||
* host, so redirects are refused. With private URLs allowed but the quota
|
||||
* on (DYNAMODB_QUOTA_TABLE), a request to a private address counts as the
|
||||
* server's: redirects are followed only to public addresses, or a public
|
||||
* URL could reach the server's own network uncounted. Undefined otherwise.
|
||||
* server's: a public URL's redirects are followed only to public addresses,
|
||||
* or it could reach the server's own network uncounted. Undefined otherwise.
|
||||
*/
|
||||
export function redirectGuardedFetch(): typeof fetch | undefined {
|
||||
const blockAll = !allowPrivateUrls()
|
||||
@@ -140,6 +140,8 @@ export function redirectGuardedFetch(): typeof fetch | undefined {
|
||||
return async (input, init) => {
|
||||
let url = input instanceof Request ? input.url : String(input)
|
||||
let next = init
|
||||
// A request to a private address already counts as the server's
|
||||
let startsPrivate: boolean | undefined
|
||||
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
||||
const response = await fetch(url, { ...next, redirect: "manual" })
|
||||
const location = response.headers.get("location")
|
||||
@@ -147,20 +149,40 @@ export function redirectGuardedFetch(): typeof fetch | undefined {
|
||||
return response
|
||||
}
|
||||
if (blockAll) throw new RedirectRefusedError()
|
||||
startsPrivate ??= await isPrivateUrl(url)
|
||||
const from = new URL(url)
|
||||
url = new URL(location, url).toString()
|
||||
if (await isPrivateUrl(url)) {
|
||||
if (!startsPrivate && (await isPrivateUrl(url))) {
|
||||
throw new RedirectRefusedError(
|
||||
"Redirects to private addresses are not allowed",
|
||||
)
|
||||
}
|
||||
// As fetch itself does: 303, and 301 or 302 after a POST, go on
|
||||
// as a GET without the body
|
||||
const method = (next?.method ?? "GET").toUpperCase()
|
||||
// The rest as fetch itself does it. Another origin gets no
|
||||
// credentials (the user's key, EdgeOne's cookies)
|
||||
const headers = new Headers(next?.headers)
|
||||
if (new URL(url).origin !== from.origin) {
|
||||
headers.delete("authorization")
|
||||
headers.delete("proxy-authorization")
|
||||
headers.delete("cookie")
|
||||
}
|
||||
next = { ...next, headers }
|
||||
// 303, and 301 or 302 after a POST, go on as a GET without the
|
||||
// body
|
||||
const method = (next.method ?? "GET").toUpperCase()
|
||||
if (
|
||||
response.status === 303 ||
|
||||
((response.status === 301 || response.status === 302) &&
|
||||
method === "POST")
|
||||
) {
|
||||
for (const name of [
|
||||
"content-type",
|
||||
"content-length",
|
||||
"content-encoding",
|
||||
"content-language",
|
||||
"content-location",
|
||||
]) {
|
||||
headers.delete(name)
|
||||
}
|
||||
next = { ...next, method: "GET", body: undefined }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user