mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-16 20:22:21 +03:00
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
/**
|
|
* API: Proxy Fallback Test
|
|
* POST /api/proxy-fallback/test
|
|
*
|
|
* Bulk-test proxy candidates against a target provider URL.
|
|
* Returns which proxies can reach the target and their latency.
|
|
*/
|
|
|
|
import { NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
|
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
|
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
|
import { isPrivateHost } from "@/shared/network/outboundUrlGuard";
|
|
import { arePrivateProviderUrlsAllowed } from "@/shared/network/outboundUrlGuardPolicy";
|
|
import {
|
|
testProxiesAgainstTarget,
|
|
getProxyCandidates,
|
|
} from "@omniroute/open-sse/utils/proxyFallback";
|
|
|
|
const testSchema = z.object({
|
|
targetUrl: z.string().url("Invalid target URL"),
|
|
proxyUrls: z.array(z.string()).optional(),
|
|
});
|
|
|
|
/**
|
|
* SSRF guard: this route fetches a caller-supplied targetUrl through
|
|
* caller-supplied proxies. Even behind management auth, never let it probe
|
|
* private / link-local / cloud-metadata hosts (169.254.x, 127/8, 10/8,
|
|
* 192.168/16, 172.16/12, ::1, fc00::/7, .internal, …) unless the operator has
|
|
* explicitly opted in via OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS.
|
|
*/
|
|
function blockedPrivateUrl(rawUrl: string): boolean {
|
|
if (arePrivateProviderUrlsAllowed()) return false;
|
|
try {
|
|
return isPrivateHost(new URL(rawUrl).hostname);
|
|
} catch {
|
|
// Unparseable URL → treat as blocked (fail closed).
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const authError = await requireManagementAuth(request);
|
|
if (authError) return authError;
|
|
|
|
try {
|
|
const rawBody = await request.json();
|
|
const validation = validateBody(testSchema, rawBody);
|
|
if (isValidationFailure(validation)) {
|
|
return NextResponse.json({ error: validation.error }, { status: 400 });
|
|
}
|
|
|
|
const { targetUrl, proxyUrls: providedUrls } = validation.data;
|
|
|
|
// SSRF guard: refuse private/link-local/metadata targets and proxies.
|
|
if (blockedPrivateUrl(targetUrl)) {
|
|
return NextResponse.json(
|
|
{ error: "Blocked private or local target URL" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
if (providedUrls && providedUrls.some((u) => blockedPrivateUrl(u))) {
|
|
return NextResponse.json(
|
|
{ error: "Blocked private or local proxy URL" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Auto-collect candidates if no proxyUrls provided
|
|
const proxyUrls =
|
|
providedUrls && providedUrls.length > 0
|
|
? providedUrls
|
|
: await getProxyCandidates(targetUrl);
|
|
|
|
if (proxyUrls.length === 0) {
|
|
return NextResponse.json(
|
|
{
|
|
results: [],
|
|
message: "No proxy candidates available to test. Configure a proxy first.",
|
|
},
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
|
|
const results = await testProxiesAgainstTarget(targetUrl, proxyUrls);
|
|
|
|
const summary = {
|
|
total: results.length,
|
|
working: results.filter((r) => r.ok).length,
|
|
failed: results.filter((r) => !r.ok).length,
|
|
};
|
|
|
|
return NextResponse.json({ results, summary });
|
|
} catch (error: unknown) {
|
|
const message =
|
|
error instanceof Error ? error.message : "Failed to test proxy fallback";
|
|
return NextResponse.json(
|
|
{ error: sanitizeErrorMessage(error) || message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|