mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-04-28 06:19:46 +00:00
- Add proper TS interfaces to CliproxyapiSettingsTab (replace any/Record) - Add HTTP status checks and error handling on all fetches - Add client-side URL validation before saving proxy URL - Add error state display for version manager service - Document localhost SSRF exception in isPrivateHost - Add ALLOWED_COLUMNS whitelist to updateVersionManagerTool
231 lines
7 KiB
TypeScript
231 lines
7 KiB
TypeScript
/** Upstream proxy config persistence for upstream_proxy_config table. */
|
|
import { getDbInstance } from "./core";
|
|
|
|
interface UpstreamProxyConfig {
|
|
id: number;
|
|
providerId: string;
|
|
mode: string;
|
|
cliproxyapiModelMapping: Record<string, unknown> | null;
|
|
nativePriority: number;
|
|
cliproxyapiPriority: number;
|
|
enabled: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
interface UpstreamProxyRow {
|
|
id: unknown;
|
|
provider_id: unknown;
|
|
mode: unknown;
|
|
cliproxyapi_model_mapping: unknown;
|
|
native_priority: unknown;
|
|
cliproxyapi_priority: unknown;
|
|
enabled: unknown;
|
|
created_at: unknown;
|
|
updated_at: unknown;
|
|
}
|
|
|
|
function toRecord(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
}
|
|
|
|
const BLOCKED_HOSTNAMES = ["metadata.google.internal", "169.254.169.254", "metadata.aws.internal"];
|
|
|
|
function isPrivateHost(hostname: string): boolean {
|
|
// CLIProxyAPI runs on localhost:8317 — allow loopback explicitly
|
|
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") return false;
|
|
if (BLOCKED_HOSTNAMES.includes(hostname)) return true;
|
|
if (
|
|
/^10\./.test(hostname) ||
|
|
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
|
|
/^192\.168\./.test(hostname)
|
|
)
|
|
return true;
|
|
if (
|
|
/^0\./.test(hostname) ||
|
|
/^127\./.test(hostname) ||
|
|
/^224\./.test(hostname) ||
|
|
/^169\.254\./.test(hostname)
|
|
)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
export function validateProxyUrl(
|
|
url: string
|
|
): { valid: true; url: string } | { valid: false; error: string } {
|
|
try {
|
|
const parsed = new URL(url);
|
|
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
return {
|
|
valid: false,
|
|
error: `Unsupported protocol "${parsed.protocol}" — use http or https`,
|
|
};
|
|
}
|
|
if (isPrivateHost(parsed.hostname)) {
|
|
return {
|
|
valid: false,
|
|
error: `Proxy URL cannot point to private/internal address "${parsed.hostname}"`,
|
|
};
|
|
}
|
|
return { valid: true, url };
|
|
} catch {
|
|
return { valid: false, error: `Invalid URL: "${url}"` };
|
|
}
|
|
}
|
|
|
|
function rowToConfig(record: Record<string, unknown>): UpstreamProxyConfig {
|
|
return {
|
|
id: record.id as number,
|
|
providerId: record.provider_id as string,
|
|
mode: record.mode as string,
|
|
cliproxyapiModelMapping:
|
|
record.cliproxyapi_model_mapping && typeof record.cliproxyapi_model_mapping === "string"
|
|
? JSON.parse(record.cliproxyapi_model_mapping)
|
|
: null,
|
|
nativePriority: record.native_priority as number,
|
|
cliproxyapiPriority: record.cliproxyapi_priority as number,
|
|
enabled: record.enabled === 1 || record.enabled === true,
|
|
createdAt: record.created_at as string,
|
|
updatedAt: record.updated_at as string,
|
|
};
|
|
}
|
|
|
|
export async function getUpstreamProxyConfigs() {
|
|
const db = getDbInstance();
|
|
const rows = db
|
|
.prepare("SELECT * FROM upstream_proxy_config ORDER BY provider_id")
|
|
.all() as UpstreamProxyRow[];
|
|
return rows.map((row) => rowToConfig(toRecord(row)));
|
|
}
|
|
|
|
export async function getUpstreamProxyConfig(providerId: string) {
|
|
const db = getDbInstance();
|
|
const row = db
|
|
.prepare("SELECT * FROM upstream_proxy_config WHERE provider_id = ?")
|
|
.get(providerId) as UpstreamProxyRow | undefined;
|
|
if (!row) return null;
|
|
return rowToConfig(toRecord(row));
|
|
}
|
|
|
|
export async function upsertUpstreamProxyConfig(data: {
|
|
providerId: string;
|
|
mode?: string;
|
|
cliproxyapiModelMapping?: Record<string, unknown> | null;
|
|
nativePriority?: number;
|
|
cliproxyapiPriority?: number;
|
|
enabled?: boolean;
|
|
}) {
|
|
const db = getDbInstance();
|
|
const mode = data.mode ?? "native";
|
|
const cliproxyapiModelMapping =
|
|
data.cliproxyapiModelMapping !== undefined
|
|
? JSON.stringify(data.cliproxyapiModelMapping)
|
|
: null;
|
|
const nativePriority = data.nativePriority ?? 1;
|
|
const cliproxyapiPriority = data.cliproxyapiPriority ?? 2;
|
|
const enabled = data.enabled !== false ? 1 : 0;
|
|
|
|
db.prepare(
|
|
`INSERT INTO upstream_proxy_config
|
|
(provider_id, mode, cliproxyapi_model_mapping, native_priority, cliproxyapi_priority, enabled, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
ON CONFLICT(provider_id) DO UPDATE SET
|
|
mode = excluded.mode,
|
|
cliproxyapi_model_mapping = excluded.cliproxyapi_model_mapping,
|
|
native_priority = excluded.native_priority,
|
|
cliproxyapi_priority = excluded.cliproxyapi_priority,
|
|
enabled = excluded.enabled,
|
|
updated_at = datetime('now')`
|
|
).run(
|
|
data.providerId,
|
|
mode,
|
|
cliproxyapiModelMapping,
|
|
nativePriority,
|
|
cliproxyapiPriority,
|
|
enabled
|
|
);
|
|
|
|
return getUpstreamProxyConfig(data.providerId);
|
|
}
|
|
|
|
export async function updateUpstreamProxyConfig(
|
|
providerId: string,
|
|
updates: Record<string, unknown>
|
|
) {
|
|
const db = getDbInstance();
|
|
const current = await getUpstreamProxyConfig(providerId);
|
|
if (!current) {
|
|
throw new Error(`Provider ${providerId} not found`);
|
|
}
|
|
|
|
const sets: string[] = ["updated_at = datetime('now')"];
|
|
const params: unknown[] = [];
|
|
|
|
if (updates.mode !== undefined) {
|
|
sets.push("mode = ?");
|
|
params.push(updates.mode);
|
|
}
|
|
if (updates.cliproxyapiModelMapping !== undefined) {
|
|
sets.push("cliproxyapi_model_mapping = ?");
|
|
params.push(
|
|
updates.cliproxyapiModelMapping === null
|
|
? null
|
|
: JSON.stringify(updates.cliproxyapiModelMapping)
|
|
);
|
|
}
|
|
if (updates.nativePriority !== undefined) {
|
|
sets.push("native_priority = ?");
|
|
params.push(updates.nativePriority);
|
|
}
|
|
if (updates.cliproxyapiPriority !== undefined) {
|
|
sets.push("cliproxyapi_priority = ?");
|
|
params.push(updates.cliproxyapiPriority);
|
|
}
|
|
if (updates.enabled !== undefined) {
|
|
sets.push("enabled = ?");
|
|
params.push(updates.enabled === true ? 1 : 0);
|
|
}
|
|
|
|
params.push(providerId);
|
|
db.prepare(`UPDATE upstream_proxy_config SET ${sets.join(", ")} WHERE provider_id = ?`).run(
|
|
...params
|
|
);
|
|
|
|
return getUpstreamProxyConfig(providerId);
|
|
}
|
|
|
|
export async function deleteUpstreamProxyConfig(providerId: string) {
|
|
const db = getDbInstance();
|
|
const result = db
|
|
.prepare("DELETE FROM upstream_proxy_config WHERE provider_id = ?")
|
|
.run(providerId);
|
|
return result.changes > 0;
|
|
}
|
|
|
|
export async function getProvidersByMode(mode: string) {
|
|
const db = getDbInstance();
|
|
const rows = db
|
|
.prepare(
|
|
"SELECT * FROM upstream_proxy_config WHERE mode = ? AND enabled = 1 ORDER BY provider_id"
|
|
)
|
|
.all(mode) as UpstreamProxyRow[];
|
|
return rows.map((row) => rowToConfig(toRecord(row)));
|
|
}
|
|
|
|
export async function getFallbackChainForProvider(providerId: string) {
|
|
const config = await getUpstreamProxyConfig(providerId);
|
|
if (!config) return [];
|
|
|
|
const chain: { executor: "native" | "cliproxyapi"; priority: number }[] = [];
|
|
|
|
if (config.enabled) {
|
|
chain.push({ executor: "native", priority: config.nativePriority });
|
|
if (config.mode === "cliproxyapi" || config.mode === "fallback") {
|
|
chain.push({ executor: "cliproxyapi", priority: config.cliproxyapiPriority });
|
|
}
|
|
}
|
|
|
|
chain.sort((a, b) => a.priority - b.priority);
|
|
return chain;
|
|
}
|