diff --git a/open-sse/utils/proxyFetch.js b/open-sse/utils/proxyFetch.js index 38b3539..c97c909 100644 --- a/open-sse/utils/proxyFetch.js +++ b/open-sse/utils/proxyFetch.js @@ -8,9 +8,14 @@ const proxyDispatchers = new Map(); // DNS cache — use Map to avoid prototype pollution via malformed hostnames const DNS_CACHE = new Map(); -const MITM_BYPASS_HOSTS = ["cloudcode-pa.googleapis.com", "daily-cloudcode-pa.googleapis.com", "googleapis.com"]; -const MITM_BYPASS_HEADER = "x-request-source"; -const MITM_BYPASS_VALUE = "local"; +const MITM_BYPASS_HOSTS = [ + "cloudcode-pa.googleapis.com", + "daily-cloudcode-pa.googleapis.com", + "api.individual.githubcopilot.com", + "q.us-east-1.amazonaws.com", + "codewhisperer.us-east-1.amazonaws.com", + "api2.cursor.sh", +]; const GOOGLE_DNS_SERVERS = ["8.8.8.8", "8.8.4.4"]; const HTTPS_PORT = 443; const HTTP_SUCCESS_MIN = 200; @@ -46,23 +51,7 @@ async function resolveRealIP(hostname) { /** * Check if request should bypass MITM DNS redirect */ -function shouldBypassMitmDns(url, options) { - if (!options?.headers) return false; - - const headers = options.headers; - const hasLocalMarker = headers[MITM_BYPASS_HEADER] === MITM_BYPASS_VALUE || - headers[MITM_BYPASS_HEADER.charAt(0).toUpperCase() + MITM_BYPASS_HEADER.slice(1)] === MITM_BYPASS_VALUE; - - if (!hasLocalMarker) { - try { - const hostname = new URL(url).hostname; - if (MITM_BYPASS_HOSTS.some(host => hostname.includes(host))) { - console.warn(`[ProxyFetch] MITM bypass NOT triggered for ${hostname} - missing header`); - } - } catch { /* invalid URL — skip debug log */ } - return false; - } - +function shouldBypassMitmDns(url) { try { const hostname = new URL(url).hostname; return MITM_BYPASS_HOSTS.some(host => hostname.includes(host)); @@ -209,8 +198,25 @@ async function createBypassRequest(parsedUrl, realIP, options) { export async function proxyAwareFetch(url, options = {}, proxyOptions = null) { const targetUrl = typeof url === "string" ? url : url.toString(); - // MITM DNS bypass: resolve real IP for googleapis.com when x-request-source: local - if (shouldBypassMitmDns(targetUrl, options)) { + const connectionProxyUrl = resolveConnectionProxyUrl(targetUrl, proxyOptions); + const envProxyUrl = connectionProxyUrl ? null : normalizeProxyUrl(getEnvProxyUrl(targetUrl)); + const proxyUrl = connectionProxyUrl || envProxyUrl; + + // MITM DNS bypass: for known MITM-intercepted hosts, resolve real IP to avoid DNS spoof + if (shouldBypassMitmDns(targetUrl)) { + if (proxyUrl) { + // Proxy resolves DNS externally (not affected by /etc/hosts) — use proxy directly + try { + const dispatcher = await getDispatcher(proxyUrl); + return await originalFetch(url, { ...options, dispatcher }); + } catch (proxyError) { + if (proxyOptions?.strictProxy === true) { + throw new Error(`[ProxyFetch] Proxy required but failed (strictProxy=true): ${proxyError.message}`); + } + console.warn(`[ProxyFetch] Proxy failed, falling back to direct bypass: ${proxyError.message}`); + } + } + // No proxy — manually resolve real IP to bypass DNS spoof try { const parsedUrl = new URL(targetUrl); const realIP = await resolveRealIP(parsedUrl.hostname); @@ -220,10 +226,6 @@ export async function proxyAwareFetch(url, options = {}, proxyOptions = null) { } } - const connectionProxyUrl = resolveConnectionProxyUrl(targetUrl, proxyOptions); - const envProxyUrl = connectionProxyUrl ? null : normalizeProxyUrl(getEnvProxyUrl(targetUrl)); - const proxyUrl = connectionProxyUrl || envProxyUrl; - if (proxyUrl) { try { const dispatcher = await getDispatcher(proxyUrl); diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js index 31e7dfe..2730717 100644 --- a/src/app/(dashboard)/dashboard/combos/page.js +++ b/src/app/(dashboard)/dashboard/combos/page.js @@ -6,7 +6,7 @@ import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; // Validate combo name: only a-z, A-Z, 0-9, -, _ -const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/; +const VALID_NAME_REGEX = /^[a-zA-Z0-9_.\-]+$/; export default function CombosPage() { const [combos, setCombos] = useState([]); @@ -329,7 +329,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { return false; } if (!VALID_NAME_REGEX.test(value)) { - setNameError("Only letters, numbers, - and _ allowed"); + setNameError("Only letters, numbers, -, _ and . allowed"); return false; } setNameError(""); @@ -394,7 +394,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { error={nameError} />

- Only letters, numbers, - and _ allowed + Only letters, numbers, -, _ and . allowed

diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 69d5ca0..70a12ee 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -290,40 +290,28 @@ export default function ProviderDetailPage() { } }; - const handleSwapPriority = async (conn1, conn2) => { - if (!conn1 || !conn2) return; + const handleSwapPriority = async (index1, index2) => { + // Optimistic update state + const newConnections = [...connections]; + [newConnections[index1], newConnections[index2]] = [newConnections[index2], newConnections[index1]]; + setConnections(newConnections); + try { - // If they have the same priority, we need to ensure the one moving up - // gets a lower value than the one moving down. - // We use a small offset which the backend re-indexing will fix. - let p1 = conn2.priority; - let p2 = conn1.priority; - - if (p1 === p2) { - // If moving conn1 "up" (index decreases) - const isConn1MovingUp = connections.indexOf(conn1) > connections.indexOf(conn2); - if (isConn1MovingUp) { - p1 = conn2.priority - 0.5; - } else { - p1 = conn2.priority + 0.5; - } - } - await Promise.all([ - fetch(`/api/providers/${conn1.id}`, { + fetch(`/api/providers/${newConnections[index1].id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ priority: p1 }), + body: JSON.stringify({ priority: index1 }), }), - fetch(`/api/providers/${conn2.id}`, { + fetch(`/api/providers/${newConnections[index2].id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ priority: p2 }), + body: JSON.stringify({ priority: index2 }), }), ]); - await fetchConnections(); } catch (error) { console.log("Error swapping priority:", error); + await fetchConnections(); } }; @@ -421,7 +409,6 @@ export default function ProviderDetailPage() { const connectionsList = (
{connections - .sort((a, b) => (a.priority || 0) - (b.priority || 0)) .map((conn, index) => (
@@ -431,8 +418,8 @@ export default function ProviderDetailPage() { isOAuth={isOAuth} isFirst={index === 0} isLast={index === connections.length - 1} - onMoveUp={() => handleSwapPriority(conn, connections[index - 1])} - onMoveDown={() => handleSwapPriority(conn, connections[index + 1])} + onMoveUp={() => handleSwapPriority(index, index - 1)} + onMoveDown={() => handleSwapPriority(index, index + 1)} onToggleActive={(isActive) => handleUpdateConnectionStatus(conn.id, isActive)} onUpdateProxy={async (proxyPoolId) => { try { diff --git a/src/app/api/combos/[id]/route.js b/src/app/api/combos/[id]/route.js index 6e11445..0eae2ce 100644 --- a/src/app/api/combos/[id]/route.js +++ b/src/app/api/combos/[id]/route.js @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { getComboById, updateCombo, deleteCombo, getComboByName } from "@/lib/localDb"; // Validate combo name: only a-z, A-Z, 0-9, -, _ -const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/; +const VALID_NAME_REGEX = /^[a-zA-Z0-9_.\-]+$/; // GET /api/combos/[id] - Get combo by ID export async function GET(request, { params }) { @@ -30,7 +30,7 @@ export async function PUT(request, { params }) { // Validate name format if provided if (body.name) { if (!VALID_NAME_REGEX.test(body.name)) { - return NextResponse.json({ error: "Name can only contain letters, numbers, - and _" }, { status: 400 }); + return NextResponse.json({ error: "Name can only contain letters, numbers, -, _ and ." }, { status: 400 }); } // Check if name already exists (exclude current combo) diff --git a/src/app/api/combos/route.js b/src/app/api/combos/route.js index 43e103a..6b7ca24 100644 --- a/src/app/api/combos/route.js +++ b/src/app/api/combos/route.js @@ -4,7 +4,7 @@ import { getCombos, createCombo, getComboByName } from "@/lib/localDb"; export const dynamic = "force-dynamic"; // Validate combo name: only a-z, A-Z, 0-9, -, _ -const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/; +const VALID_NAME_REGEX = /^[a-zA-Z0-9_.\-]+$/; // GET /api/combos - Get all combos export async function GET() { @@ -29,7 +29,7 @@ export async function POST(request) { // Validate name format if (!VALID_NAME_REGEX.test(name)) { - return NextResponse.json({ error: "Name can only contain letters, numbers, - and _" }, { status: 400 }); + return NextResponse.json({ error: "Name can only contain letters, numbers, -, _ and ." }, { status: 400 }); } // Check if name already exists diff --git a/src/mitm/server.js b/src/mitm/server.js index 154e166..9882667 100644 --- a/src/mitm/server.js +++ b/src/mitm/server.js @@ -63,14 +63,17 @@ try { // ── Helpers ─────────────────────────────────────────────────── const cachedTargetIPs = {}; +const CACHE_TTL_MS = 5 * 60 * 1000; + async function resolveTargetIP(hostname) { - if (cachedTargetIPs[hostname]) return cachedTargetIPs[hostname]; + const cached = cachedTargetIPs[hostname]; + if (cached && Date.now() - cached.ts < CACHE_TTL_MS) return cached.ip; const resolver = new dns.Resolver(); resolver.setServers(["8.8.8.8"]); const resolve4 = promisify(resolver.resolve4.bind(resolver)); const addresses = await resolve4(hostname); - cachedTargetIPs[hostname] = addresses[0]; - return cachedTargetIPs[hostname]; + cachedTargetIPs[hostname] = { ip: addresses[0], ts: Date.now() }; + return cachedTargetIPs[hostname].ip; } function collectBodyRaw(req) {