@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.
Files changed (110) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/core/models/index.d.ts +2 -0
  3. package/dist_ts/core/models/index.js +3 -1
  4. package/dist_ts/core/models/socket-types.d.ts +14 -0
  5. package/dist_ts/core/models/socket-types.js +15 -0
  6. package/dist_ts/core/models/wrapped-socket.d.ts +34 -0
  7. package/dist_ts/core/models/wrapped-socket.js +82 -0
  8. package/dist_ts/core/routing/index.d.ts +11 -0
  9. package/dist_ts/core/routing/index.js +17 -0
  10. package/dist_ts/core/routing/matchers/domain.d.ts +34 -0
  11. package/dist_ts/core/routing/matchers/domain.js +91 -0
  12. package/dist_ts/core/routing/matchers/header.d.ts +32 -0
  13. package/dist_ts/core/routing/matchers/header.js +94 -0
  14. package/dist_ts/core/routing/matchers/index.d.ts +18 -0
  15. package/dist_ts/core/routing/matchers/index.js +20 -0
  16. package/dist_ts/core/routing/matchers/ip.d.ts +53 -0
  17. package/dist_ts/core/routing/matchers/ip.js +169 -0
  18. package/dist_ts/core/routing/matchers/path.d.ts +44 -0
  19. package/dist_ts/core/routing/matchers/path.js +148 -0
  20. package/dist_ts/core/routing/route-manager.d.ts +88 -0
  21. package/dist_ts/core/routing/route-manager.js +342 -0
  22. package/dist_ts/core/routing/route-utils.d.ts +28 -0
  23. package/dist_ts/core/routing/route-utils.js +67 -0
  24. package/dist_ts/core/routing/specificity.d.ts +30 -0
  25. package/dist_ts/core/routing/specificity.js +115 -0
  26. package/dist_ts/core/routing/types.d.ts +41 -0
  27. package/dist_ts/core/routing/types.js +5 -0
  28. package/dist_ts/core/utils/index.d.ts +0 -2
  29. package/dist_ts/core/utils/index.js +1 -3
  30. package/dist_ts/core/utils/route-manager.d.ts +0 -30
  31. package/dist_ts/core/utils/route-manager.js +6 -47
  32. package/dist_ts/core/utils/route-utils.d.ts +2 -68
  33. package/dist_ts/core/utils/route-utils.js +21 -218
  34. package/dist_ts/core/utils/security-utils.js +4 -4
  35. package/dist_ts/core/utils/socket-utils.d.ts +0 -15
  36. package/dist_ts/core/utils/socket-utils.js +1 -35
  37. package/dist_ts/forwarding/handlers/https-terminate-to-http-handler.js +47 -32
  38. package/dist_ts/forwarding/handlers/https-terminate-to-https-handler.js +51 -32
  39. package/dist_ts/index.d.ts +2 -5
  40. package/dist_ts/index.js +5 -11
  41. package/dist_ts/proxies/http-proxy/http-proxy.d.ts +0 -1
  42. package/dist_ts/proxies/http-proxy/http-proxy.js +15 -60
  43. package/dist_ts/proxies/http-proxy/models/types.d.ts +0 -90
  44. package/dist_ts/proxies/http-proxy/models/types.js +1 -242
  45. package/dist_ts/proxies/http-proxy/request-handler.d.ts +3 -5
  46. package/dist_ts/proxies/http-proxy/request-handler.js +20 -171
  47. package/dist_ts/proxies/http-proxy/websocket-handler.d.ts +2 -5
  48. package/dist_ts/proxies/http-proxy/websocket-handler.js +15 -23
  49. package/dist_ts/proxies/index.d.ts +2 -2
  50. package/dist_ts/proxies/index.js +4 -3
  51. package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +3 -1
  52. package/dist_ts/proxies/smart-proxy/connection-manager.js +15 -7
  53. package/dist_ts/proxies/smart-proxy/http-proxy-bridge.d.ts +2 -1
  54. package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +5 -2
  55. package/dist_ts/proxies/smart-proxy/index.d.ts +1 -1
  56. package/dist_ts/proxies/smart-proxy/index.js +2 -2
  57. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +6 -2
  58. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +1 -1
  59. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +48 -25
  60. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +1 -1
  61. package/dist_ts/proxies/smart-proxy/smart-proxy.js +15 -4
  62. package/dist_ts/proxies/smart-proxy/utils/route-utils.js +10 -43
  63. package/dist_ts/routing/router/http-router.d.ts +89 -0
  64. package/dist_ts/routing/router/http-router.js +205 -0
  65. package/dist_ts/routing/router/index.d.ts +2 -5
  66. package/dist_ts/routing/router/index.js +3 -4
  67. package/package.json +1 -1
  68. package/readme.delete.md +187 -0
  69. package/readme.hints.md +210 -1
  70. package/readme.plan.md +621 -0
  71. package/readme.routing.md +341 -0
  72. package/ts/00_commitinfo_data.ts +1 -1
  73. package/ts/core/models/index.ts +2 -0
  74. package/ts/core/models/socket-types.ts +21 -0
  75. package/ts/core/models/wrapped-socket.ts +99 -0
  76. package/ts/core/routing/index.ts +21 -0
  77. package/ts/core/routing/matchers/domain.ts +119 -0
  78. package/ts/core/routing/matchers/header.ts +120 -0
  79. package/ts/core/routing/matchers/index.ts +22 -0
  80. package/ts/core/routing/matchers/ip.ts +207 -0
  81. package/ts/core/routing/matchers/path.ts +184 -0
  82. package/ts/core/{utils → routing}/route-manager.ts +7 -57
  83. package/ts/core/routing/route-utils.ts +88 -0
  84. package/ts/core/routing/specificity.ts +141 -0
  85. package/ts/core/routing/types.ts +49 -0
  86. package/ts/core/utils/index.ts +0 -2
  87. package/ts/core/utils/security-utils.ts +3 -7
  88. package/ts/core/utils/socket-utils.ts +0 -44
  89. package/ts/forwarding/handlers/https-terminate-to-http-handler.ts +47 -33
  90. package/ts/forwarding/handlers/https-terminate-to-https-handler.ts +55 -35
  91. package/ts/index.ts +4 -14
  92. package/ts/proxies/http-proxy/http-proxy.ts +13 -68
  93. package/ts/proxies/http-proxy/models/types.ts +0 -324
  94. package/ts/proxies/http-proxy/request-handler.ts +15 -186
  95. package/ts/proxies/http-proxy/websocket-handler.ts +15 -26
  96. package/ts/proxies/index.ts +3 -2
  97. package/ts/proxies/smart-proxy/connection-manager.ts +15 -7
  98. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +6 -2
  99. package/ts/proxies/smart-proxy/index.ts +1 -1
  100. package/ts/proxies/smart-proxy/models/interfaces.ts +8 -2
  101. package/ts/proxies/smart-proxy/route-connection-handler.ts +58 -30
  102. package/ts/proxies/smart-proxy/smart-proxy.ts +15 -3
  103. package/ts/proxies/smart-proxy/utils/route-utils.ts +11 -49
  104. package/ts/routing/router/http-router.ts +266 -0
  105. package/ts/routing/router/index.ts +3 -8
  106. package/readme.problems.md +0 -170
  107. package/ts/core/utils/route-utils.ts +0 -312
  108. package/ts/proxies/smart-proxy/route-manager.ts +0 -554
  109. package/ts/routing/router/proxy-router.ts +0 -437
  110. 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, IRouteContext } from './models/route-types.js';
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 './route-manager.js';
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
- const ipValidation = this.securityManager.validateIP(remoteIP);
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(socket);
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.setNoDelay(this.settings.noDelay);
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
- socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
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 socket) {
112
- (socket as any).setKeepAliveProbes(10);
127
+ if ('setKeepAliveProbes' in underlyingSocket) {
128
+ (underlyingSocket as any).setKeepAliveProbes(10);
113
129
  }
114
- if ('setKeepAliveInterval' in socket) {
115
- (socket as any).setKeepAliveInterval(1000);
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(socket, record);
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.getAllRoutes();
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
- socket,
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
- // Find matching route
389
- const routeMatch = this.routeManager.findMatchingRoute({
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
- skipDomainCheck: skipDomainCheck,
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 AND context
937
- const result = route.action.socketHandler(socket, routeContext);
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
- setupBidirectionalForwarding(socket, targetSocket, {
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 './route-manager.js';
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
- this.routeManager = new RouteManager(this.settings);
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
- // Handle exact path
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
- // Check each header in the route's match criteria
202
- return Object.entries(route.match.headers).every(([key, value]) => {
203
- // If the header isn't present in the request, it doesn't match
204
- if (!headers[key]) {
205
- return false;
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 selectively to avoid ambiguity between duplicate type names
6
- export { ProxyRouter } from './proxy-router.js';
7
- export type { IPathPatternConfig } from './proxy-router.js';
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';