BREAKING CHANGE(ts-api,rustproxy): remove deprecated TypeScript protocol and utility exports while hardening QUIC, HTTP/3, WebSocket, and rate limiter cleanup paths

This commit is contained in:
2026-03-21 22:23:38 +00:00
parent 33fdf42a70
commit fc04a0210b
78 changed files with 331 additions and 10754 deletions

View File

@@ -274,6 +274,12 @@ export class SocketHandlerServer {
backend.pipe(socket);
});
// Track backend socket for cleanup on stop()
this.activeSockets.add(backend);
backend.on('close', () => {
this.activeSockets.delete(backend);
});
// Connect timeout: if backend doesn't connect within 30s, destroy both
backend.setTimeout(30_000);

View File

@@ -7,9 +7,54 @@
import * as plugins from '../../../../plugins.js';
import type { IRouteConfig, TPortRange, IRouteContext } from '../../models/route-types.js';
import { ProtocolDetector } from '../../../../detection/index.js';
import { createSocketTracker } from '../../../../core/utils/socket-tracker.js';
/**
* Minimal HTTP request parser for socket handlers.
* Parses method, path, and optionally headers from a raw buffer.
*/
function parseHttpRequest(data: Buffer, extractHeaders: boolean = false): {
method: string;
path: string;
headers: Record<string, string>;
isComplete: boolean;
body?: string;
} | null {
const str = data.toString('utf8');
const headerEnd = str.indexOf('\r\n\r\n');
const isComplete = headerEnd !== -1;
const headerSection = isComplete ? str.slice(0, headerEnd) : str;
const lines = headerSection.split('\r\n');
const requestLine = lines[0];
if (!requestLine) return null;
const parts = requestLine.split(' ');
if (parts.length < 2) return null;
const method = parts[0];
const path = parts[1];
// Quick check: valid HTTP method
const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE'];
if (!validMethods.includes(method)) return null;
const headers: Record<string, string> = {};
if (extractHeaders) {
for (let i = 1; i < lines.length; i++) {
const colonIdx = lines[i].indexOf(':');
if (colonIdx > 0) {
const name = lines[i].slice(0, colonIdx).trim().toLowerCase();
const value = lines[i].slice(colonIdx + 1).trim();
headers[name] = value;
}
}
}
const body = isComplete ? str.slice(headerEnd + 4) : undefined;
return { method, path, headers, isComplete, body };
}
/**
* Pre-built socket handlers for common use cases
*/
@@ -104,30 +149,19 @@ export const SocketHandlers = {
/**
* HTTP redirect handler
* Uses the centralized detection module for HTTP parsing
*/
httpRedirect: (locationTemplate: string, statusCode: number = 301) => (socket: plugins.net.Socket, context: IRouteContext) => {
const tracker = createSocketTracker(socket);
const connectionId = ProtocolDetector.createConnectionId({
socketId: context.connectionId || `${Date.now()}-${Math.random()}`
});
const handleData = async (data: Buffer) => {
// Use detection module for parsing
const detectionResult = await ProtocolDetector.detectWithConnectionTracking(
data,
connectionId,
{ extractFullHeaders: false } // We only need method and path
);
if (detectionResult.protocol === 'http' && detectionResult.connectionInfo.path) {
const method = detectionResult.connectionInfo.method || 'GET';
const path = detectionResult.connectionInfo.path || '/';
const handleData = (data: Buffer) => {
const parsed = parseHttpRequest(data);
if (parsed) {
const path = parsed.path || '/';
const domain = context.domain || 'localhost';
const port = context.port;
let finalLocation = locationTemplate
const finalLocation = locationTemplate
.replace('{domain}', domain)
.replace('{port}', String(port))
.replace('{path}', path)
@@ -146,18 +180,13 @@ export const SocketHandlers = {
socket.write(response);
} else {
// Not a valid HTTP request, close connection
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
}
socket.end();
// Clean up detection state
ProtocolDetector.cleanupConnections();
// Clean up all tracked resources
tracker.cleanup();
};
// Use tracker to manage the listener
socket.once('data', handleData);
tracker.addListener('error', (err) => {
@@ -171,45 +200,31 @@ export const SocketHandlers = {
/**
* HTTP server handler for ACME challenges and other HTTP needs
* Uses the centralized detection module for HTTP parsing
*/
httpServer: (handler: (req: { method: string; url: string; headers: Record<string, string>; body?: string }, res: { status: (code: number) => void; header: (name: string, value: string) => void; send: (data: string) => void; end: () => void }) => void) => (socket: plugins.net.Socket, context: IRouteContext) => {
const tracker = createSocketTracker(socket);
let requestParsed = false;
let responseTimer: NodeJS.Timeout | null = null;
const connectionId = ProtocolDetector.createConnectionId({
socketId: context.connectionId || `${Date.now()}-${Math.random()}`
});
const processData = async (data: Buffer) => {
if (requestParsed) return; // Only handle the first request
const processData = (data: Buffer) => {
if (requestParsed) return;
// Use HttpDetector for parsing
const detectionResult = await ProtocolDetector.detectWithConnectionTracking(
data,
connectionId,
{ extractFullHeaders: true }
);
const parsed = parseHttpRequest(data, true);
if (detectionResult.protocol !== 'http' || !detectionResult.isComplete) {
// Not a complete HTTP request yet
return;
if (!parsed || !parsed.isComplete) {
return; // Not a complete HTTP request yet
}
requestParsed = true;
// Remove data listener after parsing request
socket.removeListener('data', processData);
const connInfo = detectionResult.connectionInfo;
// Create request object from detection result
const req = {
method: connInfo.method || 'GET',
url: connInfo.path || '/',
headers: connInfo.headers || {},
body: detectionResult.remainingBuffer?.toString() || ''
method: parsed.method,
url: parsed.path,
headers: parsed.headers,
body: parsed.body || ''
};
// Create response object
let statusCode = 200;
const responseHeaders: Record<string, string> = {};
let ended = false;
@@ -225,7 +240,6 @@ export const SocketHandlers = {
if (ended) return;
ended = true;
// Clear response timer since we're sending now
if (responseTimer) {
clearTimeout(responseTimer);
responseTimer = null;
@@ -261,26 +275,22 @@ export const SocketHandlers = {
try {
handler(req, res);
// Ensure response is sent even if handler doesn't call send()
responseTimer = setTimeout(() => {
if (!ended) {
res.send('');
}
responseTimer = null;
}, 1000);
// Track and unref the timer
tracker.addTimer(responseTimer);
} catch (error) {
if (!ended) {
res.status(500);
res.send('Internal Server Error');
}
// Use safeDestroy for error cases
tracker.safeDestroy(error instanceof Error ? error : new Error('Handler error'));
}
};
// Use tracker to manage listeners
tracker.addListener('data', processData);
tracker.addListener('error', (err) => {
@@ -290,14 +300,10 @@ export const SocketHandlers = {
});
tracker.addListener('close', () => {
// Clear any pending response timer
if (responseTimer) {
clearTimeout(responseTimer);
responseTimer = null;
}
// Clean up detection state
ProtocolDetector.cleanupConnections();
// Clean up all tracked resources
tracker.cleanup();
});
}
@@ -305,11 +311,6 @@ export const SocketHandlers = {
/**
* Create a socket handler route configuration
* @param domains Domain(s) to match
* @param ports Port(s) to listen on
* @param handler Socket handler function
* @param options Additional route options
* @returns Route configuration object
*/
export function createSocketHandlerRoute(
domains: string | string[],