File size: 3,433 Bytes
6111b2b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | /**
* 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, arePrivateProviderUrlsAllowed } from "@/shared/network/outboundUrlGuard";
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 }
);
}
}
|