This commit is contained in:
Philipp Kunz 2025-05-29 01:00:20 +00:00
parent 2024ea5a69
commit d8d1bdcd41
14 changed files with 190 additions and 791 deletions

View File

@ -478,16 +478,16 @@ const domainRouter = (socket: net.Socket, context: IRouteContext) => {
## Detailed Implementation Tasks
### Step 1: Update TSocketHandler Type (15 minutes)
- [ ] Open `ts/proxies/smart-proxy/models/route-types.ts`
- [ ] Find line 14: `export type TSocketHandler = (socket: plugins.net.Socket) => void | Promise<void>;`
- [ ] Import IRouteContext at top of file: `import type { IRouteContext } from '../../../core/models/route-context.js';`
- [ ] Update TSocketHandler to: `export type TSocketHandler = (socket: plugins.net.Socket, context: IRouteContext) => void | Promise<void>;`
- [ ] Save file
- [x] Open `ts/proxies/smart-proxy/models/route-types.ts`
- [x] Find line 14: `export type TSocketHandler = (socket: plugins.net.Socket) => void | Promise<void>;`
- [x] Import IRouteContext at top of file: `import type { IRouteContext } from '../../../core/models/route-context.js';`
- [x] Update TSocketHandler to: `export type TSocketHandler = (socket: plugins.net.Socket, context: IRouteContext) => void | Promise<void>;`
- [x] Save file
### Step 2: Update Socket Handler Implementation (30 minutes)
- [ ] Open `ts/proxies/smart-proxy/route-connection-handler.ts`
- [ ] Find `handleSocketHandlerAction` method (around line 790)
- [ ] Add route context creation after line 809:
- [x] Open `ts/proxies/smart-proxy/route-connection-handler.ts`
- [x] Find `handleSocketHandlerAction` method (around line 790)
- [x] Add route context creation after line 809:
```typescript
// Create route context for the handler
const routeContext = this.createRouteContext({
@ -502,19 +502,19 @@ const domainRouter = (socket: net.Socket, context: IRouteContext) => {
routeId: route.id,
});
```
- [ ] Update line 812 from `const result = route.action.socketHandler(socket);`
- [ ] To: `const result = route.action.socketHandler(socket, routeContext);`
- [ ] Save file
- [x] Update line 812 from `const result = route.action.socketHandler(socket);`
- [x] To: `const result = route.action.socketHandler(socket, routeContext);`
- [x] Save file
### Step 3: Update Existing Socket Handlers in route-helpers.ts (20 minutes)
- [ ] Open `ts/proxies/smart-proxy/utils/route-helpers.ts`
- [ ] Update `echo` handler (line 856):
- [x] Open `ts/proxies/smart-proxy/utils/route-helpers.ts`
- [x] Update `echo` handler (line 856):
- From: `echo: (socket: plugins.net.Socket) => {`
- To: `echo: (socket: plugins.net.Socket, context: IRouteContext) => {`
- [ ] Update `proxy` handler (line 864):
- [x] Update `proxy` handler (line 864):
- From: `proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket) => {`
- To: `proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket, context: IRouteContext) => {`
- [ ] Update `lineProtocol` handler (line 879):
- [x] Update `lineProtocol` handler (line 879):
- From: `lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket) => {`
- To: `lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket, context: IRouteContext) => {`
- [ ] Update `httpResponse` handler (line 896):
@ -635,11 +635,11 @@ const domainRouter = (socket: net.Socket, context: IRouteContext) => {
});
}
```
- [ ] Save file
- [x] Save file
### Step 9: Update Helper Functions (20 minutes)
- [ ] Still in `route-helpers.ts`
- [ ] Update `createHttpToHttpsRedirect` function (around line 109):
- [x] Still in `route-helpers.ts`
- [x] Update `createHttpToHttpsRedirect` function (around line 109):
- Change the action to use socket handler:
```typescript
action: {
@ -647,8 +647,8 @@ const domainRouter = (socket: net.Socket, context: IRouteContext) => {
socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301)
}
```
- [ ] Delete entire `createStaticFileRoute` function (lines 277-322)
- [ ] Save file
- [x] Delete entire `createStaticFileRoute` function (lines 277-322)
- [x] Save file
### Step 10: Update Test Files (1.5 hours)
#### 10.1 Update Socket Handler Tests

View File

@ -1,98 +0,0 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
import { createHttpToHttpsRedirect } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
// Test that HTTP to HTTPS redirects work correctly
tap.test('should handle HTTP to HTTPS redirects', async (tools) => {
// Create a simple HTTP to HTTPS redirect route
const redirectRoute = createHttpToHttpsRedirect(
'example.com',
443,
{
name: 'HTTP to HTTPS Redirect Test'
}
);
// Verify the route is configured correctly
expect(redirectRoute.action.type).toEqual('redirect');
expect(redirectRoute.action.redirect).toBeTruthy();
expect(redirectRoute.action.redirect?.to).toEqual('https://{domain}:443{path}');
expect(redirectRoute.action.redirect?.status).toEqual(301);
expect(redirectRoute.match.ports).toEqual(80);
expect(redirectRoute.match.domains).toEqual('example.com');
});
tap.test('should handle custom redirect configurations', async (tools) => {
// Create a custom redirect route
const customRedirect: IRouteConfig = {
name: 'custom-redirect',
match: {
ports: [8080],
domains: ['old.example.com']
},
action: {
type: 'redirect',
redirect: {
to: 'https://new.example.com{path}',
status: 302
}
}
};
// Verify the route structure
expect(customRedirect.action.redirect?.to).toEqual('https://new.example.com{path}');
expect(customRedirect.action.redirect?.status).toEqual(302);
});
tap.test('should support multiple redirect scenarios', async (tools) => {
const routes: IRouteConfig[] = [
// HTTP to HTTPS redirect
createHttpToHttpsRedirect(['example.com', 'www.example.com']),
// Custom redirect with different port
{
name: 'custom-port-redirect',
match: {
ports: 8080,
domains: 'api.example.com'
},
action: {
type: 'redirect',
redirect: {
to: 'https://{domain}:8443{path}',
status: 308
}
}
},
// Redirect to different domain entirely
{
name: 'domain-redirect',
match: {
ports: 80,
domains: 'old-domain.com'
},
action: {
type: 'redirect',
redirect: {
to: 'https://new-domain.com{path}',
status: 301
}
}
}
];
// Create SmartProxy with redirect routes
const proxy = new SmartProxy({
routes
});
// Verify all routes are redirect type
routes.forEach(route => {
expect(route.action.type).toEqual('redirect');
expect(route.action.redirect).toBeTruthy();
});
});
export default tap.start();

View File

@ -9,7 +9,7 @@ tap.test('should handle async handler that sets up listeners after delay', async
match: { ports: 7777 },
action: {
type: 'socket-handler',
socketHandler: async (socket) => {
socketHandler: async (socket, context) => {
// Simulate async work BEFORE setting up listeners
await new Promise(resolve => setTimeout(resolve, 50));

View File

@ -12,7 +12,7 @@ tap.test('simple socket handler test', async () => {
},
action: {
type: 'socket-handler',
socketHandler: (socket) => {
socketHandler: (socket, context) => {
console.log('Handler called!');
socket.write('HELLO\n');
socket.end();

View File

@ -15,7 +15,7 @@ tap.test('setup socket handler test', async () => {
},
action: {
type: 'socket-handler',
socketHandler: (socket) => {
socketHandler: (socket, context) => {
console.log('Socket handler called');
// Simple echo server
socket.write('ECHO SERVER\n');
@ -81,7 +81,7 @@ tap.test('should handle async socket handler', async () => {
match: { ports: 9999 },
action: {
type: 'socket-handler',
socketHandler: async (socket) => {
socketHandler: async (socket, context) => {
// Set up data handler first
socket.on('data', async (data) => {
console.log('Async handler received:', data.toString());
@ -134,7 +134,7 @@ tap.test('should handle errors in socket handler', async () => {
match: { ports: 9999 },
action: {
type: 'socket-handler',
socketHandler: (socket) => {
socketHandler: (socket, context) => {
throw new Error('Handler error');
}
}

View File

@ -2,5 +2,4 @@
* HTTP handlers for various route types
*/
export { RedirectHandler } from './redirect-handler.js';
export { StaticHandler } from './static-handler.js';
// Empty - all handlers have been removed

View File

@ -1,105 +0,0 @@
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 { HttpStatus, getStatusText } from '../models/http-types.js';
export interface IRedirectHandlerContext {
connectionId: string;
connectionManager: any; // Avoid circular deps
settings: any;
logger?: ILogger;
}
/**
* Handles HTTP redirect routes
*/
export class RedirectHandler {
/**
* Handle redirect routes
*/
public static async handleRedirect(
socket: plugins.net.Socket,
route: IRouteConfig,
context: IRedirectHandlerContext
): Promise<void> {
const { connectionId, connectionManager, settings } = context;
const logger = context.logger || createLogger(settings.logLevel || 'info');
const action = route.action;
// We should have a redirect configuration
if (!action.redirect) {
logger.error(`[${connectionId}] Redirect action missing redirect configuration`);
socket.end();
connectionManager.cleanupConnection({ id: connectionId }, 'missing_redirect');
return;
}
// For TLS connections, we can't do redirects at the TCP level
// This check should be done before calling this handler
// Wait for the first HTTP request to perform the redirect
const dataListeners: ((chunk: Buffer) => void)[] = [];
const httpDataHandler = (chunk: Buffer) => {
// Remove all data listeners to avoid duplicated processing
for (const listener of dataListeners) {
socket.removeListener('data', listener);
}
// Parse HTTP request to get path
try {
const headersEnd = chunk.indexOf('\r\n\r\n');
if (headersEnd === -1) {
// Not a complete HTTP request, need more data
socket.once('data', httpDataHandler);
dataListeners.push(httpDataHandler);
return;
}
const httpHeaders = chunk.slice(0, headersEnd).toString();
const requestLine = httpHeaders.split('\r\n')[0];
const [method, path] = requestLine.split(' ');
// Extract Host header
const hostMatch = httpHeaders.match(/Host: (.+?)(\r\n|\r|\n|$)/i);
const host = hostMatch ? hostMatch[1].trim() : '';
// Process the redirect URL with template variables
let redirectUrl = action.redirect.to;
redirectUrl = redirectUrl.replace(/\{domain\}/g, host);
redirectUrl = redirectUrl.replace(/\{path\}/g, path || '');
redirectUrl = redirectUrl.replace(/\{port\}/g, socket.localPort?.toString() || '80');
// Prepare the HTTP redirect response
const redirectResponse = [
`HTTP/1.1 ${action.redirect.status} Moved`,
`Location: ${redirectUrl}`,
'Connection: close',
'Content-Length: 0',
'',
'',
].join('\r\n');
if (settings.enableDetailedLogging) {
logger.info(
`[${connectionId}] Redirecting to ${redirectUrl} with status ${action.redirect.status}`
);
}
// Send the redirect response
socket.end(redirectResponse);
connectionManager.initiateCleanupOnce({ id: connectionId }, 'redirect_complete');
} catch (err) {
logger.error(`[${connectionId}] Error processing HTTP redirect: ${err}`);
socket.end();
connectionManager.initiateCleanupOnce({ id: connectionId }, 'redirect_error');
}
};
// Setup the HTTP data handler
socket.once('data', httpDataHandler);
dataListeners.push(httpDataHandler);
}
}

View File

@ -1,261 +0,0 @@
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);
});
}
}

View File

@ -693,48 +693,70 @@ export class SmartCertManager {
path: '/.well-known/acme-challenge/*'
},
action: {
type: 'static',
handler: async (context) => {
// Extract the token from the path
const token = context.path?.split('/').pop();
if (!token) {
return { status: 404, body: 'Not found' };
}
// Create mock request/response objects for SmartAcme
const mockReq = {
url: context.path,
method: 'GET',
headers: context.headers || {}
};
let responseData: any = null;
const mockRes = {
statusCode: 200,
setHeader: (name: string, value: string) => {},
end: (data: any) => {
responseData = data;
type: 'socket-handler',
socketHandler: (socket, context) => {
// Wait for HTTP request data
socket.once('data', async (data) => {
const request = data.toString();
const lines = request.split('\r\n');
const [method, path] = lines[0].split(' ');
// Extract the token from the path
const token = path?.split('/').pop();
if (!token) {
socket.write('HTTP/1.1 404 Not Found\r\n');
socket.write('Content-Type: text/plain\r\n');
socket.write('Content-Length: 9\r\n');
socket.write('Connection: close\r\n');
socket.write('\r\n');
socket.write('Not found');
socket.end();
return;
}
};
// Use SmartAcme's handler
const handled = await new Promise<boolean>((resolve) => {
http01Handler.handleRequest(mockReq as any, mockRes as any, () => {
resolve(false);
});
// Give it a moment to process
setTimeout(() => resolve(true), 100);
});
if (handled && responseData) {
return {
status: mockRes.statusCode,
headers: { 'Content-Type': 'text/plain' },
body: responseData
// Create mock request/response objects for SmartAcme
const mockReq = {
url: path,
method: 'GET',
headers: {}
};
} else {
return { status: 404, body: 'Not found' };
}
let responseData: any = null;
const mockRes = {
statusCode: 200,
setHeader: (name: string, value: string) => {},
end: (data: any) => {
responseData = data;
}
};
// Use SmartAcme's handler
const handled = await new Promise<boolean>((resolve) => {
http01Handler.handleRequest(mockReq as any, mockRes as any, () => {
resolve(false);
});
// Give it a moment to process
setTimeout(() => resolve(true), 100);
});
if (handled && responseData) {
const body = String(responseData);
socket.write(`HTTP/1.1 ${mockRes.statusCode} OK\r\n`);
socket.write('Content-Type: text/plain\r\n');
socket.write(`Content-Length: ${body.length}\r\n`);
socket.write('Connection: close\r\n');
socket.write('\r\n');
socket.write(body);
} else {
socket.write('HTTP/1.1 404 Not Found\r\n');
socket.write('Content-Type: text/plain\r\n');
socket.write('Content-Length: 9\r\n');
socket.write('Connection: close\r\n');
socket.write('\r\n');
socket.write('Not found');
}
socket.end();
});
}
}
};

View File

@ -2,16 +2,20 @@ import * as plugins from '../../../plugins.js';
// Certificate types removed - use local definition
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js';
import type { IRouteContext } from '../../../core/models/route-context.js';
// Re-export IRouteContext for convenience
export type { IRouteContext };
/**
* Supported action types for route configurations
*/
export type TRouteActionType = 'forward' | 'redirect' | 'block' | 'static' | 'socket-handler';
export type TRouteActionType = 'forward' | 'socket-handler';
/**
* Socket handler function type
*/
export type TSocketHandler = (socket: plugins.net.Socket) => void | Promise<void>;
export type TSocketHandler = (socket: plugins.net.Socket, context: IRouteContext) => void | Promise<void>;
/**
* TLS handling modes for route configurations
@ -40,36 +44,6 @@ export interface IRouteMatch {
headers?: Record<string, string | RegExp>; // Match specific HTTP headers
}
/**
* Context provided to port and host mapping functions
*/
export interface IRouteContext {
// Connection information
port: number; // The matched incoming port
domain?: string; // The domain from SNI or Host header
clientIp: string; // The client's IP address
serverIp: string; // The server's IP address
path?: string; // URL path (for HTTP connections)
query?: string; // Query string (for HTTP connections)
headers?: Record<string, string>; // HTTP headers (for HTTP connections)
method?: string; // HTTP method (for HTTP connections)
// TLS information
isTls: boolean; // Whether the connection is TLS
tlsVersion?: string; // TLS version if applicable
// Route information
routeName?: string; // The name of the matched route
routeId?: string; // The ID of the matched route
// Target information (resolved from dynamic mapping)
targetHost?: string | string[]; // The resolved target host(s)
targetPort?: number; // The resolved target port
// Additional properties
timestamp: number; // The request timestamp
connectionId: string; // Unique connection identifier
}
/**
* Target configuration for forwarding
@ -89,15 +63,6 @@ export interface IRouteAcme {
renewBeforeDays?: number; // Days before expiry to renew (default: 30)
}
/**
* Static route handler response
*/
export interface IStaticResponse {
status: number;
headers?: Record<string, string>;
body: string | Buffer;
}
/**
* TLS configuration for route actions
*/
@ -117,14 +82,6 @@ export interface IRouteTls {
sessionTimeout?: number; // TLS session timeout in seconds
}
/**
* Redirect configuration for route actions
*/
export interface IRouteRedirect {
to: string; // URL or template with {domain}, {port}, etc.
status: 301 | 302 | 307 | 308;
}
/**
* Authentication options
*/
@ -270,12 +227,6 @@ export interface IRouteAction {
// TLS handling
tls?: IRouteTls;
// For redirects
redirect?: IRouteRedirect;
// For static files
static?: IRouteStaticFiles;
// WebSocket support
websocket?: IRouteWebSocket;
@ -300,9 +251,6 @@ export interface IRouteAction {
// NFTables-specific options
nftables?: INfTablesOptions;
// Handler function for static routes
handler?: (context: IRouteContext) => Promise<IStaticResponse>;
// Socket handler function (when type is 'socket-handler')
socketHandler?: TSocketHandler;
}

View File

@ -10,7 +10,6 @@ import { HttpProxyBridge } from './http-proxy-bridge.js';
import { TimeoutManager } from './timeout-manager.js';
import { RouteManager } from './route-manager.js';
import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
import { RedirectHandler, StaticHandler } from '../http-proxy/handlers/index.js';
/**
* Handles new connection processing and setup logic with support for route-based configuration
@ -389,16 +388,6 @@ export class RouteConnectionHandler {
case 'forward':
return this.handleForwardAction(socket, record, route, initialChunk);
case 'redirect':
return this.handleRedirectAction(socket, record, route);
case 'block':
return this.handleBlockAction(socket, record, route);
case 'static':
this.handleStaticAction(socket, record, route, initialChunk);
return;
case 'socket-handler':
logger.log('info', `Handling socket-handler action for route ${route.name}`, {
connectionId,
@ -718,73 +707,6 @@ export class RouteConnectionHandler {
}
}
/**
* Handle a redirect action for a route
*/
private handleRedirectAction(
socket: plugins.net.Socket,
record: IConnectionRecord,
route: IRouteConfig
): void {
// For TLS connections, we can't do redirects at the TCP level
if (record.isTLS) {
logger.log('warn', `Cannot redirect TLS connection ${record.id} at TCP level`, {
connectionId: record.id,
component: 'route-handler'
});
socket.end();
this.connectionManager.cleanupConnection(record, 'tls_redirect_error');
return;
}
// Delegate to HttpProxy's RedirectHandler
RedirectHandler.handleRedirect(socket, route, {
connectionId: record.id,
connectionManager: this.connectionManager,
settings: this.settings
});
}
/**
* Handle a block action for a route
*/
private handleBlockAction(
socket: plugins.net.Socket,
record: IConnectionRecord,
route: IRouteConfig
): void {
const connectionId = record.id;
if (this.settings.enableDetailedLogging) {
logger.log('info', `Blocking connection ${connectionId} based on route '${route.name || 'unnamed'}'`, {
connectionId,
routeName: route.name || 'unnamed',
component: 'route-handler'
});
}
// Simply close the connection
socket.end();
this.connectionManager.initiateCleanupOnce(record, 'route_blocked');
}
/**
* Handle a static action for a route
*/
private async handleStaticAction(
socket: plugins.net.Socket,
record: IConnectionRecord,
route: IRouteConfig,
initialChunk?: Buffer
): Promise<void> {
// Delegate to HttpProxy's StaticHandler
await StaticHandler.handleStatic(socket, route, {
connectionId: record.id,
connectionManager: this.connectionManager,
settings: this.settings
}, record, initialChunk);
}
/**
* Handle a socket-handler action for a route
*/
@ -807,9 +729,22 @@ export class RouteConnectionHandler {
return;
}
// Create route context for the handler
const routeContext = this.createRouteContext({
connectionId: record.id,
port: record.localPort,
domain: record.lockedDomain,
clientIp: record.remoteIP,
serverIp: socket.localAddress || '',
isTls: record.isTLS || false,
tlsVersion: record.tlsVersion,
routeName: route.name,
routeId: route.id,
});
try {
// Call the handler
const result = route.action.socketHandler(socket);
// Call the handler with socket AND context
const result = route.action.socketHandler(socket, routeContext);
// Handle async handlers properly
if (result instanceof Promise) {

View File

@ -19,7 +19,6 @@ import {
createWebSocketRoute as createWebSocketPatternRoute,
createLoadBalancerRoute as createLoadBalancerPatternRoute,
createApiGatewayRoute,
createStaticFileServerRoute,
addRateLimiting,
addBasicAuth,
addJwtAuth
@ -29,7 +28,6 @@ export {
createWebSocketPatternRoute,
createLoadBalancerPatternRoute,
createApiGatewayRoute,
createStaticFileServerRoute,
addRateLimiting,
addBasicAuth,
addJwtAuth

View File

@ -11,7 +11,6 @@
* - HTTPS passthrough routes (createHttpsPassthroughRoute)
* - Complete HTTPS servers with redirects (createCompleteHttpsServer)
* - Load balancer routes (createLoadBalancerRoute)
* - Static file server routes (createStaticFileRoute)
* - API routes (createApiRoute)
* - WebSocket routes (createWebSocketRoute)
* - Port mapping routes (createPortMappingRoute, createOffsetPortMappingRoute)
@ -119,11 +118,8 @@ export function createHttpToHttpsRedirect(
// Create route action
const action: IRouteAction = {
type: 'redirect',
redirect: {
to: `https://{domain}:${httpsPort}{path}`,
status: 301
}
type: 'socket-handler',
socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301)
};
// Create the route config
@ -267,60 +263,6 @@ export function createLoadBalancerRoute(
};
}
/**
* Create a static file server route
* @param domains Domain(s) to match
* @param rootDir Root directory path for static files
* @param options Additional route options
* @returns Route configuration object
*/
export function createStaticFileRoute(
domains: string | string[],
rootDir: string,
options: {
indexFiles?: string[];
serveOnHttps?: boolean;
certificate?: 'auto' | { key: string; cert: string };
httpPort?: number | number[];
httpsPort?: number | number[];
name?: string;
[key: string]: any;
} = {}
): IRouteConfig {
// Create route match
const match: IRouteMatch = {
ports: options.serveOnHttps
? (options.httpsPort || 443)
: (options.httpPort || 80),
domains
};
// Create route action
const action: IRouteAction = {
type: 'static',
static: {
root: rootDir,
index: options.indexFiles || ['index.html', 'index.htm']
}
};
// Add TLS configuration if serving on HTTPS
if (options.serveOnHttps) {
action.tls = {
mode: 'terminate',
certificate: options.certificate || 'auto'
};
}
// Create the route config
return {
match,
action,
name: options.name || `Static Files for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
...options
};
}
/**
* Create an API route configuration
* @param domains Domain(s) to match
@ -853,7 +795,7 @@ export const SocketHandlers = {
/**
* Simple echo server handler
*/
echo: (socket: plugins.net.Socket) => {
echo: (socket: plugins.net.Socket, context: IRouteContext) => {
socket.write('ECHO SERVER READY\n');
socket.on('data', data => socket.write(data));
},
@ -861,7 +803,7 @@ export const SocketHandlers = {
/**
* TCP proxy handler
*/
proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket) => {
proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket, context: IRouteContext) => {
const target = plugins.net.connect(targetPort, targetHost);
socket.pipe(target);
target.pipe(socket);
@ -876,7 +818,7 @@ export const SocketHandlers = {
/**
* Line-based protocol handler
*/
lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket) => {
lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket, context: IRouteContext) => {
let buffer = '';
socket.on('data', (data) => {
buffer += data.toString();
@ -893,7 +835,7 @@ export const SocketHandlers = {
/**
* Simple HTTP response handler (for testing)
*/
httpResponse: (statusCode: number, body: string) => (socket: plugins.net.Socket) => {
httpResponse: (statusCode: number, body: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
const response = [
`HTTP/1.1 ${statusCode} ${statusCode === 200 ? 'OK' : 'Error'}`,
'Content-Type: text/plain',
@ -905,5 +847,74 @@ export const SocketHandlers = {
socket.write(response);
socket.end();
},
/**
* Block connection immediately
*/
block: (message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
const finalMessage = message || `Connection blocked from ${context.clientIp}`;
if (finalMessage) {
socket.write(finalMessage);
}
socket.end();
},
/**
* HTTP block response
*/
httpBlock: (statusCode: number = 403, message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
const defaultMessage = `Access forbidden for ${context.domain || context.clientIp}`;
const finalMessage = message || defaultMessage;
const response = [
`HTTP/1.1 ${statusCode} ${finalMessage}`,
'Content-Type: text/plain',
`Content-Length: ${finalMessage.length}`,
'Connection: close',
'',
finalMessage
].join('\r\n');
socket.write(response);
socket.end();
},
/**
* HTTP redirect handler
*/
httpRedirect: (locationTemplate: string, statusCode: number = 301) => (socket: plugins.net.Socket, context: IRouteContext) => {
let buffer = '';
socket.once('data', (data) => {
buffer += data.toString();
const lines = buffer.split('\r\n');
const requestLine = lines[0];
const [method, path] = requestLine.split(' ');
const domain = context.domain || 'localhost';
const port = context.port;
let finalLocation = locationTemplate
.replace('{domain}', domain)
.replace('{port}', String(port))
.replace('{path}', path)
.replace('{clientIp}', context.clientIp);
const message = `Redirecting to ${finalLocation}`;
const response = [
`HTTP/1.1 ${statusCode} ${statusCode === 301 ? 'Moved Permanently' : 'Found'}`,
`Location: ${finalLocation}`,
'Content-Type: text/plain',
`Content-Length: ${message.length}`,
'Connection: close',
'',
message
].join('\r\n');
socket.write(response);
socket.end();
});
}
};

View File

@ -7,6 +7,7 @@
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget } from '../models/route-types.js';
import { mergeRouteConfigs } from './route-utils.js';
import { SocketHandlers } from './route-helpers.js';
/**
* Create a basic HTTP route configuration
@ -112,11 +113,11 @@ export function createHttpToHttpsRedirect(
ports: 80
},
action: {
type: 'redirect',
redirect: {
to: options.preservePath ? 'https://{domain}{path}' : 'https://{domain}',
status: options.redirectCode || 301
}
type: 'socket-handler',
socketHandler: SocketHandlers.httpRedirect(
options.preservePath ? 'https://{domain}{path}' : 'https://{domain}',
options.redirectCode || 301
)
},
name: options.name || `HTTP to HTTPS redirect: ${Array.isArray(domains) ? domains.join(', ') : domains}`
};
@ -214,57 +215,6 @@ export function createApiGatewayRoute(
return mergeRouteConfigs(baseRoute, apiRoute);
}
/**
* Create a static file server route pattern
* @param domains Domain(s) to match
* @param rootDirectory Root directory for static files
* @param options Additional route options
* @returns Static file server route configuration
*/
export function createStaticFileServerRoute(
domains: string | string[],
rootDirectory: string,
options: {
useTls?: boolean;
certificate?: 'auto' | { key: string; cert: string };
indexFiles?: string[];
cacheControl?: string;
path?: string;
[key: string]: any;
} = {}
): IRouteConfig {
// Create base route with static action
const baseRoute: IRouteConfig = {
match: {
domains,
ports: options.useTls ? 443 : 80,
path: options.path || '/'
},
action: {
type: 'static',
static: {
root: rootDirectory,
index: options.indexFiles || ['index.html', 'index.htm'],
headers: {
'Cache-Control': options.cacheControl || 'public, max-age=3600'
}
}
},
name: options.name || `Static Server: ${Array.isArray(domains) ? domains.join(', ') : domains}`,
priority: options.priority || 50
};
// Add TLS configuration if requested
if (options.useTls) {
baseRoute.action.tls = {
mode: 'terminate',
certificate: options.certificate || 'auto'
};
}
return baseRoute;
}
/**
* Create a WebSocket route pattern
* @param domains Domain(s) to match