262 lines
8.7 KiB
TypeScript
262 lines
8.7 KiB
TypeScript
import * as plugins from '../../../plugins.js';
|
|
import type { IRouteConfig } from '../../smart-proxy/models/route-types.js';
|
|
import type { IConnectionRecord } from '../../smart-proxy/models/interfaces.js';
|
|
import type { ILogger } from '../models/types.js';
|
|
import { createLogger } from '../models/types.js';
|
|
import type { IRouteContext } from '../../../core/models/route-context.js';
|
|
import { HttpStatus, getStatusText } from '../models/http-types.js';
|
|
|
|
export interface IStaticHandlerContext {
|
|
connectionId: string;
|
|
connectionManager: any; // Avoid circular deps
|
|
settings: any;
|
|
logger?: ILogger;
|
|
}
|
|
|
|
/**
|
|
* Handles static routes including ACME challenges
|
|
*/
|
|
export class StaticHandler {
|
|
/**
|
|
* Handle static routes
|
|
*/
|
|
public static async handleStatic(
|
|
socket: plugins.net.Socket,
|
|
route: IRouteConfig,
|
|
context: IStaticHandlerContext,
|
|
record: IConnectionRecord,
|
|
initialChunk?: Buffer
|
|
): Promise<void> {
|
|
const { connectionId, connectionManager, settings } = context;
|
|
const logger = context.logger || createLogger(settings.logLevel || 'info');
|
|
|
|
if (!route.action.handler) {
|
|
logger.error(`[${connectionId}] Static route '${route.name}' has no handler`);
|
|
socket.end();
|
|
connectionManager.cleanupConnection(record, 'no_handler');
|
|
return;
|
|
}
|
|
|
|
let buffer = Buffer.alloc(0);
|
|
let processingData = false;
|
|
|
|
const handleHttpData = async (chunk: Buffer) => {
|
|
// Accumulate the data
|
|
buffer = Buffer.concat([buffer, chunk]);
|
|
|
|
// Prevent concurrent processing of the same buffer
|
|
if (processingData) return;
|
|
processingData = true;
|
|
|
|
try {
|
|
// Process data until we have a complete request or need more data
|
|
await processBuffer();
|
|
} finally {
|
|
processingData = false;
|
|
}
|
|
};
|
|
|
|
const processBuffer = async () => {
|
|
// Look for end of HTTP headers
|
|
const headerEndIndex = buffer.indexOf('\r\n\r\n');
|
|
if (headerEndIndex === -1) {
|
|
// Need more data
|
|
if (buffer.length > 8192) {
|
|
// Prevent excessive buffering
|
|
logger.error(`[${connectionId}] HTTP headers too large`);
|
|
socket.end();
|
|
connectionManager.cleanupConnection(record, 'headers_too_large');
|
|
}
|
|
return; // Wait for more data to arrive
|
|
}
|
|
|
|
// Parse the HTTP request
|
|
const headerBuffer = buffer.slice(0, headerEndIndex);
|
|
const headers = headerBuffer.toString();
|
|
const lines = headers.split('\r\n');
|
|
|
|
if (lines.length === 0) {
|
|
logger.error(`[${connectionId}] Invalid HTTP request`);
|
|
socket.end();
|
|
connectionManager.cleanupConnection(record, 'invalid_request');
|
|
return;
|
|
}
|
|
|
|
// Parse request line
|
|
const requestLine = lines[0];
|
|
const requestParts = requestLine.split(' ');
|
|
if (requestParts.length < 3) {
|
|
logger.error(`[${connectionId}] Invalid HTTP request line`);
|
|
socket.end();
|
|
connectionManager.cleanupConnection(record, 'invalid_request_line');
|
|
return;
|
|
}
|
|
|
|
const [method, path, httpVersion] = requestParts;
|
|
|
|
// Parse headers
|
|
const headersMap: Record<string, string> = {};
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const colonIndex = lines[i].indexOf(':');
|
|
if (colonIndex > 0) {
|
|
const key = lines[i].slice(0, colonIndex).trim().toLowerCase();
|
|
const value = lines[i].slice(colonIndex + 1).trim();
|
|
headersMap[key] = value;
|
|
}
|
|
}
|
|
|
|
// Check for Content-Length to handle request body
|
|
const requestBodyLength = parseInt(headersMap['content-length'] || '0', 10);
|
|
const bodyStartIndex = headerEndIndex + 4; // Skip the \r\n\r\n
|
|
|
|
// If there's a body, ensure we have the full body
|
|
if (requestBodyLength > 0) {
|
|
const totalExpectedLength = bodyStartIndex + requestBodyLength;
|
|
|
|
// If we don't have the complete body yet, wait for more data
|
|
if (buffer.length < totalExpectedLength) {
|
|
// Implement a reasonable body size limit to prevent memory issues
|
|
if (requestBodyLength > 1024 * 1024) {
|
|
// 1MB limit
|
|
logger.error(`[${connectionId}] Request body too large`);
|
|
socket.end();
|
|
connectionManager.cleanupConnection(record, 'body_too_large');
|
|
return;
|
|
}
|
|
return; // Wait for more data
|
|
}
|
|
}
|
|
|
|
// Extract query string if present
|
|
let pathname = path;
|
|
let query: string | undefined;
|
|
const queryIndex = path.indexOf('?');
|
|
if (queryIndex !== -1) {
|
|
pathname = path.slice(0, queryIndex);
|
|
query = path.slice(queryIndex + 1);
|
|
}
|
|
|
|
try {
|
|
// Get request body if present
|
|
let requestBody: Buffer | undefined;
|
|
if (requestBodyLength > 0) {
|
|
requestBody = buffer.slice(bodyStartIndex, bodyStartIndex + requestBodyLength);
|
|
}
|
|
|
|
// Pause socket to prevent data loss during async processing
|
|
socket.pause();
|
|
|
|
// Remove the data listener since we're handling the request
|
|
socket.removeListener('data', handleHttpData);
|
|
|
|
// Build route context with parsed HTTP information
|
|
const context: IRouteContext = {
|
|
port: record.localPort,
|
|
domain: record.lockedDomain || headersMap['host']?.split(':')[0],
|
|
clientIp: record.remoteIP,
|
|
serverIp: socket.localAddress!,
|
|
path: pathname,
|
|
query: query,
|
|
headers: headersMap,
|
|
isTls: record.isTLS,
|
|
tlsVersion: record.tlsVersion,
|
|
routeName: route.name,
|
|
routeId: route.id,
|
|
timestamp: Date.now(),
|
|
connectionId,
|
|
};
|
|
|
|
// Since IRouteContext doesn't have a body property,
|
|
// we need an alternative approach to handle the body
|
|
let response;
|
|
|
|
if (requestBody) {
|
|
if (settings.enableDetailedLogging) {
|
|
logger.info(
|
|
`[${connectionId}] Processing request with body (${requestBody.length} bytes)`
|
|
);
|
|
}
|
|
|
|
// Pass the body as an additional parameter by extending the context object
|
|
// This is not type-safe, but it allows handlers that expect a body to work
|
|
const extendedContext = {
|
|
...context,
|
|
// Provide both raw buffer and string representation
|
|
requestBody: requestBody,
|
|
requestBodyText: requestBody.toString(),
|
|
method: method,
|
|
};
|
|
|
|
// Call the handler with the extended context
|
|
// The handler needs to know to look for the non-standard properties
|
|
response = await route.action.handler(extendedContext as any);
|
|
} else {
|
|
// Call the handler with the standard context
|
|
const extendedContext = {
|
|
...context,
|
|
method: method,
|
|
};
|
|
response = await route.action.handler(extendedContext as any);
|
|
}
|
|
|
|
// Prepare the HTTP response
|
|
const responseHeaders = response.headers || {};
|
|
const contentLength = Buffer.byteLength(response.body || '');
|
|
responseHeaders['Content-Length'] = contentLength.toString();
|
|
|
|
if (!responseHeaders['Content-Type']) {
|
|
responseHeaders['Content-Type'] = 'text/plain';
|
|
}
|
|
|
|
// Build the response
|
|
let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`;
|
|
for (const [key, value] of Object.entries(responseHeaders)) {
|
|
httpResponse += `${key}: ${value}\r\n`;
|
|
}
|
|
httpResponse += '\r\n';
|
|
|
|
// Send response
|
|
socket.write(httpResponse);
|
|
if (response.body) {
|
|
socket.write(response.body);
|
|
}
|
|
socket.end();
|
|
|
|
connectionManager.cleanupConnection(record, 'completed');
|
|
} catch (error) {
|
|
logger.error(`[${connectionId}] Error in static handler: ${error}`);
|
|
|
|
// Send error response
|
|
const errorResponse =
|
|
'HTTP/1.1 500 Internal Server Error\r\n' +
|
|
'Content-Type: text/plain\r\n' +
|
|
'Content-Length: 21\r\n' +
|
|
'\r\n' +
|
|
'Internal Server Error';
|
|
socket.write(errorResponse);
|
|
socket.end();
|
|
|
|
connectionManager.cleanupConnection(record, 'handler_error');
|
|
}
|
|
};
|
|
|
|
// Process initial chunk if provided
|
|
if (initialChunk && initialChunk.length > 0) {
|
|
if (settings.enableDetailedLogging) {
|
|
logger.info(`[${connectionId}] Processing initial data chunk (${initialChunk.length} bytes)`);
|
|
}
|
|
// Process the initial chunk immediately
|
|
handleHttpData(initialChunk);
|
|
}
|
|
|
|
// Listen for additional data
|
|
socket.on('data', handleHttpData);
|
|
|
|
// Ensure cleanup on socket close
|
|
socket.once('close', () => {
|
|
socket.removeListener('data', handleHttpData);
|
|
});
|
|
}
|
|
}
|
|
|