Files
2026-07-13 13:39:12 +08:00

672 lines
18 KiB
JavaScript

import { createHash, randomUUID } from "node:crypto";
import { STATUS_CODES } from "node:http";
export const WS_PUBLIC_PATHS = new Set(["/v1/ws", "/api/v1/ws"]);
export const WS_ALLOWED_ENDPOINTS = new Set([
"/v1/chat/completions",
"/api/v1/chat/completions",
"/v1/messages",
"/api/v1/messages",
"/v1/responses",
"/api/v1/responses",
"/v1/completions",
"/api/v1/completions",
]);
const HANDSHAKE_PATH = "/api/v1/ws";
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
const WS_QUERY_TOKEN_KEYS = ["api_key", "token", "access_token"];
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
function isText(value) {
return typeof value === "string" && value.length > 0;
}
function jsonStringifySafe(value) {
try {
return JSON.stringify(value);
} catch {
return JSON.stringify({
type: "protocol.error",
code: "serialization_failed",
message: "Failed to serialize WebSocket payload",
});
}
}
function encodeWsFrame(opcode, payload = Buffer.alloc(0)) {
const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
const length = payloadBuffer.length;
let header;
if (length < 126) {
header = Buffer.allocUnsafe(2);
header[1] = length;
} else if (length <= 0xffff) {
header = Buffer.allocUnsafe(4);
header[1] = 126;
header.writeUInt16BE(length, 2);
} else {
header = Buffer.allocUnsafe(10);
header[1] = 127;
header.writeBigUInt64BE(BigInt(length), 2);
}
header[0] = 0x80 | (opcode & 0x0f);
return Buffer.concat([header, payloadBuffer]);
}
function decodeClientFrames(buffer) {
const frames = [];
let offset = 0;
while (buffer.length - offset >= 2) {
const byte1 = buffer[offset];
const byte2 = buffer[offset + 1];
const fin = (byte1 & 0x80) !== 0;
const opcode = byte1 & 0x0f;
const masked = (byte2 & 0x80) !== 0;
let payloadLength = byte2 & 0x7f;
let headerLength = 2;
if (!masked) {
throw new Error("Client WebSocket frames must be masked");
}
if (payloadLength === 126) {
if (buffer.length - offset < 4) break;
payloadLength = buffer.readUInt16BE(offset + 2);
headerLength = 4;
} else if (payloadLength === 127) {
if (buffer.length - offset < 10) break;
const bigLength = buffer.readBigUInt64BE(offset + 2);
if (bigLength > BigInt(Number.MAX_SAFE_INTEGER)) {
throw new Error("WebSocket payload too large");
}
payloadLength = Number(bigLength);
headerLength = 10;
}
const totalLength = headerLength + 4 + payloadLength;
if (buffer.length - offset < totalLength) break;
const mask = buffer.subarray(offset + headerLength, offset + headerLength + 4);
const payload = Buffer.from(buffer.subarray(offset + headerLength + 4, offset + totalLength));
for (let index = 0; index < payload.length; index += 1) {
payload[index] ^= mask[index % 4];
}
frames.push({ fin, opcode, payload });
offset += totalLength;
}
return {
frames,
remaining: buffer.subarray(offset),
};
}
function writeHttpError(socket, status, body, headers = {}) {
if (!socket.writable || socket.destroyed) return;
const bodyBuffer = Buffer.from(body || "", "utf8");
const statusText = STATUS_CODES[status] || "Error";
const responseHeaders = {
Connection: "close",
"Content-Length": String(bodyBuffer.length),
"Content-Type": "application/json; charset=utf-8",
...headers,
};
const head = [
`HTTP/1.1 ${status} ${statusText}`,
...Object.entries(responseHeaders).map(([name, value]) => `${name}: ${value}`),
"",
"",
].join("\r\n");
socket.write(head);
socket.end(bodyBuffer);
}
function isWsPath(pathname) {
return WS_PUBLIC_PATHS.has(pathname);
}
function normalizeEndpoint(rawEndpoint) {
const endpoint = isText(rawEndpoint) ? rawEndpoint : "/v1/chat/completions";
let parsed;
try {
parsed = new URL(endpoint, "http://omniroute.local");
} catch {
return null;
}
if (parsed.origin !== "http://omniroute.local") {
return null;
}
if (!WS_ALLOWED_ENDPOINTS.has(parsed.pathname)) {
return null;
}
return `${parsed.pathname}${parsed.search}`;
}
function getForwardHeaders(requestUrl, requestHeaders) {
const headers = {
accept: "text/event-stream",
"content-type": "application/json",
};
const authorization = requestHeaders.authorization;
if (isText(authorization)) {
headers.authorization = authorization;
} else {
const url = new URL(requestUrl, "http://omniroute.local");
for (const key of WS_QUERY_TOKEN_KEYS) {
const value = url.searchParams.get(key);
if (isText(value)) {
headers.authorization = `Bearer ${value.trim()}`;
break;
}
}
}
const cookie = requestHeaders.cookie;
if (isText(cookie)) {
headers.cookie = cookie;
}
const origin = requestHeaders.origin;
if (isText(origin)) {
headers.origin = origin;
}
return headers;
}
async function performHandshake(fetchImpl, baseUrl, requestUrl, requestHeaders) {
const incomingUrl = new URL(requestUrl, baseUrl);
const handshakeUrl = new URL(HANDSHAKE_PATH, baseUrl);
for (const [key, value] of incomingUrl.searchParams.entries()) {
handshakeUrl.searchParams.set(key, value);
}
handshakeUrl.searchParams.set("handshake", "1");
const response = await fetchImpl(handshakeUrl, {
method: "GET",
headers: {
authorization: requestHeaders.authorization || "",
cookie: requestHeaders.cookie || "",
origin: requestHeaders.origin || "",
"x-forwarded-for": requestHeaders["x-forwarded-for"] || "",
},
});
const bodyText = await response.text();
let bodyJson = null;
try {
bodyJson = bodyText ? JSON.parse(bodyText) : null;
} catch {
bodyJson = null;
}
return {
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
bodyText,
bodyJson,
ok: response.ok,
};
}
class WebSocketSession {
constructor(options) {
this.baseUrl = options.baseUrl;
this.fetchImpl = options.fetchImpl;
this.idleTimeoutMs = options.idleTimeoutMs;
this.pingIntervalMs = options.pingIntervalMs;
this.socket = options.socket;
this.requestHeaders = options.requestHeaders;
this.requestUrl = options.requestUrl;
this.sessionId = randomUUID();
this.closed = false;
this.buffer = Buffer.alloc(0);
this.fragmentOpcode = null;
this.fragmentParts = [];
this.activeRequests = new Map();
this.lastSeenAt = Date.now();
this.pingTimer = setInterval(() => {
if (this.closed) return;
const idleForMs = Date.now() - this.lastSeenAt;
if (idleForMs >= this.idleTimeoutMs) {
this.close(1001, "idle_timeout");
return;
}
this.sendFrame(0x9);
}, this.pingIntervalMs);
this.socket.setNoDelay(true);
this.socket.on("data", (chunk) => {
this.onData(chunk).catch((error) => {
this.sendProtocolError(
"frame_decode_failed",
error instanceof Error ? error.message : String(error)
);
});
});
this.socket.on("close", () => this.dispose());
this.socket.on("end", () => this.dispose());
this.socket.on("error", () => this.dispose());
}
sendFrame(opcode, payload) {
if (this.closed || this.socket.destroyed) return;
this.socket.write(encodeWsFrame(opcode, payload));
}
sendJson(payload) {
this.sendFrame(0x1, Buffer.from(jsonStringifySafe(payload), "utf8"));
}
sendProtocolError(code, message, id = null) {
this.sendJson({
type: "protocol.error",
code,
id,
message,
});
}
async onData(chunk) {
this.lastSeenAt = Date.now();
this.buffer = Buffer.concat([this.buffer, chunk]);
const parsed = decodeClientFrames(this.buffer);
this.buffer = parsed.remaining;
for (const frame of parsed.frames) {
await this.handleFrame(frame);
}
}
async handleFrame(frame) {
switch (frame.opcode) {
case 0x0:
if (this.fragmentOpcode === null) {
this.sendProtocolError("unexpected_continuation", "Unexpected continuation frame");
return;
}
this.fragmentParts.push(frame.payload);
if (frame.fin) {
const payload = Buffer.concat(this.fragmentParts);
const opcode = this.fragmentOpcode;
this.fragmentOpcode = null;
this.fragmentParts = [];
await this.handleDataFrame(opcode, payload);
}
return;
case 0x1:
case 0x2:
if (!frame.fin) {
this.fragmentOpcode = frame.opcode;
this.fragmentParts = [frame.payload];
return;
}
await this.handleDataFrame(frame.opcode, frame.payload);
return;
case 0x8:
this.close();
return;
case 0x9:
this.sendFrame(0xa, frame.payload);
return;
case 0xa:
this.lastSeenAt = Date.now();
return;
default:
this.sendProtocolError("unsupported_opcode", `Unsupported opcode ${frame.opcode}`);
}
}
async handleDataFrame(opcode, payload) {
if (opcode !== 0x1) {
this.sendProtocolError("unsupported_payload", "Only UTF-8 text messages are supported");
return;
}
const raw = textDecoder.decode(payload);
let message;
try {
message = JSON.parse(raw);
} catch {
this.sendProtocolError("invalid_json", "WebSocket message must be valid JSON");
return;
}
await this.handleMessage(message);
}
async handleMessage(message) {
if (!message || typeof message !== "object" || Array.isArray(message)) {
this.sendProtocolError("invalid_envelope", "WebSocket message must be an object");
return;
}
if (message.type === "ping") {
this.sendJson({ type: "pong", sessionId: this.sessionId });
return;
}
if (message.type === "cancel") {
const requestId = isText(message.id) ? message.id : null;
if (!requestId) {
this.sendProtocolError("invalid_cancel", "cancel envelopes require a string id");
return;
}
const active = this.activeRequests.get(requestId);
if (!active) {
this.sendProtocolError(
"unknown_request",
"No active request matches the provided id",
requestId
);
return;
}
active.abortController.abort();
return;
}
if (message.type !== "request") {
this.sendProtocolError(
"unsupported_type",
"Supported message types are request, cancel, and ping"
);
return;
}
const requestId = isText(message.id) ? message.id : null;
if (!requestId) {
this.sendProtocolError("invalid_request_id", "request envelopes require a non-empty id");
return;
}
if (this.activeRequests.has(requestId)) {
this.sendProtocolError(
"duplicate_request",
"A request with this id is already in flight",
requestId
);
return;
}
if (!message.payload || typeof message.payload !== "object" || Array.isArray(message.payload)) {
this.sendProtocolError(
"invalid_payload",
"request envelopes require an object payload",
requestId
);
return;
}
const endpoint = normalizeEndpoint(message.endpoint);
if (!endpoint) {
this.sendProtocolError(
"invalid_endpoint",
"Endpoint must target a supported /v1 chat surface",
requestId
);
return;
}
const requestPayload = {
...message.payload,
stream: message.payload.stream === undefined ? true : message.payload.stream,
};
const abortController = new AbortController();
this.activeRequests.set(requestId, { abortController });
this.executeRequest(requestId, endpoint, requestPayload, abortController).catch((error) => {
this.sendJson({
type: abortController.signal.aborted ? "response.cancelled" : "response.error",
id: requestId,
code: abortController.signal.aborted ? "client_cancelled" : "request_failed",
message: error instanceof Error ? error.message : String(error),
});
this.activeRequests.delete(requestId);
});
}
async executeRequest(requestId, endpoint, payload, abortController) {
const headers = {
...this.requestHeaders,
accept: payload.stream === false ? "application/json" : "text/event-stream",
"content-type": "application/json",
"x-omniroute-ws-session-id": this.sessionId,
"x-omniroute-ws-request-id": requestId,
};
const response = await this.fetchImpl(new URL(endpoint, this.baseUrl), {
method: "POST",
headers,
body: JSON.stringify(payload),
signal: abortController.signal,
});
const contentType = response.headers.get("content-type") || "";
this.sendJson({
type: "response.start",
id: requestId,
status: response.status,
ok: response.ok,
contentType,
endpoint,
});
if (contentType.includes("text/event-stream") && response.body) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
if (chunk) {
this.sendJson({
type: "response.chunk",
id: requestId,
chunk,
});
}
}
const tail = decoder.decode();
if (tail) {
this.sendJson({
type: "response.chunk",
id: requestId,
chunk: tail,
});
}
} finally {
this.activeRequests.delete(requestId);
}
this.sendJson({
type: response.ok ? "response.completed" : "response.error",
id: requestId,
status: response.status,
ok: response.ok,
});
return;
}
const bodyText = await response.text();
let body = bodyText;
try {
body = bodyText ? JSON.parse(bodyText) : null;
} catch {
body = bodyText;
}
this.activeRequests.delete(requestId);
this.sendJson({
type: response.ok ? "response.output" : "response.error",
id: requestId,
status: response.status,
ok: response.ok,
body,
});
this.sendJson({
type: "response.completed",
id: requestId,
status: response.status,
ok: response.ok,
});
}
close(code = 1000, reason = "normal_closure") {
if (this.closed) return;
this.closed = true;
clearInterval(this.pingTimer);
for (const active of this.activeRequests.values()) {
active.abortController.abort();
}
this.activeRequests.clear();
const reasonBuffer = Buffer.from(reason, "utf8");
const payload = Buffer.allocUnsafe(2 + reasonBuffer.length);
payload.writeUInt16BE(code, 0);
reasonBuffer.copy(payload, 2);
this.sendFrame(0x8, payload);
this.socket.end();
setTimeout(() => {
if (!this.socket.destroyed) {
this.socket.destroy();
}
}, 50).unref?.();
}
dispose() {
if (this.closed) return;
this.closed = true;
clearInterval(this.pingTimer);
for (const active of this.activeRequests.values()) {
active.abortController.abort();
}
this.activeRequests.clear();
}
}
export function createOmnirouteWsBridge({
baseUrl,
fetchImpl = fetch,
pingIntervalMs = 25000,
idleTimeoutMs = 90000,
} = {}) {
if (!isText(baseUrl)) {
throw new Error("createOmnirouteWsBridge requires a baseUrl");
}
return {
isWsPath,
async handleUpgrade(req, socket, head) {
const pathname = new URL(req.url || "/", baseUrl).pathname;
if (!isWsPath(pathname)) {
return false;
}
const upgradeHeader = String(req.headers.upgrade || "").toLowerCase();
if (upgradeHeader !== "websocket") {
writeHttpError(
socket,
426,
JSON.stringify({
error: {
message: "Upgrade Required",
code: "upgrade_required",
},
}),
{ Upgrade: "websocket" }
);
return true;
}
try {
const handshake = await performHandshake(fetchImpl, baseUrl, req.url || "/", req.headers);
if (!handshake.ok) {
writeHttpError(socket, handshake.status, handshake.bodyText || "{}", handshake.headers);
return true;
}
const wsKey = req.headers["sec-websocket-key"];
if (!isText(wsKey)) {
writeHttpError(
socket,
400,
JSON.stringify({
error: {
message: "Missing sec-websocket-key header",
code: "bad_websocket_handshake",
},
})
);
return true;
}
const acceptKey = createHash("sha1").update(`${wsKey}${WS_GUID}`).digest("base64");
const headers = [
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${acceptKey}`,
"",
"",
].join("\r\n");
socket.write(headers);
if (head && head.length > 0) {
socket.unshift(head);
}
const session = new WebSocketSession({
baseUrl,
fetchImpl,
idleTimeoutMs,
pingIntervalMs,
socket,
requestUrl: req.url || pathname,
requestHeaders: getForwardHeaders(req.url || pathname, req.headers),
});
session.sendJson({
type: "session.ready",
sessionId: session.sessionId,
path: handshake.bodyJson?.path || pathname,
wsAuth: handshake.bodyJson?.wsAuth === true,
authenticated: handshake.bodyJson?.authenticated === true,
authType: handshake.bodyJson?.authType || "none",
});
return true;
} catch (error) {
writeHttpError(
socket,
500,
JSON.stringify({
error: {
message: error instanceof Error ? error.message : String(error),
code: "websocket_bridge_failed",
},
})
);
return true;
}
},
};
}