@push.rocks/smartproxy 22.4.2 → 22.6.0
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/changelog.md +28 -0
- package/dist_rust/rustproxy +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -5
- package/dist_ts/index.js +3 -9
- package/dist_ts/protocols/common/fragment-handler.js +5 -1
- package/dist_ts/proxies/index.d.ts +1 -5
- package/dist_ts/proxies/index.js +2 -6
- package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
- package/dist_ts/proxies/smart-proxy/index.js +7 -13
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -2
- package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
- package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -621
- package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
- package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
- package/dist_ts/routing/index.d.ts +1 -1
- package/dist_ts/routing/index.js +3 -3
- package/dist_ts/routing/models/http-types.d.ts +119 -4
- package/dist_ts/routing/models/http-types.js +93 -5
- package/package.json +1 -1
- package/readme.md +470 -219
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +4 -12
- package/ts/protocols/common/fragment-handler.ts +4 -0
- package/ts/proxies/index.ts +1 -9
- package/ts/proxies/smart-proxy/index.ts +6 -13
- package/ts/proxies/smart-proxy/models/interfaces.ts +6 -4
- package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
- package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
- package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
- package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +282 -798
- package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
- package/ts/routing/index.ts +2 -2
- package/ts/routing/models/http-types.ts +147 -4
- package/ts/proxies/http-proxy/connection-pool.ts +0 -228
- package/ts/proxies/http-proxy/context-creator.ts +0 -145
- package/ts/proxies/http-proxy/default-certificates.ts +0 -150
- package/ts/proxies/http-proxy/function-cache.ts +0 -279
- package/ts/proxies/http-proxy/handlers/index.ts +0 -5
- package/ts/proxies/http-proxy/http-proxy.ts +0 -669
- package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
- package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
- package/ts/proxies/http-proxy/index.ts +0 -18
- package/ts/proxies/http-proxy/models/http-types.ts +0 -148
- package/ts/proxies/http-proxy/models/index.ts +0 -5
- package/ts/proxies/http-proxy/models/types.ts +0 -125
- package/ts/proxies/http-proxy/request-handler.ts +0 -878
- package/ts/proxies/http-proxy/security-manager.ts +0 -413
- package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
- package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
- package/ts/proxies/smart-proxy/cert-store.ts +0 -92
- package/ts/proxies/smart-proxy/certificate-manager.ts +0 -895
- package/ts/proxies/smart-proxy/connection-manager.ts +0 -809
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -213
- package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
- package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
- package/ts/proxies/smart-proxy/port-manager.ts +0 -358
- package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1712
- package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
- package/ts/proxies/smart-proxy/security-manager.ts +0 -269
- package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
- package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
- package/ts/proxies/smart-proxy/tls-manager.ts +0 -171
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { logger } from '../../core/utils/logger.js';
|
|
3
|
+
import type { IRouteConfig, IRouteContext } from './models/route-types.js';
|
|
4
|
+
import type { RoutePreprocessor } from './route-preprocessor.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Unix domain socket server that receives relayed connections from the Rust proxy.
|
|
8
|
+
*
|
|
9
|
+
* When Rust encounters a route of type `socket-handler`, it connects to this
|
|
10
|
+
* Unix socket, sends a JSON metadata line, then proxies the raw TCP bytes.
|
|
11
|
+
* This server reads the metadata, finds the original JS handler, builds an
|
|
12
|
+
* IRouteContext, and hands the socket to the handler.
|
|
13
|
+
*/
|
|
14
|
+
export class SocketHandlerServer {
|
|
15
|
+
private server: plugins.net.Server | null = null;
|
|
16
|
+
private socketPath: string;
|
|
17
|
+
private preprocessor: RoutePreprocessor;
|
|
18
|
+
private activeSockets = new Set<plugins.net.Socket>();
|
|
19
|
+
|
|
20
|
+
constructor(preprocessor: RoutePreprocessor) {
|
|
21
|
+
this.preprocessor = preprocessor;
|
|
22
|
+
this.socketPath = `/tmp/smartproxy-relay-${process.pid}.sock`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The Unix socket path this server listens on.
|
|
27
|
+
*/
|
|
28
|
+
public getSocketPath(): string {
|
|
29
|
+
return this.socketPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start listening for relayed connections from Rust.
|
|
34
|
+
*/
|
|
35
|
+
public async start(): Promise<void> {
|
|
36
|
+
// Clean up stale socket file
|
|
37
|
+
try {
|
|
38
|
+
await plugins.fs.promises.unlink(this.socketPath);
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore if doesn't exist
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new Promise<void>((resolve, reject) => {
|
|
44
|
+
this.server = plugins.net.createServer((socket) => {
|
|
45
|
+
this.activeSockets.add(socket);
|
|
46
|
+
socket.on('close', () => this.activeSockets.delete(socket));
|
|
47
|
+
this.handleConnection(socket);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.server.on('error', (err) => {
|
|
51
|
+
logger.log('error', `SocketHandlerServer error: ${err.message}`, { component: 'socket-handler-server' });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.server.listen(this.socketPath, () => {
|
|
55
|
+
logger.log('info', `SocketHandlerServer listening on ${this.socketPath}`, { component: 'socket-handler-server' });
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.server.on('error', reject);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stop the server and clean up.
|
|
65
|
+
*/
|
|
66
|
+
public async stop(): Promise<void> {
|
|
67
|
+
// Destroy all active connections first
|
|
68
|
+
for (const socket of this.activeSockets) {
|
|
69
|
+
socket.destroy();
|
|
70
|
+
}
|
|
71
|
+
this.activeSockets.clear();
|
|
72
|
+
|
|
73
|
+
if (this.server) {
|
|
74
|
+
return new Promise<void>((resolve) => {
|
|
75
|
+
this.server!.close(() => {
|
|
76
|
+
this.server = null;
|
|
77
|
+
// Clean up socket file
|
|
78
|
+
plugins.fs.unlink(this.socketPath, () => resolve());
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Handle an incoming relayed connection from Rust.
|
|
86
|
+
*
|
|
87
|
+
* Protocol: Rust sends a single JSON line with metadata, then raw bytes follow.
|
|
88
|
+
* JSON format: { "routeKey": "my-route", "remoteIP": "1.2.3.4", "remotePort": 12345,
|
|
89
|
+
* "localPort": 443, "isTLS": true, "domain": "example.com" }
|
|
90
|
+
*/
|
|
91
|
+
private handleConnection(socket: plugins.net.Socket): void {
|
|
92
|
+
let metadataBuffer = '';
|
|
93
|
+
let metadataParsed = false;
|
|
94
|
+
|
|
95
|
+
const onData = (chunk: Buffer) => {
|
|
96
|
+
if (metadataParsed) return;
|
|
97
|
+
|
|
98
|
+
metadataBuffer += chunk.toString('utf8');
|
|
99
|
+
const newlineIndex = metadataBuffer.indexOf('\n');
|
|
100
|
+
|
|
101
|
+
if (newlineIndex === -1) {
|
|
102
|
+
// Haven't received full metadata line yet
|
|
103
|
+
if (metadataBuffer.length > 8192) {
|
|
104
|
+
logger.log('error', 'Socket handler metadata too large, closing', { component: 'socket-handler-server' });
|
|
105
|
+
socket.destroy();
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
metadataParsed = true;
|
|
111
|
+
socket.removeListener('data', onData);
|
|
112
|
+
socket.pause(); // Prevent data loss between handler removal and pipe setup
|
|
113
|
+
|
|
114
|
+
const metadataJson = metadataBuffer.slice(0, newlineIndex);
|
|
115
|
+
const remainingData = metadataBuffer.slice(newlineIndex + 1);
|
|
116
|
+
|
|
117
|
+
let metadata: any;
|
|
118
|
+
try {
|
|
119
|
+
metadata = JSON.parse(metadataJson);
|
|
120
|
+
} catch {
|
|
121
|
+
logger.log('error', `Invalid socket handler metadata JSON: ${metadataJson.slice(0, 200)}`, { component: 'socket-handler-server' });
|
|
122
|
+
socket.destroy();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.dispatchToHandler(socket, metadata, remainingData);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
socket.on('data', onData);
|
|
130
|
+
socket.on('error', (err) => {
|
|
131
|
+
logger.log('error', `Socket handler relay error: ${err.message}`, { component: 'socket-handler-server' });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Dispatch a relayed connection to the appropriate JS handler.
|
|
137
|
+
*/
|
|
138
|
+
private dispatchToHandler(socket: plugins.net.Socket, metadata: any, remainingData: string): void {
|
|
139
|
+
const routeKey = metadata.routeKey as string;
|
|
140
|
+
if (!routeKey) {
|
|
141
|
+
logger.log('error', 'Socket handler relay missing routeKey', { component: 'socket-handler-server' });
|
|
142
|
+
socket.destroy();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const originalRoute = this.preprocessor.getOriginalRoute(routeKey);
|
|
147
|
+
if (!originalRoute) {
|
|
148
|
+
logger.log('error', `No handler found for route: ${routeKey}`, { component: 'socket-handler-server' });
|
|
149
|
+
socket.destroy();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Build route context
|
|
154
|
+
const context: IRouteContext = {
|
|
155
|
+
port: metadata.localPort || 0,
|
|
156
|
+
domain: metadata.domain,
|
|
157
|
+
clientIp: metadata.remoteIP || 'unknown',
|
|
158
|
+
serverIp: '0.0.0.0',
|
|
159
|
+
path: metadata.path,
|
|
160
|
+
isTls: metadata.isTLS || false,
|
|
161
|
+
tlsVersion: metadata.tlsVersion,
|
|
162
|
+
routeName: originalRoute.name,
|
|
163
|
+
routeId: originalRoute.id,
|
|
164
|
+
timestamp: Date.now(),
|
|
165
|
+
connectionId: metadata.connectionId || `relay-${Date.now()}`,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// If there was remaining data after the metadata line, push it back
|
|
169
|
+
if (remainingData.length > 0) {
|
|
170
|
+
socket.unshift(Buffer.from(remainingData, 'utf8'));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const handler = originalRoute.action.socketHandler;
|
|
174
|
+
if (handler) {
|
|
175
|
+
// Route has an explicit socket handler callback
|
|
176
|
+
try {
|
|
177
|
+
const result = handler(socket, context);
|
|
178
|
+
// If the handler is async, wait for it to finish setup before resuming.
|
|
179
|
+
// This prevents data loss when async handlers need to do work before
|
|
180
|
+
// attaching their `data` listeners.
|
|
181
|
+
if (result && typeof (result as any).then === 'function') {
|
|
182
|
+
(result as any).then(() => {
|
|
183
|
+
socket.resume();
|
|
184
|
+
}).catch((err: any) => {
|
|
185
|
+
logger.log('error', `Async socket handler rejected for route ${routeKey}: ${err.message}`, { component: 'socket-handler-server' });
|
|
186
|
+
socket.destroy();
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
// Synchronous handler — listeners are already attached, safe to resume.
|
|
190
|
+
socket.resume();
|
|
191
|
+
}
|
|
192
|
+
} catch (err: any) {
|
|
193
|
+
logger.log('error', `Socket handler threw for route ${routeKey}: ${err.message}`, { component: 'socket-handler-server' });
|
|
194
|
+
socket.destroy();
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Route has dynamic host/port functions - resolve and forward
|
|
200
|
+
if (originalRoute.action.targets && originalRoute.action.targets.length > 0) {
|
|
201
|
+
this.forwardDynamicRoute(socket, originalRoute, context);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
logger.log('error', `Route ${routeKey} has no socketHandler and no targets`, { component: 'socket-handler-server' });
|
|
206
|
+
socket.destroy();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Forward a connection to a dynamically resolved target.
|
|
211
|
+
* Used for routes with function-based host/port that Rust cannot handle.
|
|
212
|
+
*/
|
|
213
|
+
private forwardDynamicRoute(socket: plugins.net.Socket, route: IRouteConfig, context: IRouteContext): void {
|
|
214
|
+
const targets = route.action.targets!;
|
|
215
|
+
// Pick a target (round-robin would be ideal, but simple random for now)
|
|
216
|
+
const target = targets[Math.floor(Math.random() * targets.length)];
|
|
217
|
+
|
|
218
|
+
// Resolve host
|
|
219
|
+
let host: string;
|
|
220
|
+
if (typeof target.host === 'function') {
|
|
221
|
+
try {
|
|
222
|
+
const result = target.host(context);
|
|
223
|
+
host = Array.isArray(result) ? result[Math.floor(Math.random() * result.length)] : result;
|
|
224
|
+
} catch (err: any) {
|
|
225
|
+
logger.log('error', `Dynamic host function failed: ${err.message}`, { component: 'socket-handler-server' });
|
|
226
|
+
socket.destroy();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
} else if (typeof target.host === 'string') {
|
|
230
|
+
host = target.host;
|
|
231
|
+
} else if (Array.isArray(target.host)) {
|
|
232
|
+
host = target.host[Math.floor(Math.random() * target.host.length)];
|
|
233
|
+
} else {
|
|
234
|
+
host = 'localhost';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Resolve port
|
|
238
|
+
let port: number;
|
|
239
|
+
if (typeof target.port === 'function') {
|
|
240
|
+
try {
|
|
241
|
+
port = target.port(context);
|
|
242
|
+
} catch (err: any) {
|
|
243
|
+
logger.log('error', `Dynamic port function failed: ${err.message}`, { component: 'socket-handler-server' });
|
|
244
|
+
socket.destroy();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
} else if (typeof target.port === 'number') {
|
|
248
|
+
port = target.port;
|
|
249
|
+
} else {
|
|
250
|
+
port = context.port;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
logger.log('debug', `Dynamic forward: ${context.clientIp} -> ${host}:${port}`, { component: 'socket-handler-server' });
|
|
254
|
+
|
|
255
|
+
// Connect to the resolved target
|
|
256
|
+
const backend = plugins.net.connect(port, host, () => {
|
|
257
|
+
// Pipe bidirectionally
|
|
258
|
+
socket.pipe(backend);
|
|
259
|
+
backend.pipe(socket);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
backend.on('error', (err) => {
|
|
263
|
+
logger.log('error', `Dynamic forward backend error: ${err.message}`, { component: 'socket-handler-server' });
|
|
264
|
+
socket.destroy();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
socket.on('error', () => {
|
|
268
|
+
backend.destroy();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
socket.on('close', () => {
|
|
272
|
+
backend.destroy();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
backend.on('close', () => {
|
|
276
|
+
socket.destroy();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
package/ts/routing/index.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Routing functionality module
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
// Export types and models
|
|
6
|
-
export * from '
|
|
5
|
+
// Export types and models
|
|
6
|
+
export * from './models/http-types.js';
|
|
7
7
|
|
|
8
8
|
// Export router functionality
|
|
9
9
|
export * from './router/index.js';
|
|
@@ -1,6 +1,149 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* in the HttpProxy module.
|
|
2
|
+
* HTTP types for routing module.
|
|
3
|
+
* These were previously in http-proxy and are now self-contained here.
|
|
5
4
|
*/
|
|
6
|
-
|
|
5
|
+
import * as plugins from '../../plugins.js';
|
|
6
|
+
import { HttpStatus as ProtocolHttpStatus, getStatusText as getProtocolStatusText } from '../../protocols/http/index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* HTTP-specific event types
|
|
10
|
+
*/
|
|
11
|
+
export enum HttpEvents {
|
|
12
|
+
REQUEST_RECEIVED = 'request-received',
|
|
13
|
+
REQUEST_FORWARDED = 'request-forwarded',
|
|
14
|
+
REQUEST_HANDLED = 'request-handled',
|
|
15
|
+
REQUEST_ERROR = 'request-error',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Re-export for backward compatibility with subset of commonly used codes
|
|
19
|
+
export const HttpStatus = {
|
|
20
|
+
OK: ProtocolHttpStatus.OK,
|
|
21
|
+
MOVED_PERMANENTLY: ProtocolHttpStatus.MOVED_PERMANENTLY,
|
|
22
|
+
FOUND: ProtocolHttpStatus.FOUND,
|
|
23
|
+
TEMPORARY_REDIRECT: ProtocolHttpStatus.TEMPORARY_REDIRECT,
|
|
24
|
+
PERMANENT_REDIRECT: ProtocolHttpStatus.PERMANENT_REDIRECT,
|
|
25
|
+
BAD_REQUEST: ProtocolHttpStatus.BAD_REQUEST,
|
|
26
|
+
UNAUTHORIZED: ProtocolHttpStatus.UNAUTHORIZED,
|
|
27
|
+
FORBIDDEN: ProtocolHttpStatus.FORBIDDEN,
|
|
28
|
+
NOT_FOUND: ProtocolHttpStatus.NOT_FOUND,
|
|
29
|
+
METHOD_NOT_ALLOWED: ProtocolHttpStatus.METHOD_NOT_ALLOWED,
|
|
30
|
+
REQUEST_TIMEOUT: ProtocolHttpStatus.REQUEST_TIMEOUT,
|
|
31
|
+
TOO_MANY_REQUESTS: ProtocolHttpStatus.TOO_MANY_REQUESTS,
|
|
32
|
+
INTERNAL_SERVER_ERROR: ProtocolHttpStatus.INTERNAL_SERVER_ERROR,
|
|
33
|
+
NOT_IMPLEMENTED: ProtocolHttpStatus.NOT_IMPLEMENTED,
|
|
34
|
+
BAD_GATEWAY: ProtocolHttpStatus.BAD_GATEWAY,
|
|
35
|
+
SERVICE_UNAVAILABLE: ProtocolHttpStatus.SERVICE_UNAVAILABLE,
|
|
36
|
+
GATEWAY_TIMEOUT: ProtocolHttpStatus.GATEWAY_TIMEOUT,
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Base error class for HTTP-related errors
|
|
41
|
+
*/
|
|
42
|
+
export class HttpError extends Error {
|
|
43
|
+
constructor(message: string, public readonly statusCode: number = HttpStatus.INTERNAL_SERVER_ERROR) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'HttpError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Error related to certificate operations
|
|
51
|
+
*/
|
|
52
|
+
export class CertificateError extends HttpError {
|
|
53
|
+
constructor(
|
|
54
|
+
message: string,
|
|
55
|
+
public readonly domain: string,
|
|
56
|
+
public readonly isRenewal: boolean = false
|
|
57
|
+
) {
|
|
58
|
+
super(`${message} for domain ${domain}${isRenewal ? ' (renewal)' : ''}`, HttpStatus.INTERNAL_SERVER_ERROR);
|
|
59
|
+
this.name = 'CertificateError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Error related to server operations
|
|
65
|
+
*/
|
|
66
|
+
export class ServerError extends HttpError {
|
|
67
|
+
constructor(message: string, public readonly code?: string, statusCode: number = HttpStatus.INTERNAL_SERVER_ERROR) {
|
|
68
|
+
super(message, statusCode);
|
|
69
|
+
this.name = 'ServerError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Error for bad requests
|
|
75
|
+
*/
|
|
76
|
+
export class BadRequestError extends HttpError {
|
|
77
|
+
constructor(message: string) {
|
|
78
|
+
super(message, HttpStatus.BAD_REQUEST);
|
|
79
|
+
this.name = 'BadRequestError';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Error for not found resources
|
|
85
|
+
*/
|
|
86
|
+
export class NotFoundError extends HttpError {
|
|
87
|
+
constructor(message: string = 'Resource not found') {
|
|
88
|
+
super(message, HttpStatus.NOT_FOUND);
|
|
89
|
+
this.name = 'NotFoundError';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Redirect configuration for HTTP requests
|
|
95
|
+
*/
|
|
96
|
+
export interface IRedirectConfig {
|
|
97
|
+
source: string;
|
|
98
|
+
destination: string;
|
|
99
|
+
type: number;
|
|
100
|
+
preserveQuery?: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* HTTP router configuration
|
|
105
|
+
*/
|
|
106
|
+
export interface IRouterConfig {
|
|
107
|
+
routes: Array<{
|
|
108
|
+
path: string;
|
|
109
|
+
method?: string;
|
|
110
|
+
handler: (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void | Promise<void>;
|
|
111
|
+
}>;
|
|
112
|
+
notFoundHandler?: (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void;
|
|
113
|
+
errorHandler?: (error: Error, req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* HTTP request method types
|
|
118
|
+
*/
|
|
119
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE';
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Helper function to get HTTP status text
|
|
123
|
+
*/
|
|
124
|
+
export function getStatusText(status: number): string {
|
|
125
|
+
return getProtocolStatusText(status as ProtocolHttpStatus);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Legacy interfaces for backward compatibility
|
|
129
|
+
export interface IDomainOptions {
|
|
130
|
+
domainName: string;
|
|
131
|
+
sslRedirect: boolean;
|
|
132
|
+
acmeMaintenance: boolean;
|
|
133
|
+
forward?: { ip: string; port: number };
|
|
134
|
+
acmeForward?: { ip: string; port: number };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface IDomainCertificate {
|
|
138
|
+
options: IDomainOptions;
|
|
139
|
+
certObtained: boolean;
|
|
140
|
+
obtainingInProgress: boolean;
|
|
141
|
+
certificate?: string;
|
|
142
|
+
privateKey?: string;
|
|
143
|
+
expiryDate?: Date;
|
|
144
|
+
lastRenewalAttempt?: Date;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Backward compatibility exports
|
|
148
|
+
export { HttpError as Port80HandlerError };
|
|
149
|
+
export { CertificateError as CertError };
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import * as plugins from '../../plugins.js';
|
|
2
|
-
import { type IHttpProxyOptions, type IConnectionEntry, type ILogger, createLogger } from './models/types.js';
|
|
3
|
-
import { cleanupSocket } from '../../core/utils/socket-utils.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Manages a pool of backend connections for efficient reuse
|
|
7
|
-
*/
|
|
8
|
-
export class ConnectionPool {
|
|
9
|
-
private connectionPool: Map<string, Array<IConnectionEntry>> = new Map();
|
|
10
|
-
private roundRobinPositions: Map<string, number> = new Map();
|
|
11
|
-
private logger: ILogger;
|
|
12
|
-
|
|
13
|
-
constructor(private options: IHttpProxyOptions) {
|
|
14
|
-
this.logger = createLogger(options.logLevel || 'info');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Get a connection from the pool or create a new one
|
|
19
|
-
*/
|
|
20
|
-
public getConnection(host: string, port: number): Promise<plugins.net.Socket> {
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
const poolKey = `${host}:${port}`;
|
|
23
|
-
const connectionList = this.connectionPool.get(poolKey) || [];
|
|
24
|
-
|
|
25
|
-
// Look for an idle connection
|
|
26
|
-
const idleConnectionIndex = connectionList.findIndex(c => c.isIdle);
|
|
27
|
-
|
|
28
|
-
if (idleConnectionIndex >= 0) {
|
|
29
|
-
// Get existing connection from pool
|
|
30
|
-
const connection = connectionList[idleConnectionIndex];
|
|
31
|
-
connection.isIdle = false;
|
|
32
|
-
connection.lastUsed = Date.now();
|
|
33
|
-
this.logger.debug(`Reusing connection from pool for ${poolKey}`);
|
|
34
|
-
|
|
35
|
-
// Update the pool
|
|
36
|
-
this.connectionPool.set(poolKey, connectionList);
|
|
37
|
-
|
|
38
|
-
resolve(connection.socket);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// No idle connection available, create a new one if pool isn't full
|
|
43
|
-
const poolSize = this.options.connectionPoolSize || 50;
|
|
44
|
-
if (connectionList.length < poolSize) {
|
|
45
|
-
this.logger.debug(`Creating new connection to ${host}:${port}`);
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const socket = plugins.net.connect({
|
|
49
|
-
host,
|
|
50
|
-
port,
|
|
51
|
-
keepAlive: true,
|
|
52
|
-
keepAliveInitialDelay: 30000 // 30 seconds
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
socket.once('connect', () => {
|
|
56
|
-
// Add to connection pool
|
|
57
|
-
const connection = {
|
|
58
|
-
socket,
|
|
59
|
-
lastUsed: Date.now(),
|
|
60
|
-
isIdle: false
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
connectionList.push(connection);
|
|
64
|
-
this.connectionPool.set(poolKey, connectionList);
|
|
65
|
-
|
|
66
|
-
// Setup cleanup when the connection is closed
|
|
67
|
-
socket.once('close', () => {
|
|
68
|
-
const idx = connectionList.findIndex(c => c.socket === socket);
|
|
69
|
-
if (idx >= 0) {
|
|
70
|
-
connectionList.splice(idx, 1);
|
|
71
|
-
this.connectionPool.set(poolKey, connectionList);
|
|
72
|
-
this.logger.debug(`Removed closed connection from pool for ${poolKey}`);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
resolve(socket);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
socket.once('error', (err) => {
|
|
80
|
-
this.logger.error(`Error creating connection to ${host}:${port}`, err);
|
|
81
|
-
reject(err);
|
|
82
|
-
});
|
|
83
|
-
} catch (err) {
|
|
84
|
-
this.logger.error(`Failed to create connection to ${host}:${port}`, err);
|
|
85
|
-
reject(err);
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
// Pool is full, wait for an idle connection or reject
|
|
89
|
-
this.logger.warn(`Connection pool for ${poolKey} is full (${connectionList.length})`);
|
|
90
|
-
reject(new Error(`Connection pool for ${poolKey} is full`));
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Return a connection to the pool for reuse
|
|
97
|
-
*/
|
|
98
|
-
public returnConnection(socket: plugins.net.Socket, host: string, port: number): void {
|
|
99
|
-
const poolKey = `${host}:${port}`;
|
|
100
|
-
const connectionList = this.connectionPool.get(poolKey) || [];
|
|
101
|
-
|
|
102
|
-
// Find this connection in the pool
|
|
103
|
-
const connectionIndex = connectionList.findIndex(c => c.socket === socket);
|
|
104
|
-
|
|
105
|
-
if (connectionIndex >= 0) {
|
|
106
|
-
// Mark as idle and update last used time
|
|
107
|
-
connectionList[connectionIndex].isIdle = true;
|
|
108
|
-
connectionList[connectionIndex].lastUsed = Date.now();
|
|
109
|
-
|
|
110
|
-
this.logger.debug(`Returned connection to pool for ${poolKey}`);
|
|
111
|
-
} else {
|
|
112
|
-
this.logger.warn(`Attempted to return unknown connection to pool for ${poolKey}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Cleanup the connection pool by removing idle connections
|
|
118
|
-
* or reducing pool size if it exceeds the configured maximum
|
|
119
|
-
*/
|
|
120
|
-
public cleanupConnectionPool(): void {
|
|
121
|
-
const now = Date.now();
|
|
122
|
-
const idleTimeout = this.options.keepAliveTimeout || 120000; // 2 minutes default
|
|
123
|
-
|
|
124
|
-
for (const [host, connections] of this.connectionPool.entries()) {
|
|
125
|
-
// Sort by last used time (oldest first)
|
|
126
|
-
connections.sort((a, b) => a.lastUsed - b.lastUsed);
|
|
127
|
-
|
|
128
|
-
// Remove idle connections older than the idle timeout
|
|
129
|
-
let removed = 0;
|
|
130
|
-
while (connections.length > 0) {
|
|
131
|
-
const connection = connections[0];
|
|
132
|
-
|
|
133
|
-
// Remove if idle and exceeds timeout, or if pool is too large
|
|
134
|
-
if ((connection.isIdle && now - connection.lastUsed > idleTimeout) ||
|
|
135
|
-
connections.length > (this.options.connectionPoolSize || 50)) {
|
|
136
|
-
|
|
137
|
-
cleanupSocket(connection.socket, `pool-${host}-idle`, { immediate: true }).catch(() => {});
|
|
138
|
-
|
|
139
|
-
connections.shift(); // Remove from pool
|
|
140
|
-
removed++;
|
|
141
|
-
} else {
|
|
142
|
-
break; // Stop removing if we've reached active or recent connections
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (removed > 0) {
|
|
147
|
-
this.logger.debug(`Removed ${removed} idle connections from pool for ${host}, ${connections.length} remaining`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Update the pool with the remaining connections
|
|
151
|
-
if (connections.length === 0) {
|
|
152
|
-
this.connectionPool.delete(host);
|
|
153
|
-
} else {
|
|
154
|
-
this.connectionPool.set(host, connections);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Close all connections in the pool
|
|
161
|
-
*/
|
|
162
|
-
public closeAllConnections(): void {
|
|
163
|
-
for (const [host, connections] of this.connectionPool.entries()) {
|
|
164
|
-
this.logger.debug(`Closing ${connections.length} connections to ${host}`);
|
|
165
|
-
|
|
166
|
-
for (const connection of connections) {
|
|
167
|
-
cleanupSocket(connection.socket, `pool-${host}-close`, { immediate: true }).catch(() => {});
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
this.connectionPool.clear();
|
|
172
|
-
this.roundRobinPositions.clear();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Get load balancing target using round-robin
|
|
177
|
-
*/
|
|
178
|
-
public getNextTarget(targets: string[], port: number): { host: string, port: number } {
|
|
179
|
-
const targetKey = targets.join(',');
|
|
180
|
-
|
|
181
|
-
// Initialize position if not exists
|
|
182
|
-
if (!this.roundRobinPositions.has(targetKey)) {
|
|
183
|
-
this.roundRobinPositions.set(targetKey, 0);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Get current position and increment for next time
|
|
187
|
-
const currentPosition = this.roundRobinPositions.get(targetKey)!;
|
|
188
|
-
const nextPosition = (currentPosition + 1) % targets.length;
|
|
189
|
-
this.roundRobinPositions.set(targetKey, nextPosition);
|
|
190
|
-
|
|
191
|
-
// Return the selected target
|
|
192
|
-
return {
|
|
193
|
-
host: targets[currentPosition],
|
|
194
|
-
port
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Gets the connection pool status
|
|
200
|
-
*/
|
|
201
|
-
public getPoolStatus(): Record<string, { total: number, idle: number }> {
|
|
202
|
-
return Object.fromEntries(
|
|
203
|
-
Array.from(this.connectionPool.entries()).map(([host, connections]) => [
|
|
204
|
-
host,
|
|
205
|
-
{
|
|
206
|
-
total: connections.length,
|
|
207
|
-
idle: connections.filter(c => c.isIdle).length
|
|
208
|
-
}
|
|
209
|
-
])
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Setup a periodic cleanup task
|
|
215
|
-
*/
|
|
216
|
-
public setupPeriodicCleanup(interval: number = 60000): NodeJS.Timeout {
|
|
217
|
-
const timer = setInterval(() => {
|
|
218
|
-
this.cleanupConnectionPool();
|
|
219
|
-
}, interval);
|
|
220
|
-
|
|
221
|
-
// Don't prevent process exit
|
|
222
|
-
if (timer.unref) {
|
|
223
|
-
timer.unref();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return timer;
|
|
227
|
-
}
|
|
228
|
-
}
|