@push.rocks/smartproxy 3.26.0 → 3.28.1

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.
@@ -1,33 +1,351 @@
1
- import * as plugins from './plugins.js';
1
+ import * as http from 'http';
2
+ import * as url from 'url';
3
+ import * as tsclass from '@tsclass/tsclass';
4
+
5
+ /**
6
+ * Optional path pattern configuration that can be added to proxy configs
7
+ */
8
+ export interface IPathPatternConfig {
9
+ pathPattern?: string;
10
+ }
11
+
12
+ /**
13
+ * Interface for router result with additional metadata
14
+ */
15
+ export interface IRouterResult {
16
+ config: tsclass.network.IReverseProxyConfig;
17
+ pathMatch?: string;
18
+ pathParams?: Record<string, string>;
19
+ pathRemainder?: string;
20
+ }
2
21
 
3
22
  export class ProxyRouter {
4
- public reverseProxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
23
+ // Store original configs for reference
24
+ private reverseProxyConfigs: tsclass.network.IReverseProxyConfig[] = [];
25
+ // Default config to use when no match is found (optional)
26
+ private defaultConfig?: tsclass.network.IReverseProxyConfig;
27
+ // Store path patterns separately since they're not in the original interface
28
+ private pathPatterns: Map<tsclass.network.IReverseProxyConfig, string> = new Map();
29
+ // Logger interface
30
+ private logger: {
31
+ error: (message: string, data?: any) => void;
32
+ warn: (message: string, data?: any) => void;
33
+ info: (message: string, data?: any) => void;
34
+ debug: (message: string, data?: any) => void;
35
+ };
36
+
37
+ constructor(
38
+ configs?: tsclass.network.IReverseProxyConfig[],
39
+ logger?: {
40
+ error: (message: string, data?: any) => void;
41
+ warn: (message: string, data?: any) => void;
42
+ info: (message: string, data?: any) => void;
43
+ debug: (message: string, data?: any) => void;
44
+ }
45
+ ) {
46
+ this.logger = logger || console;
47
+ if (configs) {
48
+ this.setNewProxyConfigs(configs);
49
+ }
50
+ }
5
51
 
6
52
  /**
7
- * sets a new set of reverse configs to be routed to
8
- * @param reverseCandidatesArg
53
+ * Sets a new set of reverse configs to be routed to
54
+ * @param reverseCandidatesArg Array of reverse proxy configurations
9
55
  */
10
- public setNewProxyConfigs(reverseCandidatesArg: plugins.tsclass.network.IReverseProxyConfig[]) {
11
- this.reverseProxyConfigs = reverseCandidatesArg;
56
+ public setNewProxyConfigs(reverseCandidatesArg: tsclass.network.IReverseProxyConfig[]): void {
57
+ this.reverseProxyConfigs = [...reverseCandidatesArg];
58
+
59
+ // Find default config if any (config with "*" as hostname)
60
+ this.defaultConfig = this.reverseProxyConfigs.find(config => config.hostName === '*');
61
+
62
+ this.logger.info(`Router initialized with ${this.reverseProxyConfigs.length} configs (${this.getHostnames().length} unique hosts)`);
12
63
  }
13
64
 
14
65
  /**
15
- * routes a request
66
+ * Routes a request based on hostname and path
67
+ * @param req The incoming HTTP request
68
+ * @returns The matching proxy config or undefined if no match found
69
+ */
70
+ public routeReq(req: http.IncomingMessage): tsclass.network.IReverseProxyConfig {
71
+ const result = this.routeReqWithDetails(req);
72
+ return result ? result.config : undefined;
73
+ }
74
+
75
+ /**
76
+ * Routes a request with detailed matching information
77
+ * @param req The incoming HTTP request
78
+ * @returns Detailed routing result including matched config and path information
16
79
  */
17
- public routeReq(req: plugins.http.IncomingMessage): plugins.tsclass.network.IReverseProxyConfig {
80
+ public routeReqWithDetails(req: http.IncomingMessage): IRouterResult | undefined {
81
+ // Extract and validate host header
18
82
  const originalHost = req.headers.host;
19
83
  if (!originalHost) {
20
- console.error('No host header found in request');
84
+ this.logger.error('No host header found in request');
85
+ return this.defaultConfig ? { config: this.defaultConfig } : undefined;
86
+ }
87
+
88
+ // Parse URL for path matching
89
+ const parsedUrl = url.parse(req.url || '/');
90
+ const urlPath = parsedUrl.pathname || '/';
91
+
92
+ // Extract hostname without port
93
+ const hostWithoutPort = originalHost.split(':')[0].toLowerCase();
94
+
95
+ // First try exact hostname match
96
+ const exactConfig = this.findConfigForHost(hostWithoutPort, urlPath);
97
+ if (exactConfig) {
98
+ return exactConfig;
99
+ }
100
+
101
+ // Try wildcard subdomain
102
+ if (hostWithoutPort.includes('.')) {
103
+ const domainParts = hostWithoutPort.split('.');
104
+ if (domainParts.length > 2) {
105
+ const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
106
+ const wildcardConfig = this.findConfigForHost(wildcardDomain, urlPath);
107
+ if (wildcardConfig) {
108
+ return wildcardConfig;
109
+ }
110
+ }
111
+ }
112
+
113
+ // Fall back to default config if available
114
+ if (this.defaultConfig) {
115
+ this.logger.warn(`No specific config found for host: ${hostWithoutPort}, using default`);
116
+ return { config: this.defaultConfig };
117
+ }
118
+
119
+ this.logger.error(`No config found for host: ${hostWithoutPort}`);
120
+ return undefined;
121
+ }
122
+
123
+ /**
124
+ * Find a config for a specific host and path
125
+ */
126
+ private findConfigForHost(hostname: string, path: string): IRouterResult | undefined {
127
+ // Find all configs for this hostname
128
+ const configs = this.reverseProxyConfigs.filter(
129
+ config => config.hostName.toLowerCase() === hostname.toLowerCase()
130
+ );
131
+
132
+ if (configs.length === 0) {
21
133
  return undefined;
22
134
  }
23
- // Strip port from host if present
24
- const hostWithoutPort = originalHost.split(':')[0];
25
- const correspodingReverseProxyConfig = this.reverseProxyConfigs.find((reverseConfig) => {
26
- return reverseConfig.hostName === hostWithoutPort;
135
+
136
+ // First try configs with path patterns
137
+ const configsWithPaths = configs.filter(config => this.pathPatterns.has(config));
138
+
139
+ // Sort by path pattern specificity - more specific first
140
+ configsWithPaths.sort((a, b) => {
141
+ const aPattern = this.pathPatterns.get(a) || '';
142
+ const bPattern = this.pathPatterns.get(b) || '';
143
+
144
+ // Exact patterns come before wildcard patterns
145
+ const aHasWildcard = aPattern.includes('*');
146
+ const bHasWildcard = bPattern.includes('*');
147
+
148
+ if (aHasWildcard && !bHasWildcard) return 1;
149
+ if (!aHasWildcard && bHasWildcard) return -1;
150
+
151
+ // Longer patterns are considered more specific
152
+ return bPattern.length - aPattern.length;
27
153
  });
28
- if (!correspodingReverseProxyConfig) {
29
- console.error(`No config found for host: ${hostWithoutPort}`);
154
+
155
+ // Check each config with path pattern
156
+ for (const config of configsWithPaths) {
157
+ const pathPattern = this.pathPatterns.get(config);
158
+ if (pathPattern) {
159
+ const pathMatch = this.matchPath(path, pathPattern);
160
+ if (pathMatch) {
161
+ return {
162
+ config,
163
+ pathMatch: pathMatch.matched,
164
+ pathParams: pathMatch.params,
165
+ pathRemainder: pathMatch.remainder
166
+ };
167
+ }
168
+ }
30
169
  }
31
- return correspodingReverseProxyConfig;
170
+
171
+ // If no path pattern matched, use the first config without a path pattern
172
+ const configWithoutPath = configs.find(config => !this.pathPatterns.has(config));
173
+ if (configWithoutPath) {
174
+ return { config: configWithoutPath };
175
+ }
176
+
177
+ return undefined;
32
178
  }
33
- }
179
+
180
+ /**
181
+ * Matches a URL path against a pattern
182
+ * Supports:
183
+ * - Exact matches: /users/profile
184
+ * - Wildcards: /api/* (matches any path starting with /api/)
185
+ * - Path parameters: /users/:id (captures id as a parameter)
186
+ *
187
+ * @param path The URL path to match
188
+ * @param pattern The pattern to match against
189
+ * @returns Match result with params and remainder, or null if no match
190
+ */
191
+ private matchPath(path: string, pattern: string): {
192
+ matched: string;
193
+ params: Record<string, string>;
194
+ remainder: string;
195
+ } | null {
196
+ // Handle exact match
197
+ if (path === pattern) {
198
+ return {
199
+ matched: pattern,
200
+ params: {},
201
+ remainder: ''
202
+ };
203
+ }
204
+
205
+ // Handle wildcard match
206
+ if (pattern.endsWith('/*')) {
207
+ const prefix = pattern.slice(0, -2);
208
+ if (path === prefix || path.startsWith(`${prefix}/`)) {
209
+ return {
210
+ matched: prefix,
211
+ params: {},
212
+ remainder: path.slice(prefix.length)
213
+ };
214
+ }
215
+ return null;
216
+ }
217
+
218
+ // Handle path parameters
219
+ const patternParts = pattern.split('/').filter(p => p);
220
+ const pathParts = path.split('/').filter(p => p);
221
+
222
+ // Too few path parts to match
223
+ if (pathParts.length < patternParts.length) {
224
+ return null;
225
+ }
226
+
227
+ const params: Record<string, string> = {};
228
+
229
+ // Compare each part
230
+ for (let i = 0; i < patternParts.length; i++) {
231
+ const patternPart = patternParts[i];
232
+ const pathPart = pathParts[i];
233
+
234
+ // Handle parameter
235
+ if (patternPart.startsWith(':')) {
236
+ const paramName = patternPart.slice(1);
237
+ params[paramName] = pathPart;
238
+ continue;
239
+ }
240
+
241
+ // Handle wildcard at the end
242
+ if (patternPart === '*' && i === patternParts.length - 1) {
243
+ break;
244
+ }
245
+
246
+ // Handle exact match for this part
247
+ if (patternPart !== pathPart) {
248
+ return null;
249
+ }
250
+ }
251
+
252
+ // Calculate the remainder - the unmatched path parts
253
+ const remainderParts = pathParts.slice(patternParts.length);
254
+ const remainder = remainderParts.length ? '/' + remainderParts.join('/') : '';
255
+
256
+ // Calculate the matched path
257
+ const matchedParts = patternParts.map((part, i) => {
258
+ return part.startsWith(':') ? pathParts[i] : part;
259
+ });
260
+ const matched = '/' + matchedParts.join('/');
261
+
262
+ return {
263
+ matched,
264
+ params,
265
+ remainder
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Gets all currently active proxy configurations
271
+ * @returns Array of all active configurations
272
+ */
273
+ public getProxyConfigs(): tsclass.network.IReverseProxyConfig[] {
274
+ return [...this.reverseProxyConfigs];
275
+ }
276
+
277
+ /**
278
+ * Gets all hostnames that this router is configured to handle
279
+ * @returns Array of hostnames
280
+ */
281
+ public getHostnames(): string[] {
282
+ const hostnames = new Set<string>();
283
+ for (const config of this.reverseProxyConfigs) {
284
+ if (config.hostName !== '*') {
285
+ hostnames.add(config.hostName.toLowerCase());
286
+ }
287
+ }
288
+ return Array.from(hostnames);
289
+ }
290
+
291
+ /**
292
+ * Adds a single new proxy configuration
293
+ * @param config The configuration to add
294
+ * @param pathPattern Optional path pattern for route matching
295
+ */
296
+ public addProxyConfig(
297
+ config: tsclass.network.IReverseProxyConfig,
298
+ pathPattern?: string
299
+ ): void {
300
+ this.reverseProxyConfigs.push(config);
301
+
302
+ // Store path pattern if provided
303
+ if (pathPattern) {
304
+ this.pathPatterns.set(config, pathPattern);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Sets a path pattern for an existing config
310
+ * @param config The existing configuration
311
+ * @param pathPattern The path pattern to set
312
+ * @returns Boolean indicating if the config was found and updated
313
+ */
314
+ public setPathPattern(
315
+ config: tsclass.network.IReverseProxyConfig,
316
+ pathPattern: string
317
+ ): boolean {
318
+ const exists = this.reverseProxyConfigs.includes(config);
319
+ if (exists) {
320
+ this.pathPatterns.set(config, pathPattern);
321
+ return true;
322
+ }
323
+ return false;
324
+ }
325
+
326
+ /**
327
+ * Removes a proxy configuration by hostname
328
+ * @param hostname The hostname to remove
329
+ * @returns Boolean indicating whether any configs were removed
330
+ */
331
+ public removeProxyConfig(hostname: string): boolean {
332
+ const initialCount = this.reverseProxyConfigs.length;
333
+
334
+ // Find configs to remove
335
+ const configsToRemove = this.reverseProxyConfigs.filter(
336
+ config => config.hostName === hostname
337
+ );
338
+
339
+ // Remove them from the patterns map
340
+ for (const config of configsToRemove) {
341
+ this.pathPatterns.delete(config);
342
+ }
343
+
344
+ // Filter them out of the configs array
345
+ this.reverseProxyConfigs = this.reverseProxyConfigs.filter(
346
+ config => config.hostName !== hostname
347
+ );
348
+
349
+ return this.reverseProxyConfigs.length !== initialCount;
350
+ }
351
+ }
package/ts/plugins.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  // node native scope
2
+ import { EventEmitter } from 'events';
2
3
  import * as http from 'http';
3
4
  import * as https from 'https';
4
5
  import * as net from 'net';
5
6
  import * as tls from 'tls';
6
7
  import * as url from 'url';
7
8
 
8
- export { http, https, net, tls, url };
9
+
10
+ export { EventEmitter, http, https, net, tls, url };
9
11
 
10
12
  // tsclass scope
11
13
  import * as tsclass from '@tsclass/tsclass';
@@ -22,9 +24,10 @@ import * as smartstring from '@push.rocks/smartstring';
22
24
  export { lik, smartdelay, smartrequest, smartpromise, smartstring };
23
25
 
24
26
  // third party scope
27
+ import * as acme from 'acme-client';
25
28
  import prettyMs from 'pretty-ms';
26
29
  import * as ws from 'ws';
27
30
  import wsDefault from 'ws';
28
31
  import { minimatch } from 'minimatch';
29
32
 
30
- export { prettyMs, ws, wsDefault, minimatch };
33
+ export { acme, prettyMs, ws, wsDefault, minimatch };