@push.rocks/smartproxy 19.5.18 → 19.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/core/models/index.d.ts +2 -0
- package/dist_ts/core/models/index.js +3 -1
- package/dist_ts/core/models/socket-types.d.ts +14 -0
- package/dist_ts/core/models/socket-types.js +15 -0
- package/dist_ts/core/models/wrapped-socket.d.ts +34 -0
- package/dist_ts/core/models/wrapped-socket.js +82 -0
- package/dist_ts/core/routing/index.d.ts +11 -0
- package/dist_ts/core/routing/index.js +17 -0
- package/dist_ts/core/routing/matchers/domain.d.ts +34 -0
- package/dist_ts/core/routing/matchers/domain.js +91 -0
- package/dist_ts/core/routing/matchers/header.d.ts +32 -0
- package/dist_ts/core/routing/matchers/header.js +94 -0
- package/dist_ts/core/routing/matchers/index.d.ts +18 -0
- package/dist_ts/core/routing/matchers/index.js +20 -0
- package/dist_ts/core/routing/matchers/ip.d.ts +53 -0
- package/dist_ts/core/routing/matchers/ip.js +169 -0
- package/dist_ts/core/routing/matchers/path.d.ts +44 -0
- package/dist_ts/core/routing/matchers/path.js +148 -0
- package/dist_ts/core/routing/route-manager.d.ts +88 -0
- package/dist_ts/core/routing/route-manager.js +342 -0
- package/dist_ts/core/routing/route-utils.d.ts +28 -0
- package/dist_ts/core/routing/route-utils.js +67 -0
- package/dist_ts/core/routing/specificity.d.ts +30 -0
- package/dist_ts/core/routing/specificity.js +115 -0
- package/dist_ts/core/routing/types.d.ts +41 -0
- package/dist_ts/core/routing/types.js +5 -0
- package/dist_ts/core/utils/index.d.ts +0 -2
- package/dist_ts/core/utils/index.js +1 -3
- package/dist_ts/core/utils/route-manager.d.ts +0 -30
- package/dist_ts/core/utils/route-manager.js +6 -47
- package/dist_ts/core/utils/route-utils.d.ts +2 -68
- package/dist_ts/core/utils/route-utils.js +21 -218
- package/dist_ts/core/utils/security-utils.js +4 -4
- package/dist_ts/core/utils/socket-utils.d.ts +0 -15
- package/dist_ts/core/utils/socket-utils.js +1 -35
- package/dist_ts/forwarding/handlers/https-terminate-to-http-handler.js +47 -32
- package/dist_ts/forwarding/handlers/https-terminate-to-https-handler.js +51 -32
- package/dist_ts/index.d.ts +2 -5
- package/dist_ts/index.js +5 -11
- package/dist_ts/proxies/http-proxy/http-proxy.d.ts +0 -1
- package/dist_ts/proxies/http-proxy/http-proxy.js +15 -60
- package/dist_ts/proxies/http-proxy/models/types.d.ts +0 -90
- package/dist_ts/proxies/http-proxy/models/types.js +1 -242
- package/dist_ts/proxies/http-proxy/request-handler.d.ts +3 -5
- package/dist_ts/proxies/http-proxy/request-handler.js +20 -171
- package/dist_ts/proxies/http-proxy/websocket-handler.d.ts +2 -5
- package/dist_ts/proxies/http-proxy/websocket-handler.js +15 -23
- package/dist_ts/proxies/index.d.ts +2 -2
- package/dist_ts/proxies/index.js +4 -3
- package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +3 -1
- package/dist_ts/proxies/smart-proxy/connection-manager.js +15 -7
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.d.ts +2 -1
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +5 -2
- package/dist_ts/proxies/smart-proxy/index.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/index.js +2 -2
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +6 -2
- package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +48 -25
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +15 -4
- package/dist_ts/proxies/smart-proxy/utils/route-utils.js +10 -43
- package/dist_ts/routing/router/http-router.d.ts +89 -0
- package/dist_ts/routing/router/http-router.js +205 -0
- package/dist_ts/routing/router/index.d.ts +2 -5
- package/dist_ts/routing/router/index.js +3 -4
- package/package.json +1 -1
- package/readme.delete.md +187 -0
- package/readme.hints.md +210 -1
- package/readme.plan.md +621 -0
- package/readme.routing.md +341 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/models/index.ts +2 -0
- package/ts/core/models/socket-types.ts +21 -0
- package/ts/core/models/wrapped-socket.ts +99 -0
- package/ts/core/routing/index.ts +21 -0
- package/ts/core/routing/matchers/domain.ts +119 -0
- package/ts/core/routing/matchers/header.ts +120 -0
- package/ts/core/routing/matchers/index.ts +22 -0
- package/ts/core/routing/matchers/ip.ts +207 -0
- package/ts/core/routing/matchers/path.ts +184 -0
- package/ts/core/{utils → routing}/route-manager.ts +7 -57
- package/ts/core/routing/route-utils.ts +88 -0
- package/ts/core/routing/specificity.ts +141 -0
- package/ts/core/routing/types.ts +49 -0
- package/ts/core/utils/index.ts +0 -2
- package/ts/core/utils/security-utils.ts +3 -7
- package/ts/core/utils/socket-utils.ts +0 -44
- package/ts/forwarding/handlers/https-terminate-to-http-handler.ts +47 -33
- package/ts/forwarding/handlers/https-terminate-to-https-handler.ts +55 -35
- package/ts/index.ts +4 -14
- package/ts/proxies/http-proxy/http-proxy.ts +13 -68
- package/ts/proxies/http-proxy/models/types.ts +0 -324
- package/ts/proxies/http-proxy/request-handler.ts +15 -186
- package/ts/proxies/http-proxy/websocket-handler.ts +15 -26
- package/ts/proxies/index.ts +3 -2
- package/ts/proxies/smart-proxy/connection-manager.ts +15 -7
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +6 -2
- package/ts/proxies/smart-proxy/index.ts +1 -1
- package/ts/proxies/smart-proxy/models/interfaces.ts +8 -2
- package/ts/proxies/smart-proxy/route-connection-handler.ts +58 -30
- package/ts/proxies/smart-proxy/smart-proxy.ts +15 -3
- package/ts/proxies/smart-proxy/utils/route-utils.ts +11 -49
- package/ts/routing/router/http-router.ts +266 -0
- package/ts/routing/router/index.ts +3 -8
- package/readme.problems.md +0 -170
- package/ts/core/utils/route-utils.ts +0 -312
- package/ts/proxies/smart-proxy/route-manager.ts +0 -554
- package/ts/routing/router/proxy-router.ts +0 -437
- package/ts/routing/router/route-router.ts +0 -482
|
@@ -2,14 +2,17 @@ import * as plugins from '../../plugins.js';
|
|
|
2
2
|
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
|
|
3
3
|
import { logger } from '../../core/utils/logger.js';
|
|
4
4
|
// Route checking functions have been removed
|
|
5
|
-
import type { IRouteConfig, IRouteAction
|
|
5
|
+
import type { IRouteConfig, IRouteAction } from './models/route-types.js';
|
|
6
|
+
import type { IRouteContext } from '../../core/models/route-context.js';
|
|
6
7
|
import { ConnectionManager } from './connection-manager.js';
|
|
7
8
|
import { SecurityManager } from './security-manager.js';
|
|
8
9
|
import { TlsManager } from './tls-manager.js';
|
|
9
10
|
import { HttpProxyBridge } from './http-proxy-bridge.js';
|
|
10
11
|
import { TimeoutManager } from './timeout-manager.js';
|
|
11
|
-
import { RouteManager } from '
|
|
12
|
+
import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
|
|
12
13
|
import { cleanupSocket, createIndependentSocketHandlers, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
|
|
14
|
+
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
|
|
15
|
+
import { getUnderlyingSocket } from '../../core/models/socket-types.js';
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Handles new connection processing and setup logic with support for route-based configuration
|
|
@@ -80,39 +83,52 @@ export class RouteConnectionHandler {
|
|
|
80
83
|
const remoteIP = socket.remoteAddress || '';
|
|
81
84
|
const localPort = socket.localPort || 0;
|
|
82
85
|
|
|
86
|
+
// Always wrap the socket to prepare for potential PROXY protocol
|
|
87
|
+
const wrappedSocket = new WrappedSocket(socket);
|
|
88
|
+
|
|
89
|
+
// If this is from a trusted proxy, log it
|
|
90
|
+
if (this.settings.proxyIPs?.includes(remoteIP)) {
|
|
91
|
+
logger.log('debug', `Connection from trusted proxy ${remoteIP}, PROXY protocol parsing will be enabled`, {
|
|
92
|
+
remoteIP,
|
|
93
|
+
component: 'route-handler'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
// Validate IP against rate limits and connection limits
|
|
84
|
-
|
|
98
|
+
// Note: For wrapped sockets, this will use the underlying socket IP until PROXY protocol is parsed
|
|
99
|
+
const ipValidation = this.securityManager.validateIP(wrappedSocket.remoteAddress || '');
|
|
85
100
|
if (!ipValidation.allowed) {
|
|
86
|
-
logger.log('warn', `Connection rejected`, { remoteIP, reason: ipValidation.reason, component: 'route-handler' });
|
|
87
|
-
cleanupSocket(socket, `rejected-${ipValidation.reason}`, { immediate: true });
|
|
101
|
+
logger.log('warn', `Connection rejected`, { remoteIP: wrappedSocket.remoteAddress, reason: ipValidation.reason, component: 'route-handler' });
|
|
102
|
+
cleanupSocket(wrappedSocket.socket, `rejected-${ipValidation.reason}`, { immediate: true });
|
|
88
103
|
return;
|
|
89
104
|
}
|
|
90
105
|
|
|
91
|
-
// Create a new connection record
|
|
92
|
-
const record = this.connectionManager.createConnection(
|
|
106
|
+
// Create a new connection record with the wrapped socket
|
|
107
|
+
const record = this.connectionManager.createConnection(wrappedSocket);
|
|
93
108
|
if (!record) {
|
|
94
109
|
// Connection was rejected due to limit - socket already destroyed by connection manager
|
|
95
110
|
return;
|
|
96
111
|
}
|
|
97
112
|
const connectionId = record.id;
|
|
98
113
|
|
|
99
|
-
// Apply socket optimizations
|
|
100
|
-
socket
|
|
114
|
+
// Apply socket optimizations (apply to underlying socket)
|
|
115
|
+
const underlyingSocket = wrappedSocket.socket;
|
|
116
|
+
underlyingSocket.setNoDelay(this.settings.noDelay);
|
|
101
117
|
|
|
102
118
|
// Apply keep-alive settings if enabled
|
|
103
119
|
if (this.settings.keepAlive) {
|
|
104
|
-
|
|
120
|
+
underlyingSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
105
121
|
record.hasKeepAlive = true;
|
|
106
122
|
|
|
107
123
|
// Apply enhanced TCP keep-alive options if enabled
|
|
108
124
|
if (this.settings.enableKeepAliveProbes) {
|
|
109
125
|
try {
|
|
110
126
|
// These are platform-specific and may not be available
|
|
111
|
-
if ('setKeepAliveProbes' in
|
|
112
|
-
(
|
|
127
|
+
if ('setKeepAliveProbes' in underlyingSocket) {
|
|
128
|
+
(underlyingSocket as any).setKeepAliveProbes(10);
|
|
113
129
|
}
|
|
114
|
-
if ('setKeepAliveInterval' in
|
|
115
|
-
(
|
|
130
|
+
if ('setKeepAliveInterval' in underlyingSocket) {
|
|
131
|
+
(underlyingSocket as any).setKeepAliveInterval(1000);
|
|
116
132
|
}
|
|
117
133
|
} catch (err) {
|
|
118
134
|
// Ignore errors - these are optional enhancements
|
|
@@ -150,19 +166,19 @@ export class RouteConnectionHandler {
|
|
|
150
166
|
}
|
|
151
167
|
|
|
152
168
|
// Handle the connection - wait for initial data to determine if it's TLS
|
|
153
|
-
this.handleInitialData(
|
|
169
|
+
this.handleInitialData(wrappedSocket, record);
|
|
154
170
|
}
|
|
155
171
|
|
|
156
172
|
/**
|
|
157
173
|
* Handle initial data from a connection to determine routing
|
|
158
174
|
*/
|
|
159
|
-
private handleInitialData(socket: plugins.net.Socket, record: IConnectionRecord): void {
|
|
175
|
+
private handleInitialData(socket: plugins.net.Socket | WrappedSocket, record: IConnectionRecord): void {
|
|
160
176
|
const connectionId = record.id;
|
|
161
177
|
const localPort = record.localPort;
|
|
162
178
|
let initialDataReceived = false;
|
|
163
179
|
|
|
164
180
|
// Check if any routes on this port require TLS handling
|
|
165
|
-
const allRoutes = this.routeManager.
|
|
181
|
+
const allRoutes = this.routeManager.getRoutes();
|
|
166
182
|
const needsTlsHandling = allRoutes.some(route => {
|
|
167
183
|
// Check if route matches this port
|
|
168
184
|
const matchesPort = this.routeManager.getRoutesForPort(localPort).includes(route);
|
|
@@ -176,9 +192,11 @@ export class RouteConnectionHandler {
|
|
|
176
192
|
|
|
177
193
|
// If no routes require TLS handling and it's not port 443, route immediately
|
|
178
194
|
if (!needsTlsHandling && localPort !== 443) {
|
|
195
|
+
// Extract underlying socket for socket-utils functions
|
|
196
|
+
const underlyingSocket = getUnderlyingSocket(socket);
|
|
179
197
|
// Set up proper socket handlers for immediate routing
|
|
180
198
|
setupSocketHandlers(
|
|
181
|
-
|
|
199
|
+
underlyingSocket,
|
|
182
200
|
(reason) => {
|
|
183
201
|
// Only cleanup if connection hasn't been fully established
|
|
184
202
|
// Check if outgoing connection exists and is connected
|
|
@@ -370,7 +388,7 @@ export class RouteConnectionHandler {
|
|
|
370
388
|
* Route the connection based on match criteria
|
|
371
389
|
*/
|
|
372
390
|
private routeConnection(
|
|
373
|
-
socket: plugins.net.Socket,
|
|
391
|
+
socket: plugins.net.Socket | WrappedSocket,
|
|
374
392
|
record: IConnectionRecord,
|
|
375
393
|
serverName: string,
|
|
376
394
|
initialChunk?: Buffer
|
|
@@ -385,15 +403,21 @@ export class RouteConnectionHandler {
|
|
|
385
403
|
// For HTTP proxy ports without TLS, skip domain check since domain info comes from HTTP headers
|
|
386
404
|
const skipDomainCheck = isHttpProxyPort && !record.isTLS;
|
|
387
405
|
|
|
388
|
-
//
|
|
389
|
-
const
|
|
406
|
+
// Create route context for matching
|
|
407
|
+
const routeContext: IRouteContext = {
|
|
390
408
|
port: localPort,
|
|
391
|
-
domain: serverName,
|
|
409
|
+
domain: skipDomainCheck ? undefined : serverName, // Skip domain if HTTP proxy without TLS
|
|
392
410
|
clientIp: remoteIP,
|
|
411
|
+
serverIp: socket.localAddress || '',
|
|
393
412
|
path: undefined, // We don't have path info at this point
|
|
413
|
+
isTls: record.isTLS,
|
|
394
414
|
tlsVersion: undefined, // We don't extract TLS version yet
|
|
395
|
-
|
|
396
|
-
|
|
415
|
+
timestamp: Date.now(),
|
|
416
|
+
connectionId: record.id
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// Find matching route
|
|
420
|
+
const routeMatch = this.routeManager.findMatchingRoute(routeContext);
|
|
397
421
|
|
|
398
422
|
if (!routeMatch) {
|
|
399
423
|
logger.log('warn', `No route found for ${serverName || 'connection'} on port ${localPort} (connection: ${connectionId})`, {
|
|
@@ -552,7 +576,7 @@ export class RouteConnectionHandler {
|
|
|
552
576
|
* Handle a forward action for a route
|
|
553
577
|
*/
|
|
554
578
|
private handleForwardAction(
|
|
555
|
-
socket: plugins.net.Socket,
|
|
579
|
+
socket: plugins.net.Socket | WrappedSocket,
|
|
556
580
|
record: IConnectionRecord,
|
|
557
581
|
route: IRouteConfig,
|
|
558
582
|
initialChunk?: Buffer
|
|
@@ -869,7 +893,7 @@ export class RouteConnectionHandler {
|
|
|
869
893
|
* Handle a socket-handler action for a route
|
|
870
894
|
*/
|
|
871
895
|
private async handleSocketHandlerAction(
|
|
872
|
-
socket: plugins.net.Socket,
|
|
896
|
+
socket: plugins.net.Socket | WrappedSocket,
|
|
873
897
|
record: IConnectionRecord,
|
|
874
898
|
route: IRouteConfig,
|
|
875
899
|
initialChunk?: Buffer
|
|
@@ -933,8 +957,9 @@ export class RouteConnectionHandler {
|
|
|
933
957
|
});
|
|
934
958
|
|
|
935
959
|
try {
|
|
936
|
-
// Call the handler with socket
|
|
937
|
-
const
|
|
960
|
+
// Call the handler with the appropriate socket (extract underlying if needed)
|
|
961
|
+
const handlerSocket = getUnderlyingSocket(socket);
|
|
962
|
+
const result = route.action.socketHandler(handlerSocket, routeContext);
|
|
938
963
|
|
|
939
964
|
// Handle async handlers properly
|
|
940
965
|
if (result instanceof Promise) {
|
|
@@ -988,7 +1013,7 @@ export class RouteConnectionHandler {
|
|
|
988
1013
|
* Sets up a direct connection to the target
|
|
989
1014
|
*/
|
|
990
1015
|
private setupDirectConnection(
|
|
991
|
-
socket: plugins.net.Socket,
|
|
1016
|
+
socket: plugins.net.Socket | WrappedSocket,
|
|
992
1017
|
record: IConnectionRecord,
|
|
993
1018
|
serverName?: string,
|
|
994
1019
|
initialChunk?: Buffer,
|
|
@@ -1138,7 +1163,10 @@ export class RouteConnectionHandler {
|
|
|
1138
1163
|
}
|
|
1139
1164
|
|
|
1140
1165
|
// Use centralized bidirectional forwarding setup
|
|
1141
|
-
|
|
1166
|
+
// Extract underlying sockets for socket-utils functions
|
|
1167
|
+
const incomingSocket = getUnderlyingSocket(socket);
|
|
1168
|
+
|
|
1169
|
+
setupBidirectionalForwarding(incomingSocket, targetSocket, {
|
|
1142
1170
|
onClientData: (chunk) => {
|
|
1143
1171
|
record.bytesReceived += chunk.length;
|
|
1144
1172
|
this.timeoutManager.updateActivity(record);
|
|
@@ -8,7 +8,7 @@ import { TlsManager } from './tls-manager.js';
|
|
|
8
8
|
import { HttpProxyBridge } from './http-proxy-bridge.js';
|
|
9
9
|
import { TimeoutManager } from './timeout-manager.js';
|
|
10
10
|
import { PortManager } from './port-manager.js';
|
|
11
|
-
import { RouteManager } from '
|
|
11
|
+
import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
|
|
12
12
|
import { RouteConnectionHandler } from './route-connection-handler.js';
|
|
13
13
|
import { NFTablesManager } from './nftables-manager.js';
|
|
14
14
|
|
|
@@ -162,8 +162,20 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
162
162
|
this.timeoutManager
|
|
163
163
|
);
|
|
164
164
|
|
|
165
|
-
// Create the route manager
|
|
166
|
-
|
|
165
|
+
// Create the route manager with SharedRouteManager API
|
|
166
|
+
// Create a logger adapter to match ILogger interface
|
|
167
|
+
const loggerAdapter = {
|
|
168
|
+
debug: (message: string, data?: any) => logger.log('debug', message, data),
|
|
169
|
+
info: (message: string, data?: any) => logger.log('info', message, data),
|
|
170
|
+
warn: (message: string, data?: any) => logger.log('warn', message, data),
|
|
171
|
+
error: (message: string, data?: any) => logger.log('error', message, data)
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
this.routeManager = new RouteManager({
|
|
175
|
+
logger: loggerAdapter,
|
|
176
|
+
enableDetailedLogging: this.settings.enableDetailedLogging,
|
|
177
|
+
routes: this.settings.routes
|
|
178
|
+
});
|
|
167
179
|
|
|
168
180
|
|
|
169
181
|
// Create other required components
|
|
@@ -92,6 +92,8 @@ export function mergeRouteConfigs(
|
|
|
92
92
|
return mergedRoute;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
import { DomainMatcher, PathMatcher, HeaderMatcher } from '../../../core/routing/matchers/index.js';
|
|
96
|
+
|
|
95
97
|
/**
|
|
96
98
|
* Check if a route matches a domain
|
|
97
99
|
* @param route The route to check
|
|
@@ -107,14 +109,7 @@ export function routeMatchesDomain(route: IRouteConfig, domain: string): boolean
|
|
|
107
109
|
? route.match.domains
|
|
108
110
|
: [route.match.domains];
|
|
109
111
|
|
|
110
|
-
return domains.some(d =>
|
|
111
|
-
// Handle wildcard domains
|
|
112
|
-
if (d.startsWith('*.')) {
|
|
113
|
-
const suffix = d.substring(2);
|
|
114
|
-
return domain.endsWith(suffix) && domain.split('.').length > suffix.split('.').length;
|
|
115
|
-
}
|
|
116
|
-
return d.toLowerCase() === domain.toLowerCase();
|
|
117
|
-
});
|
|
112
|
+
return domains.some(d => DomainMatcher.match(d, domain));
|
|
118
113
|
}
|
|
119
114
|
|
|
120
115
|
/**
|
|
@@ -160,28 +155,7 @@ export function routeMatchesPath(route: IRouteConfig, path: string): boolean {
|
|
|
160
155
|
return true; // No path specified means it matches any path
|
|
161
156
|
}
|
|
162
157
|
|
|
163
|
-
|
|
164
|
-
if (route.match.path === path) {
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Handle path prefix with trailing slash (e.g., /api/)
|
|
169
|
-
if (route.match.path.endsWith('/') && path.startsWith(route.match.path)) {
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Handle exact path match without trailing slash
|
|
174
|
-
if (!route.match.path.endsWith('/') && path === route.match.path) {
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Handle wildcard paths (e.g., /api/*)
|
|
179
|
-
if (route.match.path.endsWith('*')) {
|
|
180
|
-
const prefix = route.match.path.slice(0, -1);
|
|
181
|
-
return path.startsWith(prefix);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return false;
|
|
158
|
+
return PathMatcher.match(route.match.path, path).matches;
|
|
185
159
|
}
|
|
186
160
|
|
|
187
161
|
/**
|
|
@@ -198,25 +172,13 @@ export function routeMatchesHeaders(
|
|
|
198
172
|
return true; // No headers specified means it matches any headers
|
|
199
173
|
}
|
|
200
174
|
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// Handle exact match
|
|
209
|
-
if (typeof value === 'string') {
|
|
210
|
-
return headers[key] === value;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Handle regex match
|
|
214
|
-
if (value instanceof RegExp) {
|
|
215
|
-
return value.test(headers[key]);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return false;
|
|
219
|
-
});
|
|
175
|
+
// Convert RegExp patterns to strings for HeaderMatcher
|
|
176
|
+
const stringHeaders: Record<string, string> = {};
|
|
177
|
+
for (const [key, value] of Object.entries(route.match.headers)) {
|
|
178
|
+
stringHeaders[key] = value instanceof RegExp ? value.source : value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return HeaderMatcher.matchAll(stringHeaders, headers);
|
|
220
182
|
}
|
|
221
183
|
|
|
222
184
|
/**
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
|
|
3
|
+
import { DomainMatcher, PathMatcher } from '../../core/routing/matchers/index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interface for router result with additional metadata
|
|
7
|
+
*/
|
|
8
|
+
export interface RouterResult {
|
|
9
|
+
route: IRouteConfig;
|
|
10
|
+
pathMatch?: string;
|
|
11
|
+
pathParams?: Record<string, string>;
|
|
12
|
+
pathRemainder?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Logger interface for HttpRouter
|
|
18
|
+
*/
|
|
19
|
+
export interface ILogger {
|
|
20
|
+
debug?: (message: string, data?: any) => void;
|
|
21
|
+
info: (message: string, data?: any) => void;
|
|
22
|
+
warn: (message: string, data?: any) => void;
|
|
23
|
+
error: (message: string, data?: any) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Unified HTTP Router for reverse proxy requests
|
|
28
|
+
*
|
|
29
|
+
* Domain matching patterns:
|
|
30
|
+
* - Exact matches: "example.com"
|
|
31
|
+
* - Wildcard subdomains: "*.example.com" (matches any subdomain of example.com)
|
|
32
|
+
* - TLD wildcards: "example.*" (matches example.com, example.org, etc.)
|
|
33
|
+
* - Complex wildcards: "*.lossless*" (matches any subdomain of any lossless domain)
|
|
34
|
+
* - Default fallback: "*" (matches any unmatched domain)
|
|
35
|
+
*
|
|
36
|
+
* Path pattern matching:
|
|
37
|
+
* - Exact path: "/api/users"
|
|
38
|
+
* - Wildcard paths: "/api/*"
|
|
39
|
+
* - Path parameters: "/users/:id/profile"
|
|
40
|
+
*/
|
|
41
|
+
export class HttpRouter {
|
|
42
|
+
// Store routes sorted by priority
|
|
43
|
+
private routes: IRouteConfig[] = [];
|
|
44
|
+
// Default route to use when no match is found (optional)
|
|
45
|
+
private defaultRoute?: IRouteConfig;
|
|
46
|
+
// Logger interface
|
|
47
|
+
private logger: ILogger;
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
routes?: IRouteConfig[],
|
|
51
|
+
logger?: ILogger
|
|
52
|
+
) {
|
|
53
|
+
this.logger = logger || {
|
|
54
|
+
error: console.error.bind(console),
|
|
55
|
+
warn: console.warn.bind(console),
|
|
56
|
+
info: console.info.bind(console),
|
|
57
|
+
debug: console.debug?.bind(console)
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (routes) {
|
|
61
|
+
this.setRoutes(routes);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sets a new set of routes
|
|
67
|
+
* @param routes Array of route configurations
|
|
68
|
+
*/
|
|
69
|
+
public setRoutes(routes: IRouteConfig[]): void {
|
|
70
|
+
this.routes = [...routes];
|
|
71
|
+
|
|
72
|
+
// Sort routes by priority (higher priority first)
|
|
73
|
+
this.routes.sort((a, b) => {
|
|
74
|
+
const priorityA = a.priority ?? 0;
|
|
75
|
+
const priorityB = b.priority ?? 0;
|
|
76
|
+
return priorityB - priorityA;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Find default route if any (route with "*" as domain)
|
|
80
|
+
this.defaultRoute = this.routes.find(route => {
|
|
81
|
+
const domains = Array.isArray(route.match.domains)
|
|
82
|
+
? route.match.domains
|
|
83
|
+
: route.match.domains ? [route.match.domains] : [];
|
|
84
|
+
return domains.includes('*');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const uniqueDomains = this.getHostnames();
|
|
88
|
+
this.logger.info(`HttpRouter initialized with ${this.routes.length} routes (${uniqueDomains.length} unique hosts)`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Routes a request based on hostname and path
|
|
93
|
+
* @param req The incoming HTTP request
|
|
94
|
+
* @returns The matching route or undefined if no match found
|
|
95
|
+
*/
|
|
96
|
+
public routeReq(req: plugins.http.IncomingMessage): IRouteConfig | undefined {
|
|
97
|
+
const result = this.routeReqWithDetails(req);
|
|
98
|
+
return result ? result.route : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Routes a request with detailed matching information
|
|
103
|
+
* @param req The incoming HTTP request
|
|
104
|
+
* @returns Detailed routing result including matched route and path information
|
|
105
|
+
*/
|
|
106
|
+
public routeReqWithDetails(req: plugins.http.IncomingMessage): RouterResult | undefined {
|
|
107
|
+
// Extract and validate host header
|
|
108
|
+
const originalHost = req.headers.host;
|
|
109
|
+
if (!originalHost) {
|
|
110
|
+
this.logger.error('No host header found in request');
|
|
111
|
+
return this.defaultRoute ? { route: this.defaultRoute } : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Parse URL for path matching
|
|
115
|
+
const parsedUrl = plugins.url.parse(req.url || '/');
|
|
116
|
+
const urlPath = parsedUrl.pathname || '/';
|
|
117
|
+
|
|
118
|
+
// Extract hostname without port
|
|
119
|
+
const hostWithoutPort = originalHost.split(':')[0].toLowerCase();
|
|
120
|
+
|
|
121
|
+
// Find matching route
|
|
122
|
+
const matchingRoute = this.findMatchingRoute(hostWithoutPort, urlPath);
|
|
123
|
+
|
|
124
|
+
if (matchingRoute) {
|
|
125
|
+
return matchingRoute;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Fall back to default route if available
|
|
129
|
+
if (this.defaultRoute) {
|
|
130
|
+
this.logger.warn(`No specific route found for host: ${hostWithoutPort}, using default`);
|
|
131
|
+
return { route: this.defaultRoute };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.logger.error(`No route found for host: ${hostWithoutPort}`);
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Find the best matching route for a given hostname and path
|
|
140
|
+
*/
|
|
141
|
+
private findMatchingRoute(hostname: string, path: string): RouterResult | undefined {
|
|
142
|
+
// Try each route in priority order
|
|
143
|
+
for (const route of this.routes) {
|
|
144
|
+
// Skip disabled routes
|
|
145
|
+
if (route.enabled === false) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check domain match
|
|
150
|
+
if (route.match.domains) {
|
|
151
|
+
const domains = Array.isArray(route.match.domains)
|
|
152
|
+
? route.match.domains
|
|
153
|
+
: [route.match.domains];
|
|
154
|
+
|
|
155
|
+
// Check if any domain pattern matches
|
|
156
|
+
const domainMatches = domains.some(domain =>
|
|
157
|
+
DomainMatcher.match(domain, hostname)
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (!domainMatches) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check path match if specified
|
|
166
|
+
if (route.match.path) {
|
|
167
|
+
const pathResult = PathMatcher.match(route.match.path, path);
|
|
168
|
+
if (pathResult.matches) {
|
|
169
|
+
return {
|
|
170
|
+
route,
|
|
171
|
+
pathMatch: path,
|
|
172
|
+
pathParams: pathResult.params,
|
|
173
|
+
pathRemainder: pathResult.pathRemainder
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// No path specified, so domain match is sufficient
|
|
178
|
+
return { route };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Gets all currently active route configurations
|
|
187
|
+
* @returns Array of all active routes
|
|
188
|
+
*/
|
|
189
|
+
public getRoutes(): IRouteConfig[] {
|
|
190
|
+
return [...this.routes];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Gets all hostnames that this router is configured to handle
|
|
195
|
+
* @returns Array of unique hostnames
|
|
196
|
+
*/
|
|
197
|
+
public getHostnames(): string[] {
|
|
198
|
+
const hostnames = new Set<string>();
|
|
199
|
+
for (const route of this.routes) {
|
|
200
|
+
if (!route.match.domains) continue;
|
|
201
|
+
|
|
202
|
+
const domains = Array.isArray(route.match.domains)
|
|
203
|
+
? route.match.domains
|
|
204
|
+
: [route.match.domains];
|
|
205
|
+
|
|
206
|
+
for (const domain of domains) {
|
|
207
|
+
if (domain !== '*') {
|
|
208
|
+
hostnames.add(domain.toLowerCase());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return Array.from(hostnames);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Adds a single new route configuration
|
|
217
|
+
* @param route The route configuration to add
|
|
218
|
+
*/
|
|
219
|
+
public addRoute(route: IRouteConfig): void {
|
|
220
|
+
this.routes.push(route);
|
|
221
|
+
|
|
222
|
+
// Re-sort routes by priority
|
|
223
|
+
this.routes.sort((a, b) => {
|
|
224
|
+
const priorityA = a.priority ?? 0;
|
|
225
|
+
const priorityB = b.priority ?? 0;
|
|
226
|
+
return priorityB - priorityA;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Removes routes by domain pattern
|
|
232
|
+
* @param domain The domain pattern to remove routes for
|
|
233
|
+
* @returns Boolean indicating whether any routes were removed
|
|
234
|
+
*/
|
|
235
|
+
public removeRoutesByDomain(domain: string): boolean {
|
|
236
|
+
const initialCount = this.routes.length;
|
|
237
|
+
|
|
238
|
+
// Filter out routes that match the domain
|
|
239
|
+
this.routes = this.routes.filter(route => {
|
|
240
|
+
if (!route.match.domains) return true;
|
|
241
|
+
|
|
242
|
+
const domains = Array.isArray(route.match.domains)
|
|
243
|
+
? route.match.domains
|
|
244
|
+
: [route.match.domains];
|
|
245
|
+
|
|
246
|
+
return !domains.includes(domain);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return this.routes.length !== initialCount;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Remove a specific route by reference
|
|
254
|
+
* @param route The route to remove
|
|
255
|
+
* @returns Boolean indicating if the route was found and removed
|
|
256
|
+
*/
|
|
257
|
+
public removeRoute(route: IRouteConfig): boolean {
|
|
258
|
+
const index = this.routes.indexOf(route);
|
|
259
|
+
if (index !== -1) {
|
|
260
|
+
this.routes.splice(index, 1);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
}
|
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
* HTTP routing
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
// Export
|
|
6
|
-
export {
|
|
7
|
-
export type {
|
|
8
|
-
// Re-export the RouterResult and PathPatternConfig from proxy-router.js (legacy names maintained for compatibility)
|
|
9
|
-
export type { PathPatternConfig as ProxyPathPatternConfig, RouterResult as ProxyRouterResult } from './proxy-router.js';
|
|
10
|
-
|
|
11
|
-
export { RouteRouter } from './route-router.js';
|
|
12
|
-
export type { PathPatternConfig as RoutePathPatternConfig, RouterResult as RouteRouterResult } from './route-router.js';
|
|
5
|
+
// Export the unified HttpRouter
|
|
6
|
+
export { HttpRouter } from './http-router.js';
|
|
7
|
+
export type { RouterResult, ILogger } from './http-router.js';
|