import { request as undiciRequest } from "undici"; import { createProxyDispatcher, isSocks5ProxyEnabled, proxyConfigToUrl, proxyUrlForLogs, } from "@omniroute/open-sse/utils/proxyDispatcher.ts"; import { testProxySchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse"; import { getProxyById } from "@/lib/localDb"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; const BASE_SUPPORTED_PROXY_TYPES = new Set(["http", "https"]); function getErrorMessage(error: unknown, fallbackMessage: string): string { if (error instanceof Error && error.message) { return error.message; } return fallbackMessage; } function getSupportedProxyTypes() { if (isSocks5ProxyEnabled()) { return new Set([...BASE_SUPPORTED_PROXY_TYPES, "socks5"]); } return BASE_SUPPORTED_PROXY_TYPES; } function supportedTypesMessage() { return isSocks5ProxyEnabled() ? "http, https, or socks5" : "http or https"; } /** * POST /api/settings/proxy/test — test proxy connectivity * Body: { proxy: { type, host, port, username?, password? } } * Returns: { success, publicIp?, latencyMs?, error? } */ export async function POST(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; let rawBody: unknown; try { rawBody = await request.json(); } catch { return createErrorResponse({ status: 400, message: "Invalid JSON body", type: "invalid_request", }); } try { const validation = validateBody(testProxySchema, rawBody); if (isValidationFailure(validation)) { return createErrorResponse({ status: 400, message: validation.error.message, details: validation.error.details, type: "invalid_request", }); } let { proxy } = validation.data; // If a proxyId is provided, look up the real (non-redacted) credentials from DB. // The frontend sends redacted credentials (***) from listProxies(), so we need // the actual secrets for testing. const body = rawBody as Record; const proxyId = typeof body.proxyId === "string" ? body.proxyId.trim() : null; if (proxyId) { const dbProxy = await getProxyById(proxyId, { includeSecrets: true }); if (dbProxy) { proxy = { ...proxy, host: proxy.host || dbProxy.host, port: proxy.port || String(dbProxy.port), type: proxy.type || dbProxy.type, username: dbProxy.username, password: dbProxy.password, }; } } const proxyType = String(proxy.type || "http").toLowerCase(); if (proxyType === "socks5" && !isSocks5ProxyEnabled()) { return createErrorResponse({ status: 400, message: "SOCKS5 proxy is disabled (set ENABLE_SOCKS5_PROXY=true to enable)", type: "invalid_request", }); } if (proxyType.startsWith("socks") && proxyType !== "socks5") { return createErrorResponse({ status: 400, message: `proxy.type must be ${supportedTypesMessage()}`, type: "invalid_request", }); } if (!getSupportedProxyTypes().has(proxyType)) { return createErrorResponse({ status: 400, message: `proxy.type must be ${supportedTypesMessage()}`, type: "invalid_request", }); } let proxyUrl: string; try { const normalizedProxyUrl = proxyConfigToUrl( { type: proxyType, host: proxy.host, port: proxy.port, username: proxy.username || "", password: proxy.password || "", }, { allowSocks5: isSocks5ProxyEnabled() } ); if (!normalizedProxyUrl) { return createErrorResponse({ status: 400, message: "Invalid proxy configuration", type: "invalid_request", }); } proxyUrl = normalizedProxyUrl; } catch (proxyError) { return createErrorResponse({ status: 400, message: getErrorMessage(proxyError, "Invalid proxy configuration"), type: "invalid_request", }); } const publicProxyUrl = proxyUrlForLogs(proxyUrl); const startTime = Date.now(); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); const dispatcher = createProxyDispatcher(proxyUrl); try { const result = await undiciRequest("https://api64.ipify.org?format=json", { method: "GET", dispatcher, signal: controller.signal, headersTimeout: 10000, bodyTimeout: 10000, }); const responseText = await result.body.text(); let parsed: { ip?: string }; try { const parsedJson = JSON.parse(responseText); if (parsedJson && typeof parsedJson === "object") { parsed = parsedJson as { ip?: string }; } else { parsed = { ip: String(parsedJson) }; } } catch { parsed = { ip: responseText.trim() }; } return Response.json({ success: true, publicIp: parsed.ip || null, latencyMs: Date.now() - startTime, proxyUrl: publicProxyUrl, }); } catch (fetchError) { return Response.json({ success: false, error: fetchError instanceof Error && fetchError.name === "AbortError" ? "Connection timeout (10s)" : getErrorMessage(fetchError, "Connection failed"), latencyMs: Date.now() - startTime, proxyUrl: publicProxyUrl, }); } finally { clearTimeout(timeout); } } catch (error) { return createErrorResponseFromUnknown(error, "Unexpected server error"); } }