Enhance proxy functionality with Vercel relay support
This commit is contained in:
@@ -37,6 +37,11 @@ function normalizeProxyPoolUpdate(body = {}) {
|
||||
updates.strictProxy = body?.strictProxy === true;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(body, "type")) {
|
||||
const validTypes = ["http", "vercel"];
|
||||
updates.type = validTypes.includes(body?.type) ? body.type : "http";
|
||||
}
|
||||
|
||||
return { updates };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,37 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProxyPoolById, updateProxyPool } from "@/models";
|
||||
import { testProxyUrl } from "@/lib/network/proxyTest";
|
||||
import { fetch as undiciFetch } from "undici";
|
||||
|
||||
async function testVercelRelay(relayUrl, timeoutMs = 10000) {
|
||||
const controller = new AbortController();
|
||||
const startedAt = Date.now();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const res = await undiciFetch(relayUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"x-relay-target": "https://httpbin.org",
|
||||
"x-relay-path": "/get",
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
return {
|
||||
ok: res.ok,
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 500,
|
||||
error: err?.name === "AbortError" ? "Relay test timed out" : (err?.message || String(err)),
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/proxy-pools/[id]/test - Test proxy pool entry
|
||||
export async function POST(request, { params }) {
|
||||
@@ -12,7 +43,9 @@ export async function POST(request, { params }) {
|
||||
return NextResponse.json({ error: "Proxy pool not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const result = await testProxyUrl({ proxyUrl: proxyPool.proxyUrl });
|
||||
const result = proxyPool.type === "vercel"
|
||||
? await testVercelRelay(proxyPool.proxyUrl)
|
||||
: await testProxyUrl({ proxyUrl: proxyPool.proxyUrl });
|
||||
const now = new Date().toISOString();
|
||||
|
||||
await updateProxyPool(id, {
|
||||
|
||||
@@ -7,12 +7,15 @@ function toBoolean(value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const VALID_PROXY_TYPES = ["http", "vercel"];
|
||||
|
||||
function normalizeProxyPoolInput(body = {}) {
|
||||
const name = typeof body?.name === "string" ? body.name.trim() : "";
|
||||
const proxyUrl = typeof body?.proxyUrl === "string" ? body.proxyUrl.trim() : "";
|
||||
const noProxy = typeof body?.noProxy === "string" ? body.noProxy.trim() : "";
|
||||
const isActive = body?.isActive === undefined ? true : body.isActive === true;
|
||||
const strictProxy = body?.strictProxy === true;
|
||||
const type = VALID_PROXY_TYPES.includes(body?.type) ? body.type : "http";
|
||||
|
||||
if (!name) {
|
||||
return { error: "Name is required" };
|
||||
@@ -22,7 +25,7 @@ function normalizeProxyPoolInput(body = {}) {
|
||||
return { error: "Proxy URL is required" };
|
||||
}
|
||||
|
||||
return { name, proxyUrl, noProxy, isActive, strictProxy };
|
||||
return { name, proxyUrl, noProxy, isActive, strictProxy, type };
|
||||
}
|
||||
|
||||
function buildUsageMap(connections = []) {
|
||||
|
||||
142
src/app/api/proxy-pools/vercel-deploy/route.js
Normal file
142
src/app/api/proxy-pools/vercel-deploy/route.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createProxyPool } from "@/models";
|
||||
|
||||
const VERCEL_API = "https://api.vercel.com";
|
||||
|
||||
// Relay function source code deployed to Vercel
|
||||
// Forwards requests to target URL specified in x-relay-target header
|
||||
const RELAY_FUNCTION_CODE = `
|
||||
export const config = { runtime: "edge" };
|
||||
|
||||
export default async function handler(req) {
|
||||
const target = req.headers.get("x-relay-target");
|
||||
const relayPath = req.headers.get("x-relay-path") || "/";
|
||||
if (!target) {
|
||||
return new Response(JSON.stringify({ error: "Missing x-relay-target header" }), {
|
||||
status: 400,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const targetUrl = target.replace(/\\/$/, "") + relayPath;
|
||||
|
||||
const headers = new Headers(req.headers);
|
||||
headers.delete("x-relay-target");
|
||||
headers.delete("x-relay-path");
|
||||
headers.delete("host");
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method: req.method,
|
||||
headers,
|
||||
body: req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
|
||||
duplex: "half",
|
||||
});
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
});
|
||||
}
|
||||
`;
|
||||
|
||||
async function pollDeployment(deploymentId, token, maxMs = 120000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < maxMs) {
|
||||
const res = await fetch(`${VERCEL_API}/v13/deployments/${deploymentId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.readyState === "READY") return data;
|
||||
if (data.readyState === "ERROR" || data.readyState === "CANCELED") {
|
||||
throw new Error(`Deployment failed: ${data.readyState}`);
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
}
|
||||
throw new Error("Deployment timed out");
|
||||
}
|
||||
|
||||
// POST /api/proxy-pools/vercel-deploy
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const vercelToken = body.vercelToken;
|
||||
const projectName = body.projectName?.trim() || `relay-${Date.now().toString(36)}`;
|
||||
|
||||
if (!vercelToken) {
|
||||
return NextResponse.json({ error: "Vercel API token is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Deploy relay function to Vercel
|
||||
const deployRes = await fetch(`${VERCEL_API}/v13/deployments`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${vercelToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: projectName,
|
||||
files: [
|
||||
{
|
||||
file: "api/relay.js",
|
||||
data: RELAY_FUNCTION_CODE,
|
||||
},
|
||||
{
|
||||
file: "package.json",
|
||||
data: JSON.stringify({ name: projectName, version: "1.0.0" }),
|
||||
},
|
||||
{
|
||||
file: "vercel.json",
|
||||
data: JSON.stringify({
|
||||
rewrites: [{ source: "/(.*)", destination: "/api/relay" }],
|
||||
}),
|
||||
},
|
||||
],
|
||||
projectSettings: {
|
||||
framework: null,
|
||||
},
|
||||
target: "production",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!deployRes.ok) {
|
||||
const err = await deployRes.json().catch(() => ({}));
|
||||
return NextResponse.json(
|
||||
{ error: err.error?.message || "Failed to create Vercel deployment" },
|
||||
{ status: deployRes.status }
|
||||
);
|
||||
}
|
||||
|
||||
const deployment = await deployRes.json();
|
||||
const deploymentId = deployment.id || deployment.uid;
|
||||
|
||||
// Disable deployment protection (Vercel Authentication)
|
||||
const projectId = deployment.projectId || projectName;
|
||||
await fetch(`${VERCEL_API}/v9/projects/${projectId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: `Bearer ${vercelToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ ssoProtection: null }),
|
||||
});
|
||||
|
||||
// Poll until deployment is ready
|
||||
const ready = await pollDeployment(deploymentId, vercelToken);
|
||||
const deployUrl = `https://${ready.url}`;
|
||||
|
||||
// Create proxy pool entry with type vercel
|
||||
const proxyPool = await createProxyPool({
|
||||
name: projectName,
|
||||
proxyUrl: deployUrl,
|
||||
type: "vercel",
|
||||
noProxy: "",
|
||||
isActive: true,
|
||||
strictProxy: false,
|
||||
});
|
||||
|
||||
return NextResponse.json({ proxyPool, deployUrl }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.log("Error deploying Vercel relay:", error);
|
||||
return NextResponse.json({ error: error.message || "Deploy failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user