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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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[],
|
||||
|
||||
Reference in New Issue
Block a user