update
This commit is contained in:
parent
2024ea5a69
commit
d8d1bdcd41
@ -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
|
||||
|
@ -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();
|
@ -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));
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user