1
0
cvsa-legacy/packages/cf-worker/src/index.ts

232 lines
5.2 KiB
TypeScript

/**
* Cloudflare Worker Proxy
*
* Accepts POST requests with body format:
* {
* url: string,
* headers: object
* }
*
* Always sends GET requests to the target URL with the specified headers.
* Returns JSON response with data and time fields.
*/
import { connect } from "cloudflare:sockets";
import { HttpParser, MessageType } from '@alikia/http-parser';
interface ProxyConfig {
TIMEOUT_MS: number;
}
interface ProxyRequest {
url: string;
headers?: Record<string, string>;
}
interface ParsedHeaders {
status: number;
statusText: string;
headers: Headers;
headerEnd: number;
}
const CONFIG: ProxyConfig = {
TIMEOUT_MS: 5000,
};
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array {
const total = arrays.reduce((sum, arr) => sum + arr.length, 0);
const result = new Uint8Array(total);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
function parseHttpHeaders(buff: Uint8Array): ParsedHeaders | null {
const text = decoder.decode(buff);
const headerEnd = text.indexOf("\r\n\r\n");
if (headerEnd === -1) return null;
const lines = text.slice(0, headerEnd).split("\r\n");
const statusMatch = lines[0].match(/HTTP\/1\.[01] (\d+) (.*)/);
if (!statusMatch) throw new Error("Invalid status line");
const headers = new Headers();
for (let i = 1; i < lines.length; i++) {
const idx = lines[i].indexOf(": ");
if (idx !== -1) {
headers.append(lines[i].slice(0, idx), lines[i].slice(idx + 2));
}
}
return {
headerEnd,
headers,
status: Number(statusMatch[1]),
statusText: statusMatch[2],
};
}
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) => setTimeout(() => reject(new Error("Request timeout")), ms)),
]);
}
interface ProxyResponseData {
data: string;
time: number;
}
async function handleSocket(
dstUrl: string,
customHeaders: Record<string, string>,
requestTime: number
): Promise<ProxyResponseData> {
const targetUrl = new URL(dstUrl);
const port = targetUrl.protocol === "https:" ? 443 : 80;
const socket = connect(
{
hostname: targetUrl.hostname,
port: port,
},
{ allowHalfOpen: false, secureTransport: targetUrl.protocol === "https:" ? "on" : "off" }
);
const writer = socket.writable.getWriter();
const headers = new Headers(customHeaders);
headers.set("Host", targetUrl.hostname);
headers.set("Connection", "close");
if (!headers.has("Accept-Encoding")) {
headers.set("Accept-Encoding", "identity");
}
const requestLine =
`GET ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n` +
Array.from(headers.entries())
.map(([k, v]) => `${k}: ${v}`)
.join("\r\n") +
"\r\n\r\n";
await writer.write(encoder.encode(requestLine));
const reader = socket.readable.getReader();
const buffer: Uint8Array[] = [];
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer.push(value);
}
const rawContent = concatUint8Arrays(...buffer)
const parser = new HttpParser();
const parsed = parser.parse(rawContent);
for (const msg of parsed) {
if (msg.type === MessageType.RESPONSE) {
return {
data: new TextDecoder().decode(msg.body),
time: Math.floor((requestTime + Date.now()) / 2),
}
}
}
throw new Error("Invalid response");
}
async function handleFetch(
dstUrl: string,
customHeaders: Record<string, string>,
requestTime: number
): Promise<ProxyResponseData> {
const response = await fetch(dstUrl, {
headers: customHeaders,
method: "GET",
});
const responseTime = Date.now();
const combinedTime = Math.floor((requestTime + responseTime) / 2);
const data = await response.text();
return {
data,
time: combinedTime,
};
}
function createJsonResponse(data: ProxyResponseData): Response {
return new Response(JSON.stringify(data), {
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json",
},
});
}
function createErrorResponse(message: string, status: number): Response {
return new Response(
JSON.stringify({
data: "",
error: message,
time: Date.now(),
}),
{
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json",
},
status,
}
);
}
export default {
async fetch(request: Request, _env: Env, _ctx: ExecutionContext): Promise<Response> {
if (request.method !== "POST") {
return createErrorResponse("Method not allowed", 405);
}
let targetUrl: string;
let customHeaders: Record<string, string>;
const requestTime = Date.now();
try {
const body = (await request.json()) as ProxyRequest;
targetUrl = body.url;
new URL(targetUrl);
customHeaders = body.headers || {};
} catch {
return createErrorResponse("Invalid request", 400);
}
try {
const data = await withTimeout(
handleSocket(targetUrl, customHeaders, requestTime),
CONFIG.TIMEOUT_MS
);
return createJsonResponse(data);
} catch {
try {
const data = await withTimeout(
handleFetch(targetUrl, customHeaders, requestTime),
CONFIG.TIMEOUT_MS
);
return createJsonResponse(data);
} catch {
return createErrorResponse("Socket timeout", 504);
}
}
},
};