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

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