@push.rocks/smartproxy 5.0.0 → 6.0.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.pp.interfaces.d.ts +23 -0
- package/dist_ts/classes.pp.networkproxybridge.d.ts +15 -1
- package/dist_ts/classes.pp.networkproxybridge.js +116 -21
- package/dist_ts/classes.pp.portproxy.d.ts +20 -4
- package/dist_ts/classes.pp.portproxy.js +321 -22
- package/dist_ts/index.d.ts +6 -6
- package/dist_ts/index.js +7 -7
- package/dist_ts/networkproxy/classes.np.certificatemanager.d.ts +77 -0
- package/dist_ts/networkproxy/classes.np.certificatemanager.js +354 -0
- package/dist_ts/networkproxy/classes.np.connectionpool.d.ts +47 -0
- package/dist_ts/networkproxy/classes.np.connectionpool.js +210 -0
- package/dist_ts/networkproxy/classes.np.networkproxy.d.ts +117 -0
- package/dist_ts/networkproxy/classes.np.networkproxy.js +375 -0
- package/dist_ts/networkproxy/classes.np.requesthandler.d.ts +51 -0
- package/dist_ts/networkproxy/classes.np.requesthandler.js +210 -0
- package/dist_ts/networkproxy/classes.np.types.d.ts +82 -0
- package/dist_ts/networkproxy/classes.np.types.js +35 -0
- package/dist_ts/networkproxy/classes.np.websockethandler.d.ts +38 -0
- package/dist_ts/networkproxy/classes.np.websockethandler.js +188 -0
- package/dist_ts/networkproxy/index.d.ts +6 -0
- package/dist_ts/networkproxy/index.js +8 -0
- package/dist_ts/nfttablesproxy/classes.nftablesproxy.d.ts +219 -0
- package/dist_ts/nfttablesproxy/classes.nftablesproxy.js +1542 -0
- package/dist_ts/port80handler/classes.port80handler.d.ts +260 -0
- package/dist_ts/port80handler/classes.port80handler.js +928 -0
- package/dist_ts/smartproxy/classes.pp.connectionhandler.d.ts +39 -0
- package/dist_ts/smartproxy/classes.pp.connectionhandler.js +754 -0
- package/dist_ts/smartproxy/classes.pp.connectionmanager.d.ts +78 -0
- package/dist_ts/smartproxy/classes.pp.connectionmanager.js +378 -0
- package/dist_ts/smartproxy/classes.pp.domainconfigmanager.d.ts +55 -0
- package/dist_ts/smartproxy/classes.pp.domainconfigmanager.js +103 -0
- package/dist_ts/smartproxy/classes.pp.interfaces.d.ts +133 -0
- package/dist_ts/smartproxy/classes.pp.interfaces.js +2 -0
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +57 -0
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +306 -0
- package/dist_ts/smartproxy/classes.pp.portrangemanager.d.ts +56 -0
- package/dist_ts/smartproxy/classes.pp.portrangemanager.js +179 -0
- package/dist_ts/smartproxy/classes.pp.securitymanager.d.ts +47 -0
- package/dist_ts/smartproxy/classes.pp.securitymanager.js +126 -0
- package/dist_ts/smartproxy/classes.pp.snihandler.d.ts +153 -0
- package/dist_ts/smartproxy/classes.pp.snihandler.js +1053 -0
- package/dist_ts/smartproxy/classes.pp.timeoutmanager.d.ts +47 -0
- package/dist_ts/smartproxy/classes.pp.timeoutmanager.js +154 -0
- package/dist_ts/smartproxy/classes.pp.tlsalert.d.ts +149 -0
- package/dist_ts/smartproxy/classes.pp.tlsalert.js +225 -0
- package/dist_ts/smartproxy/classes.pp.tlsmanager.d.ts +57 -0
- package/dist_ts/smartproxy/classes.pp.tlsmanager.js +132 -0
- package/dist_ts/smartproxy/classes.smartproxy.d.ts +64 -0
- package/dist_ts/smartproxy/classes.smartproxy.js +567 -0
- package/package.json +1 -1
- package/readme.md +77 -27
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +6 -6
- package/ts/networkproxy/classes.np.certificatemanager.ts +398 -0
- package/ts/networkproxy/classes.np.connectionpool.ts +241 -0
- package/ts/networkproxy/classes.np.networkproxy.ts +469 -0
- package/ts/networkproxy/classes.np.requesthandler.ts +278 -0
- package/ts/networkproxy/classes.np.types.ts +123 -0
- package/ts/networkproxy/classes.np.websockethandler.ts +226 -0
- package/ts/networkproxy/index.ts +7 -0
- package/ts/{classes.port80handler.ts → port80handler/classes.port80handler.ts} +249 -1
- package/ts/{classes.pp.connectionhandler.ts → smartproxy/classes.pp.connectionhandler.ts} +1 -1
- package/ts/{classes.pp.connectionmanager.ts → smartproxy/classes.pp.connectionmanager.ts} +1 -1
- package/ts/{classes.pp.domainconfigmanager.ts → smartproxy/classes.pp.domainconfigmanager.ts} +1 -1
- package/ts/{classes.pp.interfaces.ts → smartproxy/classes.pp.interfaces.ts} +31 -5
- package/ts/{classes.pp.networkproxybridge.ts → smartproxy/classes.pp.networkproxybridge.ts} +129 -28
- package/ts/{classes.pp.securitymanager.ts → smartproxy/classes.pp.securitymanager.ts} +1 -1
- package/ts/{classes.pp.tlsmanager.ts → smartproxy/classes.pp.tlsmanager.ts} +1 -1
- package/ts/smartproxy/classes.smartproxy.ts +679 -0
- package/ts/classes.networkproxy.ts +0 -1730
- package/ts/classes.pp.acmemanager.ts +0 -149
- package/ts/classes.pp.portproxy.ts +0 -344
- /package/ts/{classes.nftablesproxy.ts → nfttablesproxy/classes.nftablesproxy.ts} +0 -0
- /package/ts/{classes.pp.portrangemanager.ts → smartproxy/classes.pp.portrangemanager.ts} +0 -0
- /package/ts/{classes.pp.snihandler.ts → smartproxy/classes.pp.snihandler.ts} +0 -0
- /package/ts/{classes.pp.timeoutmanager.ts → smartproxy/classes.pp.timeoutmanager.ts} +0 -0
- /package/ts/{classes.pp.tlsalert.ts → smartproxy/classes.pp.tlsalert.ts} +0 -0
|
@@ -1,1730 +0,0 @@
|
|
|
1
|
-
import * as plugins from './plugins.js';
|
|
2
|
-
import { ProxyRouter } from './classes.router.js';
|
|
3
|
-
import { Port80Handler, Port80HandlerEvents, type IDomainOptions } from './classes.port80handler.js';
|
|
4
|
-
import * as fs from 'fs';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
|
|
8
|
-
export interface INetworkProxyOptions {
|
|
9
|
-
port: number;
|
|
10
|
-
maxConnections?: number;
|
|
11
|
-
keepAliveTimeout?: number;
|
|
12
|
-
headersTimeout?: number;
|
|
13
|
-
logLevel?: 'error' | 'warn' | 'info' | 'debug';
|
|
14
|
-
cors?: {
|
|
15
|
-
allowOrigin?: string;
|
|
16
|
-
allowMethods?: string;
|
|
17
|
-
allowHeaders?: string;
|
|
18
|
-
maxAge?: number;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
// New settings for PortProxy integration
|
|
22
|
-
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
|
|
23
|
-
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
|
|
24
|
-
|
|
25
|
-
// ACME certificate management options
|
|
26
|
-
acme?: {
|
|
27
|
-
enabled?: boolean; // Whether to enable automatic certificate management
|
|
28
|
-
port?: number; // Port to listen on for ACME challenges (default: 80)
|
|
29
|
-
contactEmail?: string; // Email for Let's Encrypt account
|
|
30
|
-
useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
|
|
31
|
-
renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
|
|
32
|
-
autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
|
|
33
|
-
certificateStore?: string; // Directory to store certificates (default: ./certs)
|
|
34
|
-
skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface IWebSocketWithHeartbeat extends plugins.wsDefault {
|
|
39
|
-
lastPong: number;
|
|
40
|
-
isAlive: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export class NetworkProxy {
|
|
44
|
-
// Configuration
|
|
45
|
-
public options: INetworkProxyOptions;
|
|
46
|
-
public proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
|
47
|
-
public defaultHeaders: { [key: string]: string } = {};
|
|
48
|
-
|
|
49
|
-
// Server instances
|
|
50
|
-
public httpsServer: plugins.https.Server;
|
|
51
|
-
public wsServer: plugins.ws.WebSocketServer;
|
|
52
|
-
|
|
53
|
-
// State tracking
|
|
54
|
-
public router = new ProxyRouter();
|
|
55
|
-
public socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
|
|
56
|
-
public activeContexts: Set<string> = new Set();
|
|
57
|
-
public connectedClients: number = 0;
|
|
58
|
-
public startTime: number = 0;
|
|
59
|
-
public requestsServed: number = 0;
|
|
60
|
-
public failedRequests: number = 0;
|
|
61
|
-
|
|
62
|
-
// New tracking for PortProxy integration
|
|
63
|
-
private portProxyConnections: number = 0;
|
|
64
|
-
private tlsTerminatedConnections: number = 0;
|
|
65
|
-
|
|
66
|
-
// Timers and intervals
|
|
67
|
-
private heartbeatInterval: NodeJS.Timeout;
|
|
68
|
-
private metricsInterval: NodeJS.Timeout;
|
|
69
|
-
private connectionPoolCleanupInterval: NodeJS.Timeout;
|
|
70
|
-
|
|
71
|
-
// Certificates
|
|
72
|
-
private defaultCertificates: { key: string; cert: string };
|
|
73
|
-
private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map();
|
|
74
|
-
|
|
75
|
-
// Port80Handler for certificate management
|
|
76
|
-
private port80Handler: Port80Handler | null = null;
|
|
77
|
-
private certificateStoreDir: string;
|
|
78
|
-
|
|
79
|
-
// New connection pool for backend connections
|
|
80
|
-
private connectionPool: Map<string, Array<{
|
|
81
|
-
socket: plugins.net.Socket;
|
|
82
|
-
lastUsed: number;
|
|
83
|
-
isIdle: boolean;
|
|
84
|
-
}>> = new Map();
|
|
85
|
-
|
|
86
|
-
// Track round-robin positions for load balancing
|
|
87
|
-
private roundRobinPositions: Map<string, number> = new Map();
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Creates a new NetworkProxy instance
|
|
91
|
-
*/
|
|
92
|
-
constructor(optionsArg: INetworkProxyOptions) {
|
|
93
|
-
// Set default options
|
|
94
|
-
this.options = {
|
|
95
|
-
port: optionsArg.port,
|
|
96
|
-
maxConnections: optionsArg.maxConnections || 10000,
|
|
97
|
-
keepAliveTimeout: optionsArg.keepAliveTimeout || 120000, // 2 minutes
|
|
98
|
-
headersTimeout: optionsArg.headersTimeout || 60000, // 1 minute
|
|
99
|
-
logLevel: optionsArg.logLevel || 'info',
|
|
100
|
-
cors: optionsArg.cors || {
|
|
101
|
-
allowOrigin: '*',
|
|
102
|
-
allowMethods: 'GET, POST, PUT, DELETE, OPTIONS',
|
|
103
|
-
allowHeaders: 'Content-Type, Authorization',
|
|
104
|
-
maxAge: 86400
|
|
105
|
-
},
|
|
106
|
-
// New defaults for PortProxy integration
|
|
107
|
-
connectionPoolSize: optionsArg.connectionPoolSize || 50,
|
|
108
|
-
portProxyIntegration: optionsArg.portProxyIntegration || false,
|
|
109
|
-
// Default ACME options
|
|
110
|
-
acme: {
|
|
111
|
-
enabled: optionsArg.acme?.enabled || false,
|
|
112
|
-
port: optionsArg.acme?.port || 80,
|
|
113
|
-
contactEmail: optionsArg.acme?.contactEmail || 'admin@example.com',
|
|
114
|
-
useProduction: optionsArg.acme?.useProduction || false, // Default to staging for safety
|
|
115
|
-
renewThresholdDays: optionsArg.acme?.renewThresholdDays || 30,
|
|
116
|
-
autoRenew: optionsArg.acme?.autoRenew !== false, // Default to true
|
|
117
|
-
certificateStore: optionsArg.acme?.certificateStore || './certs',
|
|
118
|
-
skipConfiguredCerts: optionsArg.acme?.skipConfiguredCerts || false
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// Set up certificate store directory
|
|
123
|
-
this.certificateStoreDir = path.resolve(this.options.acme.certificateStore);
|
|
124
|
-
|
|
125
|
-
// Ensure certificate store directory exists
|
|
126
|
-
try {
|
|
127
|
-
if (!fs.existsSync(this.certificateStoreDir)) {
|
|
128
|
-
fs.mkdirSync(this.certificateStoreDir, { recursive: true });
|
|
129
|
-
this.log('info', `Created certificate store directory: ${this.certificateStoreDir}`);
|
|
130
|
-
}
|
|
131
|
-
} catch (error) {
|
|
132
|
-
this.log('warn', `Failed to create certificate store directory: ${error}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
this.loadDefaultCertificates();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Loads default certificates from the filesystem
|
|
140
|
-
*/
|
|
141
|
-
private loadDefaultCertificates(): void {
|
|
142
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
143
|
-
const certPath = path.join(__dirname, '..', 'assets', 'certs');
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
this.defaultCertificates = {
|
|
147
|
-
key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'),
|
|
148
|
-
cert: fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8')
|
|
149
|
-
};
|
|
150
|
-
this.log('info', 'Default certificates loaded successfully');
|
|
151
|
-
} catch (error) {
|
|
152
|
-
this.log('error', 'Error loading default certificates', error);
|
|
153
|
-
|
|
154
|
-
// Generate self-signed fallback certificates
|
|
155
|
-
try {
|
|
156
|
-
// This is a placeholder for actual certificate generation code
|
|
157
|
-
// In a real implementation, you would use a library like selfsigned to generate certs
|
|
158
|
-
this.defaultCertificates = {
|
|
159
|
-
key: "FALLBACK_KEY_CONTENT",
|
|
160
|
-
cert: "FALLBACK_CERT_CONTENT"
|
|
161
|
-
};
|
|
162
|
-
this.log('warn', 'Using fallback self-signed certificates');
|
|
163
|
-
} catch (fallbackError) {
|
|
164
|
-
this.log('error', 'Failed to generate fallback certificates', fallbackError);
|
|
165
|
-
throw new Error('Could not load or generate SSL certificates');
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Returns the port number this NetworkProxy is listening on
|
|
172
|
-
* Useful for PortProxy to determine where to forward connections
|
|
173
|
-
*/
|
|
174
|
-
public getListeningPort(): number {
|
|
175
|
-
return this.options.port;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Updates the server capacity settings
|
|
180
|
-
* @param maxConnections Maximum number of simultaneous connections
|
|
181
|
-
* @param keepAliveTimeout Keep-alive timeout in milliseconds
|
|
182
|
-
* @param connectionPoolSize Size of the connection pool per backend
|
|
183
|
-
*/
|
|
184
|
-
public updateCapacity(maxConnections?: number, keepAliveTimeout?: number, connectionPoolSize?: number): void {
|
|
185
|
-
if (maxConnections !== undefined) {
|
|
186
|
-
this.options.maxConnections = maxConnections;
|
|
187
|
-
this.log('info', `Updated max connections to ${maxConnections}`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (keepAliveTimeout !== undefined) {
|
|
191
|
-
this.options.keepAliveTimeout = keepAliveTimeout;
|
|
192
|
-
|
|
193
|
-
if (this.httpsServer) {
|
|
194
|
-
this.httpsServer.keepAliveTimeout = keepAliveTimeout;
|
|
195
|
-
this.log('info', `Updated keep-alive timeout to ${keepAliveTimeout}ms`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (connectionPoolSize !== undefined) {
|
|
200
|
-
this.options.connectionPoolSize = connectionPoolSize;
|
|
201
|
-
this.log('info', `Updated connection pool size to ${connectionPoolSize}`);
|
|
202
|
-
|
|
203
|
-
// Cleanup excess connections in the pool if the size was reduced
|
|
204
|
-
this.cleanupConnectionPool();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Returns current server metrics
|
|
210
|
-
* Useful for PortProxy to determine which NetworkProxy to use for load balancing
|
|
211
|
-
*/
|
|
212
|
-
public getMetrics(): any {
|
|
213
|
-
return {
|
|
214
|
-
activeConnections: this.connectedClients,
|
|
215
|
-
totalRequests: this.requestsServed,
|
|
216
|
-
failedRequests: this.failedRequests,
|
|
217
|
-
portProxyConnections: this.portProxyConnections,
|
|
218
|
-
tlsTerminatedConnections: this.tlsTerminatedConnections,
|
|
219
|
-
connectionPoolSize: Array.from(this.connectionPool.entries()).reduce((acc, [host, connections]) => {
|
|
220
|
-
acc[host] = connections.length;
|
|
221
|
-
return acc;
|
|
222
|
-
}, {} as Record<string, number>),
|
|
223
|
-
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
|
224
|
-
memoryUsage: process.memoryUsage(),
|
|
225
|
-
activeWebSockets: this.wsServer?.clients.size || 0
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Cleanup the connection pool by removing idle connections
|
|
231
|
-
* or reducing pool size if it exceeds the configured maximum
|
|
232
|
-
*/
|
|
233
|
-
private cleanupConnectionPool(): void {
|
|
234
|
-
const now = Date.now();
|
|
235
|
-
const idleTimeout = this.options.keepAliveTimeout || 120000; // 2 minutes default
|
|
236
|
-
|
|
237
|
-
for (const [host, connections] of this.connectionPool.entries()) {
|
|
238
|
-
// Sort by last used time (oldest first)
|
|
239
|
-
connections.sort((a, b) => a.lastUsed - b.lastUsed);
|
|
240
|
-
|
|
241
|
-
// Remove idle connections older than the idle timeout
|
|
242
|
-
let removed = 0;
|
|
243
|
-
while (connections.length > 0) {
|
|
244
|
-
const connection = connections[0];
|
|
245
|
-
|
|
246
|
-
// Remove if idle and exceeds timeout, or if pool is too large
|
|
247
|
-
if ((connection.isIdle && now - connection.lastUsed > idleTimeout) ||
|
|
248
|
-
connections.length > this.options.connectionPoolSize!) {
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
if (!connection.socket.destroyed) {
|
|
252
|
-
connection.socket.end();
|
|
253
|
-
connection.socket.destroy();
|
|
254
|
-
}
|
|
255
|
-
} catch (err) {
|
|
256
|
-
this.log('error', `Error destroying pooled connection to ${host}`, err);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
connections.shift(); // Remove from pool
|
|
260
|
-
removed++;
|
|
261
|
-
} else {
|
|
262
|
-
break; // Stop removing if we've reached active or recent connections
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (removed > 0) {
|
|
267
|
-
this.log('debug', `Removed ${removed} idle connections from pool for ${host}, ${connections.length} remaining`);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Update the pool with the remaining connections
|
|
271
|
-
if (connections.length === 0) {
|
|
272
|
-
this.connectionPool.delete(host);
|
|
273
|
-
} else {
|
|
274
|
-
this.connectionPool.set(host, connections);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Get a connection from the pool or create a new one
|
|
281
|
-
*/
|
|
282
|
-
private getConnectionFromPool(host: string, port: number): Promise<plugins.net.Socket> {
|
|
283
|
-
return new Promise((resolve, reject) => {
|
|
284
|
-
const poolKey = `${host}:${port}`;
|
|
285
|
-
const connectionList = this.connectionPool.get(poolKey) || [];
|
|
286
|
-
|
|
287
|
-
// Look for an idle connection
|
|
288
|
-
const idleConnectionIndex = connectionList.findIndex(c => c.isIdle);
|
|
289
|
-
|
|
290
|
-
if (idleConnectionIndex >= 0) {
|
|
291
|
-
// Get existing connection from pool
|
|
292
|
-
const connection = connectionList[idleConnectionIndex];
|
|
293
|
-
connection.isIdle = false;
|
|
294
|
-
connection.lastUsed = Date.now();
|
|
295
|
-
this.log('debug', `Reusing connection from pool for ${poolKey}`);
|
|
296
|
-
|
|
297
|
-
// Update the pool
|
|
298
|
-
this.connectionPool.set(poolKey, connectionList);
|
|
299
|
-
|
|
300
|
-
resolve(connection.socket);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// No idle connection available, create a new one if pool isn't full
|
|
305
|
-
if (connectionList.length < this.options.connectionPoolSize!) {
|
|
306
|
-
this.log('debug', `Creating new connection to ${host}:${port}`);
|
|
307
|
-
|
|
308
|
-
try {
|
|
309
|
-
const socket = plugins.net.connect({
|
|
310
|
-
host,
|
|
311
|
-
port,
|
|
312
|
-
keepAlive: true,
|
|
313
|
-
keepAliveInitialDelay: 30000 // 30 seconds
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
socket.once('connect', () => {
|
|
317
|
-
// Add to connection pool
|
|
318
|
-
const connection = {
|
|
319
|
-
socket,
|
|
320
|
-
lastUsed: Date.now(),
|
|
321
|
-
isIdle: false
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
connectionList.push(connection);
|
|
325
|
-
this.connectionPool.set(poolKey, connectionList);
|
|
326
|
-
|
|
327
|
-
// Setup cleanup when the connection is closed
|
|
328
|
-
socket.once('close', () => {
|
|
329
|
-
const idx = connectionList.findIndex(c => c.socket === socket);
|
|
330
|
-
if (idx >= 0) {
|
|
331
|
-
connectionList.splice(idx, 1);
|
|
332
|
-
this.connectionPool.set(poolKey, connectionList);
|
|
333
|
-
this.log('debug', `Removed closed connection from pool for ${poolKey}`);
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
resolve(socket);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
socket.once('error', (err) => {
|
|
341
|
-
this.log('error', `Error creating connection to ${host}:${port}`, err);
|
|
342
|
-
reject(err);
|
|
343
|
-
});
|
|
344
|
-
} catch (err) {
|
|
345
|
-
this.log('error', `Failed to create connection to ${host}:${port}`, err);
|
|
346
|
-
reject(err);
|
|
347
|
-
}
|
|
348
|
-
} else {
|
|
349
|
-
// Pool is full, wait for an idle connection or reject
|
|
350
|
-
this.log('warn', `Connection pool for ${poolKey} is full (${connectionList.length})`);
|
|
351
|
-
reject(new Error(`Connection pool for ${poolKey} is full`));
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Return a connection to the pool for reuse
|
|
358
|
-
*/
|
|
359
|
-
private returnConnectionToPool(socket: plugins.net.Socket, host: string, port: number): void {
|
|
360
|
-
const poolKey = `${host}:${port}`;
|
|
361
|
-
const connectionList = this.connectionPool.get(poolKey) || [];
|
|
362
|
-
|
|
363
|
-
// Find this connection in the pool
|
|
364
|
-
const connectionIndex = connectionList.findIndex(c => c.socket === socket);
|
|
365
|
-
|
|
366
|
-
if (connectionIndex >= 0) {
|
|
367
|
-
// Mark as idle and update last used time
|
|
368
|
-
connectionList[connectionIndex].isIdle = true;
|
|
369
|
-
connectionList[connectionIndex].lastUsed = Date.now();
|
|
370
|
-
|
|
371
|
-
this.log('debug', `Returned connection to pool for ${poolKey}`);
|
|
372
|
-
} else {
|
|
373
|
-
this.log('warn', `Attempted to return unknown connection to pool for ${poolKey}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Initializes the Port80Handler for ACME certificate management
|
|
379
|
-
* @private
|
|
380
|
-
*/
|
|
381
|
-
private async initializePort80Handler(): Promise<void> {
|
|
382
|
-
if (!this.options.acme.enabled) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Create certificate manager
|
|
387
|
-
this.port80Handler = new Port80Handler({
|
|
388
|
-
port: this.options.acme.port,
|
|
389
|
-
contactEmail: this.options.acme.contactEmail,
|
|
390
|
-
useProduction: this.options.acme.useProduction,
|
|
391
|
-
renewThresholdDays: this.options.acme.renewThresholdDays,
|
|
392
|
-
httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
|
|
393
|
-
renewCheckIntervalHours: 24 // Check daily for renewals
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
// Register event handlers
|
|
397
|
-
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
|
|
398
|
-
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
|
|
399
|
-
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
|
|
400
|
-
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
|
|
401
|
-
this.log('info', `Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
// Start the handler
|
|
405
|
-
try {
|
|
406
|
-
await this.port80Handler.start();
|
|
407
|
-
this.log('info', `Port80Handler started on port ${this.options.acme.port}`);
|
|
408
|
-
|
|
409
|
-
// Add domains from proxy configs
|
|
410
|
-
this.registerDomainsWithPort80Handler();
|
|
411
|
-
} catch (error) {
|
|
412
|
-
this.log('error', `Failed to start Port80Handler: ${error}`);
|
|
413
|
-
this.port80Handler = null;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Registers domains from proxy configs with the Port80Handler
|
|
419
|
-
* @private
|
|
420
|
-
*/
|
|
421
|
-
private registerDomainsWithPort80Handler(): void {
|
|
422
|
-
if (!this.port80Handler) return;
|
|
423
|
-
|
|
424
|
-
// Get all hostnames from proxy configs
|
|
425
|
-
this.proxyConfigs.forEach(config => {
|
|
426
|
-
const hostname = config.hostName;
|
|
427
|
-
|
|
428
|
-
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
|
|
429
|
-
if (hostname.includes('*')) {
|
|
430
|
-
this.log('info', `Skipping wildcard domain for ACME: ${hostname}`);
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Skip domains already with certificates if configured to do so
|
|
435
|
-
if (this.options.acme.skipConfiguredCerts) {
|
|
436
|
-
const cachedCert = this.certificateCache.get(hostname);
|
|
437
|
-
if (cachedCert) {
|
|
438
|
-
this.log('info', `Skipping domain with existing certificate: ${hostname}`);
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Check for existing certificate in the store
|
|
444
|
-
const certPath = path.join(this.certificateStoreDir, `${hostname}.cert.pem`);
|
|
445
|
-
const keyPath = path.join(this.certificateStoreDir, `${hostname}.key.pem`);
|
|
446
|
-
|
|
447
|
-
try {
|
|
448
|
-
if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
|
|
449
|
-
// Load existing certificate and key
|
|
450
|
-
const cert = fs.readFileSync(certPath, 'utf8');
|
|
451
|
-
const key = fs.readFileSync(keyPath, 'utf8');
|
|
452
|
-
|
|
453
|
-
// Extract expiry date from certificate if possible
|
|
454
|
-
let expiryDate: Date | undefined;
|
|
455
|
-
try {
|
|
456
|
-
const matches = cert.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
|
|
457
|
-
if (matches && matches[1]) {
|
|
458
|
-
expiryDate = new Date(matches[1]);
|
|
459
|
-
}
|
|
460
|
-
} catch (error) {
|
|
461
|
-
this.log('warn', `Failed to extract expiry date from certificate for ${hostname}`);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Update the certificate in the handler
|
|
465
|
-
this.port80Handler.setCertificate(hostname, cert, key, expiryDate);
|
|
466
|
-
|
|
467
|
-
// Also update our own certificate cache
|
|
468
|
-
this.updateCertificateCache(hostname, cert, key, expiryDate);
|
|
469
|
-
|
|
470
|
-
this.log('info', `Loaded existing certificate for ${hostname}`);
|
|
471
|
-
} else {
|
|
472
|
-
// Register the domain for certificate issuance with new domain options format
|
|
473
|
-
const domainOptions: IDomainOptions = {
|
|
474
|
-
domainName: hostname,
|
|
475
|
-
sslRedirect: true,
|
|
476
|
-
acmeMaintenance: true
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
this.port80Handler.addDomain(domainOptions);
|
|
480
|
-
this.log('info', `Registered domain for ACME certificate issuance: ${hostname}`);
|
|
481
|
-
}
|
|
482
|
-
} catch (error) {
|
|
483
|
-
this.log('error', `Error registering domain ${hostname} with Port80Handler: ${error}`);
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Handles newly issued or renewed certificates from Port80Handler
|
|
490
|
-
* @private
|
|
491
|
-
*/
|
|
492
|
-
private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
|
|
493
|
-
const { domain, certificate, privateKey, expiryDate } = data;
|
|
494
|
-
|
|
495
|
-
this.log('info', `Certificate ${this.certificateCache.has(domain) ? 'renewed' : 'issued'} for ${domain}, valid until ${expiryDate.toISOString()}`);
|
|
496
|
-
|
|
497
|
-
// Update certificate in HTTPS server
|
|
498
|
-
this.updateCertificateCache(domain, certificate, privateKey, expiryDate);
|
|
499
|
-
|
|
500
|
-
// Save the certificate to the filesystem
|
|
501
|
-
this.saveCertificateToStore(domain, certificate, privateKey);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Handles certificate issuance failures
|
|
506
|
-
* @private
|
|
507
|
-
*/
|
|
508
|
-
private handleCertificateFailed(data: { domain: string; error: string }): void {
|
|
509
|
-
this.log('error', `Certificate issuance failed for ${data.domain}: ${data.error}`);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* Saves certificate and private key to the filesystem
|
|
514
|
-
* @private
|
|
515
|
-
*/
|
|
516
|
-
private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
|
|
517
|
-
try {
|
|
518
|
-
const certPath = path.join(this.certificateStoreDir, `${domain}.cert.pem`);
|
|
519
|
-
const keyPath = path.join(this.certificateStoreDir, `${domain}.key.pem`);
|
|
520
|
-
|
|
521
|
-
fs.writeFileSync(certPath, certificate);
|
|
522
|
-
fs.writeFileSync(keyPath, privateKey);
|
|
523
|
-
|
|
524
|
-
// Ensure private key has restricted permissions
|
|
525
|
-
try {
|
|
526
|
-
fs.chmodSync(keyPath, 0o600);
|
|
527
|
-
} catch (error) {
|
|
528
|
-
this.log('warn', `Failed to set permissions on private key for ${domain}: ${error}`);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
this.log('info', `Saved certificate for ${domain} to ${certPath}`);
|
|
532
|
-
} catch (error) {
|
|
533
|
-
this.log('error', `Failed to save certificate for ${domain}: ${error}`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Handles SNI (Server Name Indication) for TLS connections
|
|
539
|
-
* Used by the HTTPS server to select the correct certificate for each domain
|
|
540
|
-
* @private
|
|
541
|
-
*/
|
|
542
|
-
private handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
|
|
543
|
-
this.log('debug', `SNI request for domain: ${domain}`);
|
|
544
|
-
|
|
545
|
-
// Check if we have a certificate for this domain
|
|
546
|
-
const certs = this.certificateCache.get(domain);
|
|
547
|
-
|
|
548
|
-
if (certs) {
|
|
549
|
-
try {
|
|
550
|
-
// Create TLS context with the cached certificate
|
|
551
|
-
const context = plugins.tls.createSecureContext({
|
|
552
|
-
key: certs.key,
|
|
553
|
-
cert: certs.cert
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
this.log('debug', `Using cached certificate for ${domain}`);
|
|
557
|
-
cb(null, context);
|
|
558
|
-
return;
|
|
559
|
-
} catch (err) {
|
|
560
|
-
this.log('error', `Error creating secure context for ${domain}:`, err);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Check if we should trigger certificate issuance
|
|
565
|
-
if (this.options.acme?.enabled && this.port80Handler && !domain.includes('*')) {
|
|
566
|
-
// Check if this domain is already registered
|
|
567
|
-
const certData = this.port80Handler.getCertificate(domain);
|
|
568
|
-
|
|
569
|
-
if (!certData) {
|
|
570
|
-
this.log('info', `No certificate found for ${domain}, registering for issuance`);
|
|
571
|
-
|
|
572
|
-
// Register with new domain options format
|
|
573
|
-
const domainOptions: IDomainOptions = {
|
|
574
|
-
domainName: domain,
|
|
575
|
-
sslRedirect: true,
|
|
576
|
-
acmeMaintenance: true
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
this.port80Handler.addDomain(domainOptions);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Fall back to default certificate
|
|
584
|
-
try {
|
|
585
|
-
const context = plugins.tls.createSecureContext({
|
|
586
|
-
key: this.defaultCertificates.key,
|
|
587
|
-
cert: this.defaultCertificates.cert
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
this.log('debug', `Using default certificate for ${domain}`);
|
|
591
|
-
cb(null, context);
|
|
592
|
-
} catch (err) {
|
|
593
|
-
this.log('error', `Error creating default secure context:`, err);
|
|
594
|
-
cb(new Error('Cannot create secure context'), null);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Starts the proxy server
|
|
600
|
-
*/
|
|
601
|
-
public async start(): Promise<void> {
|
|
602
|
-
this.startTime = Date.now();
|
|
603
|
-
|
|
604
|
-
// Initialize Port80Handler if enabled
|
|
605
|
-
if (this.options.acme.enabled) {
|
|
606
|
-
await this.initializePort80Handler();
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// Create the HTTPS server
|
|
610
|
-
this.httpsServer = plugins.https.createServer(
|
|
611
|
-
{
|
|
612
|
-
key: this.defaultCertificates.key,
|
|
613
|
-
cert: this.defaultCertificates.cert,
|
|
614
|
-
SNICallback: (domain, cb) => this.handleSNI(domain, cb)
|
|
615
|
-
},
|
|
616
|
-
(req, res) => this.handleRequest(req, res)
|
|
617
|
-
);
|
|
618
|
-
|
|
619
|
-
// Configure server timeouts
|
|
620
|
-
this.httpsServer.keepAliveTimeout = this.options.keepAliveTimeout;
|
|
621
|
-
this.httpsServer.headersTimeout = this.options.headersTimeout;
|
|
622
|
-
|
|
623
|
-
// Setup connection tracking
|
|
624
|
-
this.setupConnectionTracking();
|
|
625
|
-
|
|
626
|
-
// Setup WebSocket support
|
|
627
|
-
this.setupWebsocketSupport();
|
|
628
|
-
|
|
629
|
-
// Start metrics collection
|
|
630
|
-
this.setupMetricsCollection();
|
|
631
|
-
|
|
632
|
-
// Setup connection pool cleanup interval
|
|
633
|
-
this.setupConnectionPoolCleanup();
|
|
634
|
-
|
|
635
|
-
// Start the server
|
|
636
|
-
return new Promise((resolve) => {
|
|
637
|
-
this.httpsServer.listen(this.options.port, () => {
|
|
638
|
-
this.log('info', `NetworkProxy started on port ${this.options.port}`);
|
|
639
|
-
resolve();
|
|
640
|
-
});
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Sets up tracking of TCP connections
|
|
646
|
-
*/
|
|
647
|
-
private setupConnectionTracking(): void {
|
|
648
|
-
this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
|
|
649
|
-
// Check if max connections reached
|
|
650
|
-
if (this.socketMap.getArray().length >= this.options.maxConnections) {
|
|
651
|
-
this.log('warn', `Max connections (${this.options.maxConnections}) reached, rejecting new connection`);
|
|
652
|
-
connection.destroy();
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Add connection to tracking
|
|
657
|
-
this.socketMap.add(connection);
|
|
658
|
-
this.connectedClients = this.socketMap.getArray().length;
|
|
659
|
-
|
|
660
|
-
// Check for connection from PortProxy by inspecting the source port
|
|
661
|
-
// This is a heuristic - in a production environment you might use a more robust method
|
|
662
|
-
const localPort = connection.localPort;
|
|
663
|
-
const remotePort = connection.remotePort;
|
|
664
|
-
|
|
665
|
-
// If this connection is from a PortProxy (usually indicated by it coming from localhost)
|
|
666
|
-
if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
|
|
667
|
-
this.portProxyConnections++;
|
|
668
|
-
this.log('debug', `New connection from PortProxy (local: ${localPort}, remote: ${remotePort})`);
|
|
669
|
-
} else {
|
|
670
|
-
this.log('debug', `New direct connection (local: ${localPort}, remote: ${remotePort})`);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Setup connection cleanup handlers
|
|
674
|
-
const cleanupConnection = () => {
|
|
675
|
-
if (this.socketMap.checkForObject(connection)) {
|
|
676
|
-
this.socketMap.remove(connection);
|
|
677
|
-
this.connectedClients = this.socketMap.getArray().length;
|
|
678
|
-
|
|
679
|
-
// If this was a PortProxy connection, decrement the counter
|
|
680
|
-
if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
|
|
681
|
-
this.portProxyConnections--;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
this.log('debug', `Connection closed. ${this.connectedClients} connections remaining`);
|
|
685
|
-
}
|
|
686
|
-
};
|
|
687
|
-
|
|
688
|
-
connection.on('close', cleanupConnection);
|
|
689
|
-
connection.on('error', (err) => {
|
|
690
|
-
this.log('debug', 'Connection error', err);
|
|
691
|
-
cleanupConnection();
|
|
692
|
-
});
|
|
693
|
-
connection.on('end', cleanupConnection);
|
|
694
|
-
connection.on('timeout', () => {
|
|
695
|
-
this.log('debug', 'Connection timeout');
|
|
696
|
-
cleanupConnection();
|
|
697
|
-
});
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
// Track TLS handshake completions
|
|
701
|
-
this.httpsServer.on('secureConnection', (tlsSocket) => {
|
|
702
|
-
this.tlsTerminatedConnections++;
|
|
703
|
-
this.log('debug', 'TLS handshake completed, connection secured');
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
/**
|
|
708
|
-
* Sets up WebSocket support
|
|
709
|
-
*/
|
|
710
|
-
private setupWebsocketSupport(): void {
|
|
711
|
-
// Create WebSocket server
|
|
712
|
-
this.wsServer = new plugins.ws.WebSocketServer({
|
|
713
|
-
server: this.httpsServer,
|
|
714
|
-
// Add WebSocket specific timeout
|
|
715
|
-
clientTracking: true
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
// Handle WebSocket connections
|
|
719
|
-
this.wsServer.on('connection', (wsIncoming: IWebSocketWithHeartbeat, reqArg: plugins.http.IncomingMessage) => {
|
|
720
|
-
this.handleWebSocketConnection(wsIncoming, reqArg);
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
// Set up the heartbeat interval (check every 30 seconds, terminate after 2 minutes of inactivity)
|
|
724
|
-
this.heartbeatInterval = setInterval(() => {
|
|
725
|
-
if (this.wsServer.clients.size === 0) {
|
|
726
|
-
return; // Skip if no active connections
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
this.log('debug', `WebSocket heartbeat check for ${this.wsServer.clients.size} clients`);
|
|
730
|
-
this.wsServer.clients.forEach((ws: plugins.wsDefault) => {
|
|
731
|
-
const wsWithHeartbeat = ws as IWebSocketWithHeartbeat;
|
|
732
|
-
|
|
733
|
-
if (wsWithHeartbeat.isAlive === false) {
|
|
734
|
-
this.log('debug', 'Terminating inactive WebSocket connection');
|
|
735
|
-
return wsWithHeartbeat.terminate();
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
wsWithHeartbeat.isAlive = false;
|
|
739
|
-
wsWithHeartbeat.ping();
|
|
740
|
-
});
|
|
741
|
-
}, 30000);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/**
|
|
745
|
-
* Sets up metrics collection
|
|
746
|
-
*/
|
|
747
|
-
private setupMetricsCollection(): void {
|
|
748
|
-
this.metricsInterval = setInterval(() => {
|
|
749
|
-
const uptime = Math.floor((Date.now() - this.startTime) / 1000);
|
|
750
|
-
const metrics = {
|
|
751
|
-
uptime,
|
|
752
|
-
activeConnections: this.connectedClients,
|
|
753
|
-
totalRequests: this.requestsServed,
|
|
754
|
-
failedRequests: this.failedRequests,
|
|
755
|
-
portProxyConnections: this.portProxyConnections,
|
|
756
|
-
tlsTerminatedConnections: this.tlsTerminatedConnections,
|
|
757
|
-
activeWebSockets: this.wsServer?.clients.size || 0,
|
|
758
|
-
memoryUsage: process.memoryUsage(),
|
|
759
|
-
activeContexts: Array.from(this.activeContexts),
|
|
760
|
-
connectionPool: Object.fromEntries(
|
|
761
|
-
Array.from(this.connectionPool.entries()).map(([host, connections]) => [
|
|
762
|
-
host,
|
|
763
|
-
{
|
|
764
|
-
total: connections.length,
|
|
765
|
-
idle: connections.filter(c => c.isIdle).length
|
|
766
|
-
}
|
|
767
|
-
])
|
|
768
|
-
)
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
this.log('debug', 'Proxy metrics', metrics);
|
|
772
|
-
}, 60000); // Log metrics every minute
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* Sets up connection pool cleanup
|
|
777
|
-
*/
|
|
778
|
-
private setupConnectionPoolCleanup(): void {
|
|
779
|
-
// Clean up idle connections every minute
|
|
780
|
-
this.connectionPoolCleanupInterval = setInterval(() => {
|
|
781
|
-
this.cleanupConnectionPool();
|
|
782
|
-
}, 60000); // 1 minute
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
/**
|
|
786
|
-
* Handles an incoming WebSocket connection
|
|
787
|
-
*/
|
|
788
|
-
private handleWebSocketConnection(wsIncoming: IWebSocketWithHeartbeat, reqArg: plugins.http.IncomingMessage): void {
|
|
789
|
-
const wsPath = reqArg.url;
|
|
790
|
-
const wsHost = reqArg.headers.host;
|
|
791
|
-
|
|
792
|
-
this.log('info', `WebSocket connection for ${wsHost}${wsPath}`);
|
|
793
|
-
|
|
794
|
-
// Setup heartbeat tracking
|
|
795
|
-
wsIncoming.isAlive = true;
|
|
796
|
-
wsIncoming.lastPong = Date.now();
|
|
797
|
-
wsIncoming.on('pong', () => {
|
|
798
|
-
wsIncoming.isAlive = true;
|
|
799
|
-
wsIncoming.lastPong = Date.now();
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
// Get the destination configuration
|
|
803
|
-
const wsDestinationConfig = this.router.routeReq(reqArg);
|
|
804
|
-
if (!wsDestinationConfig) {
|
|
805
|
-
this.log('warn', `No route found for WebSocket ${wsHost}${wsPath}`);
|
|
806
|
-
wsIncoming.terminate();
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Check authentication if required
|
|
811
|
-
if (wsDestinationConfig.authentication) {
|
|
812
|
-
try {
|
|
813
|
-
if (!this.authenticateRequest(reqArg, wsDestinationConfig)) {
|
|
814
|
-
this.log('warn', `WebSocket authentication failed for ${wsHost}${wsPath}`);
|
|
815
|
-
wsIncoming.terminate();
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
} catch (error) {
|
|
819
|
-
this.log('error', 'WebSocket authentication error', error);
|
|
820
|
-
wsIncoming.terminate();
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Setup outgoing WebSocket connection
|
|
826
|
-
let wsOutgoing: plugins.wsDefault;
|
|
827
|
-
const outGoingDeferred = plugins.smartpromise.defer();
|
|
828
|
-
|
|
829
|
-
try {
|
|
830
|
-
// Select destination IP and port for WebSocket
|
|
831
|
-
const wsDestinationIp = this.selectDestinationIp(wsDestinationConfig);
|
|
832
|
-
const wsDestinationPort = this.selectDestinationPort(wsDestinationConfig);
|
|
833
|
-
const wsTarget = `ws://${wsDestinationIp}:${wsDestinationPort}${reqArg.url}`;
|
|
834
|
-
this.log('debug', `Proxying WebSocket to ${wsTarget}`);
|
|
835
|
-
|
|
836
|
-
wsOutgoing = new plugins.wsDefault(wsTarget);
|
|
837
|
-
|
|
838
|
-
wsOutgoing.on('open', () => {
|
|
839
|
-
this.log('debug', 'Outgoing WebSocket connection established');
|
|
840
|
-
outGoingDeferred.resolve();
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
wsOutgoing.on('error', (error) => {
|
|
844
|
-
this.log('error', 'Outgoing WebSocket error', error);
|
|
845
|
-
outGoingDeferred.reject(error);
|
|
846
|
-
if (wsIncoming.readyState === wsIncoming.OPEN) {
|
|
847
|
-
wsIncoming.terminate();
|
|
848
|
-
}
|
|
849
|
-
});
|
|
850
|
-
} catch (err) {
|
|
851
|
-
this.log('error', 'Failed to create outgoing WebSocket connection', err);
|
|
852
|
-
wsIncoming.terminate();
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// Handle message forwarding from client to backend
|
|
857
|
-
wsIncoming.on('message', async (message, isBinary) => {
|
|
858
|
-
try {
|
|
859
|
-
// Wait for outgoing connection to be ready
|
|
860
|
-
await outGoingDeferred.promise;
|
|
861
|
-
|
|
862
|
-
// Only forward if both connections are still open
|
|
863
|
-
if (wsOutgoing.readyState === wsOutgoing.OPEN) {
|
|
864
|
-
wsOutgoing.send(message, { binary: isBinary });
|
|
865
|
-
}
|
|
866
|
-
} catch (error) {
|
|
867
|
-
this.log('error', 'Error forwarding WebSocket message to backend', error);
|
|
868
|
-
}
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
// Handle message forwarding from backend to client
|
|
872
|
-
wsOutgoing.on('message', (message, isBinary) => {
|
|
873
|
-
try {
|
|
874
|
-
// Only forward if the incoming connection is still open
|
|
875
|
-
if (wsIncoming.readyState === wsIncoming.OPEN) {
|
|
876
|
-
wsIncoming.send(message, { binary: isBinary });
|
|
877
|
-
}
|
|
878
|
-
} catch (error) {
|
|
879
|
-
this.log('error', 'Error forwarding WebSocket message to client', error);
|
|
880
|
-
}
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
// Clean up connections when either side closes
|
|
884
|
-
wsIncoming.on('close', (code, reason) => {
|
|
885
|
-
this.log('debug', `Incoming WebSocket closed: ${code} - ${reason}`);
|
|
886
|
-
if (wsOutgoing && wsOutgoing.readyState !== wsOutgoing.CLOSED) {
|
|
887
|
-
try {
|
|
888
|
-
// Validate close code (must be 1000-4999) or use 1000 as default
|
|
889
|
-
const validCode = (code >= 1000 && code <= 4999) ? code : 1000;
|
|
890
|
-
wsOutgoing.close(validCode, reason.toString() || '');
|
|
891
|
-
} catch (error) {
|
|
892
|
-
this.log('error', 'Error closing outgoing WebSocket', error);
|
|
893
|
-
wsOutgoing.terminate();
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
wsOutgoing.on('close', (code, reason) => {
|
|
899
|
-
this.log('debug', `Outgoing WebSocket closed: ${code} - ${reason}`);
|
|
900
|
-
if (wsIncoming && wsIncoming.readyState !== wsIncoming.CLOSED) {
|
|
901
|
-
try {
|
|
902
|
-
// Validate close code (must be 1000-4999) or use 1000 as default
|
|
903
|
-
const validCode = (code >= 1000 && code <= 4999) ? code : 1000;
|
|
904
|
-
wsIncoming.close(validCode, reason.toString() || '');
|
|
905
|
-
} catch (error) {
|
|
906
|
-
this.log('error', 'Error closing incoming WebSocket', error);
|
|
907
|
-
wsIncoming.terminate();
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
/**
|
|
914
|
-
* Handles an HTTP/HTTPS request
|
|
915
|
-
*/
|
|
916
|
-
private async handleRequest(
|
|
917
|
-
originRequest: plugins.http.IncomingMessage,
|
|
918
|
-
originResponse: plugins.http.ServerResponse
|
|
919
|
-
): Promise<void> {
|
|
920
|
-
this.requestsServed++;
|
|
921
|
-
const startTime = Date.now();
|
|
922
|
-
const reqId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
923
|
-
|
|
924
|
-
try {
|
|
925
|
-
const reqPath = plugins.url.parse(originRequest.url).path;
|
|
926
|
-
this.log('info', `[${reqId}] ${originRequest.method} ${originRequest.headers.host}${reqPath}`);
|
|
927
|
-
|
|
928
|
-
// Handle preflight OPTIONS requests for CORS
|
|
929
|
-
if (originRequest.method === 'OPTIONS' && this.options.cors) {
|
|
930
|
-
this.handleCorsRequest(originRequest, originResponse);
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// Get destination configuration
|
|
935
|
-
const destinationConfig = this.router.routeReq(originRequest);
|
|
936
|
-
if (!destinationConfig) {
|
|
937
|
-
this.log('warn', `[${reqId}] No route found for ${originRequest.headers.host}`);
|
|
938
|
-
this.sendErrorResponse(originResponse, 404, 'Not Found: No matching route');
|
|
939
|
-
this.failedRequests++;
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// Handle authentication if configured
|
|
944
|
-
if (destinationConfig.authentication) {
|
|
945
|
-
try {
|
|
946
|
-
if (!this.authenticateRequest(originRequest, destinationConfig)) {
|
|
947
|
-
this.sendErrorResponse(originResponse, 401, 'Unauthorized', {
|
|
948
|
-
'WWW-Authenticate': 'Basic realm="Access to the proxy site", charset="UTF-8"'
|
|
949
|
-
});
|
|
950
|
-
this.failedRequests++;
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
} catch (error) {
|
|
954
|
-
this.log('error', `[${reqId}] Authentication error`, error);
|
|
955
|
-
this.sendErrorResponse(originResponse, 500, 'Internal Server Error: Authentication failed');
|
|
956
|
-
this.failedRequests++;
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
// Determine if we should use connection pooling
|
|
962
|
-
const useConnectionPool = this.options.portProxyIntegration &&
|
|
963
|
-
originRequest.socket.remoteAddress?.includes('127.0.0.1');
|
|
964
|
-
|
|
965
|
-
// Select destination IP and port from the arrays
|
|
966
|
-
const destinationIp = this.selectDestinationIp(destinationConfig);
|
|
967
|
-
const destinationPort = this.selectDestinationPort(destinationConfig);
|
|
968
|
-
|
|
969
|
-
// Construct destination URL
|
|
970
|
-
const destinationUrl = `http://${destinationIp}:${destinationPort}${originRequest.url}`;
|
|
971
|
-
|
|
972
|
-
if (useConnectionPool) {
|
|
973
|
-
this.log('debug', `[${reqId}] Proxying to ${destinationUrl} (using connection pool)`);
|
|
974
|
-
await this.forwardRequestUsingConnectionPool(
|
|
975
|
-
reqId,
|
|
976
|
-
originRequest,
|
|
977
|
-
originResponse,
|
|
978
|
-
destinationIp,
|
|
979
|
-
destinationPort,
|
|
980
|
-
originRequest.url
|
|
981
|
-
);
|
|
982
|
-
} else {
|
|
983
|
-
this.log('debug', `[${reqId}] Proxying to ${destinationUrl}`);
|
|
984
|
-
await this.forwardRequest(reqId, originRequest, originResponse, destinationUrl);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const processingTime = Date.now() - startTime;
|
|
988
|
-
this.log('debug', `[${reqId}] Request completed in ${processingTime}ms`);
|
|
989
|
-
} catch (error) {
|
|
990
|
-
this.log('error', `[${reqId}] Unhandled error in request handler`, error);
|
|
991
|
-
try {
|
|
992
|
-
this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Server error');
|
|
993
|
-
} catch (responseError) {
|
|
994
|
-
this.log('error', `[${reqId}] Failed to send error response`, responseError);
|
|
995
|
-
}
|
|
996
|
-
this.failedRequests++;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
/**
|
|
1001
|
-
* Handles a CORS preflight request
|
|
1002
|
-
*/
|
|
1003
|
-
private handleCorsRequest(
|
|
1004
|
-
req: plugins.http.IncomingMessage,
|
|
1005
|
-
res: plugins.http.ServerResponse
|
|
1006
|
-
): void {
|
|
1007
|
-
const cors = this.options.cors;
|
|
1008
|
-
|
|
1009
|
-
// Set CORS headers
|
|
1010
|
-
res.setHeader('Access-Control-Allow-Origin', cors.allowOrigin);
|
|
1011
|
-
res.setHeader('Access-Control-Allow-Methods', cors.allowMethods);
|
|
1012
|
-
res.setHeader('Access-Control-Allow-Headers', cors.allowHeaders);
|
|
1013
|
-
res.setHeader('Access-Control-Max-Age', String(cors.maxAge));
|
|
1014
|
-
|
|
1015
|
-
// Handle preflight request
|
|
1016
|
-
res.statusCode = 204;
|
|
1017
|
-
res.end();
|
|
1018
|
-
|
|
1019
|
-
// Count this as a request served
|
|
1020
|
-
this.requestsServed++;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/**
|
|
1024
|
-
* Authenticates a request against the destination config
|
|
1025
|
-
*/
|
|
1026
|
-
private authenticateRequest(
|
|
1027
|
-
req: plugins.http.IncomingMessage,
|
|
1028
|
-
config: plugins.tsclass.network.IReverseProxyConfig
|
|
1029
|
-
): boolean {
|
|
1030
|
-
const authInfo = config.authentication;
|
|
1031
|
-
if (!authInfo) {
|
|
1032
|
-
return true; // No authentication required
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
switch (authInfo.type) {
|
|
1036
|
-
case 'Basic': {
|
|
1037
|
-
const authHeader = req.headers.authorization;
|
|
1038
|
-
if (!authHeader || !authHeader.includes('Basic ')) {
|
|
1039
|
-
return false;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const authStringBase64 = authHeader.replace('Basic ', '');
|
|
1043
|
-
const authString: string = plugins.smartstring.base64.decode(authStringBase64);
|
|
1044
|
-
const [user, pass] = authString.split(':');
|
|
1045
|
-
|
|
1046
|
-
// Use constant-time comparison to prevent timing attacks
|
|
1047
|
-
const userMatch = user === authInfo.user;
|
|
1048
|
-
const passMatch = pass === authInfo.pass;
|
|
1049
|
-
|
|
1050
|
-
return userMatch && passMatch;
|
|
1051
|
-
}
|
|
1052
|
-
default:
|
|
1053
|
-
throw new Error(`Unsupported authentication method: ${authInfo.type}`);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
/**
|
|
1058
|
-
* Forwards a request to the destination using connection pool
|
|
1059
|
-
* for optimized connection reuse from PortProxy
|
|
1060
|
-
*/
|
|
1061
|
-
private async forwardRequestUsingConnectionPool(
|
|
1062
|
-
reqId: string,
|
|
1063
|
-
originRequest: plugins.http.IncomingMessage,
|
|
1064
|
-
originResponse: plugins.http.ServerResponse,
|
|
1065
|
-
host: string,
|
|
1066
|
-
port: number,
|
|
1067
|
-
path: string
|
|
1068
|
-
): Promise<void> {
|
|
1069
|
-
try {
|
|
1070
|
-
// Try to get a connection from the pool
|
|
1071
|
-
const socket = await this.getConnectionFromPool(host, port);
|
|
1072
|
-
|
|
1073
|
-
// Create an HTTP client request using the pooled socket
|
|
1074
|
-
const reqOptions = {
|
|
1075
|
-
createConnection: () => socket,
|
|
1076
|
-
host,
|
|
1077
|
-
port,
|
|
1078
|
-
path,
|
|
1079
|
-
method: originRequest.method,
|
|
1080
|
-
headers: this.prepareForwardHeaders(originRequest),
|
|
1081
|
-
timeout: 30000 // 30 second timeout
|
|
1082
|
-
};
|
|
1083
|
-
|
|
1084
|
-
const proxyReq = plugins.http.request(reqOptions);
|
|
1085
|
-
|
|
1086
|
-
// Handle timeouts
|
|
1087
|
-
proxyReq.on('timeout', () => {
|
|
1088
|
-
this.log('warn', `[${reqId}] Request to ${host}:${port}${path} timed out`);
|
|
1089
|
-
proxyReq.destroy();
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
// Handle errors
|
|
1093
|
-
proxyReq.on('error', (err) => {
|
|
1094
|
-
this.log('error', `[${reqId}] Error in proxy request to ${host}:${port}${path}`, err);
|
|
1095
|
-
|
|
1096
|
-
// Check if the client response is still writable
|
|
1097
|
-
if (!originResponse.writableEnded) {
|
|
1098
|
-
this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Error communicating with upstream server');
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// Don't return the socket to the pool on error
|
|
1102
|
-
try {
|
|
1103
|
-
if (!socket.destroyed) {
|
|
1104
|
-
socket.destroy();
|
|
1105
|
-
}
|
|
1106
|
-
} catch (socketErr) {
|
|
1107
|
-
this.log('error', `[${reqId}] Error destroying socket after request error`, socketErr);
|
|
1108
|
-
}
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
// Forward request body
|
|
1112
|
-
originRequest.pipe(proxyReq);
|
|
1113
|
-
|
|
1114
|
-
// Handle response
|
|
1115
|
-
proxyReq.on('response', (proxyRes) => {
|
|
1116
|
-
// Copy status and headers
|
|
1117
|
-
originResponse.statusCode = proxyRes.statusCode;
|
|
1118
|
-
|
|
1119
|
-
for (const [name, value] of Object.entries(proxyRes.headers)) {
|
|
1120
|
-
if (value !== undefined) {
|
|
1121
|
-
originResponse.setHeader(name, value);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// Forward the response body
|
|
1126
|
-
proxyRes.pipe(originResponse);
|
|
1127
|
-
|
|
1128
|
-
// Return connection to pool when the response completes
|
|
1129
|
-
proxyRes.on('end', () => {
|
|
1130
|
-
if (!socket.destroyed) {
|
|
1131
|
-
this.returnConnectionToPool(socket, host, port);
|
|
1132
|
-
}
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
proxyRes.on('error', (err) => {
|
|
1136
|
-
this.log('error', `[${reqId}] Error in proxy response from ${host}:${port}${path}`, err);
|
|
1137
|
-
|
|
1138
|
-
// Don't return the socket to the pool on error
|
|
1139
|
-
try {
|
|
1140
|
-
if (!socket.destroyed) {
|
|
1141
|
-
socket.destroy();
|
|
1142
|
-
}
|
|
1143
|
-
} catch (socketErr) {
|
|
1144
|
-
this.log('error', `[${reqId}] Error destroying socket after response error`, socketErr);
|
|
1145
|
-
}
|
|
1146
|
-
});
|
|
1147
|
-
});
|
|
1148
|
-
} catch (error) {
|
|
1149
|
-
this.log('error', `[${reqId}] Error setting up pooled connection to ${host}:${port}`, error);
|
|
1150
|
-
this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Unable to reach upstream server');
|
|
1151
|
-
throw error;
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
/**
|
|
1156
|
-
* Forwards a request to the destination (standard method)
|
|
1157
|
-
*/
|
|
1158
|
-
private async forwardRequest(
|
|
1159
|
-
reqId: string,
|
|
1160
|
-
originRequest: plugins.http.IncomingMessage,
|
|
1161
|
-
originResponse: plugins.http.ServerResponse,
|
|
1162
|
-
destinationUrl: string
|
|
1163
|
-
): Promise<void> {
|
|
1164
|
-
try {
|
|
1165
|
-
const proxyRequest = await plugins.smartrequest.request(
|
|
1166
|
-
destinationUrl,
|
|
1167
|
-
{
|
|
1168
|
-
method: originRequest.method,
|
|
1169
|
-
headers: this.prepareForwardHeaders(originRequest),
|
|
1170
|
-
keepAlive: true,
|
|
1171
|
-
timeout: 30000 // 30 second timeout
|
|
1172
|
-
},
|
|
1173
|
-
true, // streaming
|
|
1174
|
-
(proxyRequestStream) => this.setupRequestStreaming(originRequest, proxyRequestStream)
|
|
1175
|
-
);
|
|
1176
|
-
|
|
1177
|
-
// Handle the response
|
|
1178
|
-
this.processProxyResponse(reqId, originResponse, proxyRequest);
|
|
1179
|
-
} catch (error) {
|
|
1180
|
-
this.log('error', `[${reqId}] Error forwarding request`, error);
|
|
1181
|
-
this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Unable to reach upstream server');
|
|
1182
|
-
throw error; // Let the main handler catch this
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
/**
|
|
1187
|
-
* Prepares headers to forward to the backend
|
|
1188
|
-
*/
|
|
1189
|
-
private prepareForwardHeaders(req: plugins.http.IncomingMessage): plugins.http.OutgoingHttpHeaders {
|
|
1190
|
-
const safeHeaders = { ...req.headers };
|
|
1191
|
-
|
|
1192
|
-
// Add forwarding headers
|
|
1193
|
-
safeHeaders['X-Forwarded-Host'] = req.headers.host;
|
|
1194
|
-
safeHeaders['X-Forwarded-Proto'] = 'https';
|
|
1195
|
-
safeHeaders['X-Forwarded-For'] = (req.socket.remoteAddress || '').replace(/^::ffff:/, '');
|
|
1196
|
-
|
|
1197
|
-
// Add proxy-specific headers
|
|
1198
|
-
safeHeaders['X-Proxy-Id'] = `NetworkProxy-${this.options.port}`;
|
|
1199
|
-
|
|
1200
|
-
// If this is coming from PortProxy, add a header to indicate that
|
|
1201
|
-
if (this.options.portProxyIntegration && req.socket.remoteAddress?.includes('127.0.0.1')) {
|
|
1202
|
-
safeHeaders['X-PortProxy-Forwarded'] = 'true';
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// Remove sensitive headers we don't want to forward
|
|
1206
|
-
const sensitiveHeaders = ['connection', 'upgrade', 'http2-settings'];
|
|
1207
|
-
for (const header of sensitiveHeaders) {
|
|
1208
|
-
delete safeHeaders[header];
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
return safeHeaders;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
/**
|
|
1215
|
-
* Sets up request streaming for the proxy
|
|
1216
|
-
*/
|
|
1217
|
-
private setupRequestStreaming(
|
|
1218
|
-
originRequest: plugins.http.IncomingMessage,
|
|
1219
|
-
proxyRequest: plugins.http.ClientRequest
|
|
1220
|
-
): void {
|
|
1221
|
-
// Forward request body data
|
|
1222
|
-
originRequest.on('data', (chunk) => {
|
|
1223
|
-
proxyRequest.write(chunk);
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
// End the request when done
|
|
1227
|
-
originRequest.on('end', () => {
|
|
1228
|
-
proxyRequest.end();
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
|
-
// Handle request errors
|
|
1232
|
-
originRequest.on('error', (error) => {
|
|
1233
|
-
this.log('error', 'Error in client request stream', error);
|
|
1234
|
-
proxyRequest.destroy(error);
|
|
1235
|
-
});
|
|
1236
|
-
|
|
1237
|
-
// Handle client abort/timeout
|
|
1238
|
-
originRequest.on('close', () => {
|
|
1239
|
-
if (!originRequest.complete) {
|
|
1240
|
-
this.log('debug', 'Client closed connection before request completed');
|
|
1241
|
-
proxyRequest.destroy();
|
|
1242
|
-
}
|
|
1243
|
-
});
|
|
1244
|
-
|
|
1245
|
-
originRequest.on('timeout', () => {
|
|
1246
|
-
this.log('debug', 'Client request timeout');
|
|
1247
|
-
proxyRequest.destroy(new Error('Client request timeout'));
|
|
1248
|
-
});
|
|
1249
|
-
|
|
1250
|
-
// Handle proxy request errors
|
|
1251
|
-
proxyRequest.on('error', (error) => {
|
|
1252
|
-
this.log('error', 'Error in outgoing proxy request', error);
|
|
1253
|
-
});
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
/**
|
|
1257
|
-
* Processes a proxy response
|
|
1258
|
-
*/
|
|
1259
|
-
private processProxyResponse(
|
|
1260
|
-
reqId: string,
|
|
1261
|
-
originResponse: plugins.http.ServerResponse,
|
|
1262
|
-
proxyResponse: plugins.http.IncomingMessage
|
|
1263
|
-
): void {
|
|
1264
|
-
this.log('debug', `[${reqId}] Received upstream response: ${proxyResponse.statusCode}`);
|
|
1265
|
-
|
|
1266
|
-
// Set status code
|
|
1267
|
-
originResponse.statusCode = proxyResponse.statusCode;
|
|
1268
|
-
|
|
1269
|
-
// Add default headers
|
|
1270
|
-
for (const [headerName, headerValue] of Object.entries(this.defaultHeaders)) {
|
|
1271
|
-
originResponse.setHeader(headerName, headerValue);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// Add CORS headers if enabled
|
|
1275
|
-
if (this.options.cors) {
|
|
1276
|
-
originResponse.setHeader('Access-Control-Allow-Origin', this.options.cors.allowOrigin);
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
// Copy response headers
|
|
1280
|
-
for (const [headerName, headerValue] of Object.entries(proxyResponse.headers)) {
|
|
1281
|
-
// Skip hop-by-hop headers
|
|
1282
|
-
const hopByHopHeaders = ['connection', 'keep-alive', 'transfer-encoding', 'te',
|
|
1283
|
-
'trailer', 'upgrade', 'proxy-authorization', 'proxy-authenticate'];
|
|
1284
|
-
if (!hopByHopHeaders.includes(headerName.toLowerCase())) {
|
|
1285
|
-
originResponse.setHeader(headerName, headerValue);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// Stream response body
|
|
1290
|
-
proxyResponse.on('data', (chunk) => {
|
|
1291
|
-
const canContinue = originResponse.write(chunk);
|
|
1292
|
-
|
|
1293
|
-
// Apply backpressure if needed
|
|
1294
|
-
if (!canContinue) {
|
|
1295
|
-
proxyResponse.pause();
|
|
1296
|
-
originResponse.once('drain', () => {
|
|
1297
|
-
proxyResponse.resume();
|
|
1298
|
-
});
|
|
1299
|
-
}
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
// End the response when done
|
|
1303
|
-
proxyResponse.on('end', () => {
|
|
1304
|
-
originResponse.end();
|
|
1305
|
-
});
|
|
1306
|
-
|
|
1307
|
-
// Handle response errors
|
|
1308
|
-
proxyResponse.on('error', (error) => {
|
|
1309
|
-
this.log('error', `[${reqId}] Error in proxy response stream`, error);
|
|
1310
|
-
originResponse.destroy(error);
|
|
1311
|
-
});
|
|
1312
|
-
|
|
1313
|
-
originResponse.on('error', (error) => {
|
|
1314
|
-
this.log('error', `[${reqId}] Error in client response stream`, error);
|
|
1315
|
-
proxyResponse.destroy();
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
/**
|
|
1320
|
-
* Sends an error response to the client
|
|
1321
|
-
*/
|
|
1322
|
-
private sendErrorResponse(
|
|
1323
|
-
res: plugins.http.ServerResponse,
|
|
1324
|
-
statusCode: number = 500,
|
|
1325
|
-
message: string = 'Internal Server Error',
|
|
1326
|
-
headers: plugins.http.OutgoingHttpHeaders = {}
|
|
1327
|
-
): void {
|
|
1328
|
-
try {
|
|
1329
|
-
// If headers already sent, just end the response
|
|
1330
|
-
if (res.headersSent) {
|
|
1331
|
-
res.end();
|
|
1332
|
-
return;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
// Add default headers
|
|
1336
|
-
for (const [key, value] of Object.entries(this.defaultHeaders)) {
|
|
1337
|
-
res.setHeader(key, value);
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
// Add provided headers
|
|
1341
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
1342
|
-
res.setHeader(key, value);
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
// Send error response
|
|
1346
|
-
res.writeHead(statusCode, message);
|
|
1347
|
-
|
|
1348
|
-
// Send error body as JSON for API clients
|
|
1349
|
-
if (res.getHeader('Content-Type') === 'application/json') {
|
|
1350
|
-
res.end(JSON.stringify({ error: { status: statusCode, message } }));
|
|
1351
|
-
} else {
|
|
1352
|
-
// Send as plain text
|
|
1353
|
-
res.end(message);
|
|
1354
|
-
}
|
|
1355
|
-
} catch (error) {
|
|
1356
|
-
this.log('error', 'Error sending error response', error);
|
|
1357
|
-
try {
|
|
1358
|
-
res.destroy();
|
|
1359
|
-
} catch (destroyError) {
|
|
1360
|
-
// Last resort - nothing more we can do
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
/**
|
|
1366
|
-
* Selects a destination IP from the array using round-robin
|
|
1367
|
-
* @param config The proxy configuration
|
|
1368
|
-
* @returns A destination IP address
|
|
1369
|
-
*/
|
|
1370
|
-
private selectDestinationIp(config: plugins.tsclass.network.IReverseProxyConfig): string {
|
|
1371
|
-
// For array-based configs
|
|
1372
|
-
if (Array.isArray(config.destinationIps) && config.destinationIps.length > 0) {
|
|
1373
|
-
// Get the current position or initialize it
|
|
1374
|
-
const key = `ip_${config.hostName}`;
|
|
1375
|
-
let position = this.roundRobinPositions.get(key) || 0;
|
|
1376
|
-
|
|
1377
|
-
// Select the IP using round-robin
|
|
1378
|
-
const selectedIp = config.destinationIps[position];
|
|
1379
|
-
|
|
1380
|
-
// Update the position for next time
|
|
1381
|
-
position = (position + 1) % config.destinationIps.length;
|
|
1382
|
-
this.roundRobinPositions.set(key, position);
|
|
1383
|
-
|
|
1384
|
-
return selectedIp;
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
// For backward compatibility with test suites that rely on specific behavior
|
|
1388
|
-
// Check if there's a proxyConfigs entry that matches this hostname
|
|
1389
|
-
const matchingConfig = this.proxyConfigs.find(cfg =>
|
|
1390
|
-
cfg.hostName === config.hostName &&
|
|
1391
|
-
(cfg as any).destinationIp
|
|
1392
|
-
);
|
|
1393
|
-
|
|
1394
|
-
if (matchingConfig) {
|
|
1395
|
-
return (matchingConfig as any).destinationIp;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
// Fallback to localhost
|
|
1399
|
-
return 'localhost';
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
/**
|
|
1403
|
-
* Selects a destination port from the array using round-robin
|
|
1404
|
-
* @param config The proxy configuration
|
|
1405
|
-
* @returns A destination port number
|
|
1406
|
-
*/
|
|
1407
|
-
private selectDestinationPort(config: plugins.tsclass.network.IReverseProxyConfig): number {
|
|
1408
|
-
// For array-based configs
|
|
1409
|
-
if (Array.isArray(config.destinationPorts) && config.destinationPorts.length > 0) {
|
|
1410
|
-
// Get the current position or initialize it
|
|
1411
|
-
const key = `port_${config.hostName}`;
|
|
1412
|
-
let position = this.roundRobinPositions.get(key) || 0;
|
|
1413
|
-
|
|
1414
|
-
// Select the port using round-robin
|
|
1415
|
-
const selectedPort = config.destinationPorts[position];
|
|
1416
|
-
|
|
1417
|
-
// Update the position for next time
|
|
1418
|
-
position = (position + 1) % config.destinationPorts.length;
|
|
1419
|
-
this.roundRobinPositions.set(key, position);
|
|
1420
|
-
|
|
1421
|
-
return selectedPort;
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
// For backward compatibility with test suites that rely on specific behavior
|
|
1425
|
-
// Check if there's a proxyConfigs entry that matches this hostname
|
|
1426
|
-
const matchingConfig = this.proxyConfigs.find(cfg =>
|
|
1427
|
-
cfg.hostName === config.hostName &&
|
|
1428
|
-
(cfg as any).destinationPort
|
|
1429
|
-
);
|
|
1430
|
-
|
|
1431
|
-
if (matchingConfig) {
|
|
1432
|
-
return parseInt((matchingConfig as any).destinationPort, 10);
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// Fallback to port 80
|
|
1436
|
-
return 80;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
/**
|
|
1440
|
-
* Updates proxy configurations
|
|
1441
|
-
*/
|
|
1442
|
-
public async updateProxyConfigs(
|
|
1443
|
-
proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[]
|
|
1444
|
-
): Promise<void> {
|
|
1445
|
-
this.log('info', `Updating proxy configurations (${proxyConfigsArg.length} configs)`);
|
|
1446
|
-
|
|
1447
|
-
// Update internal configs
|
|
1448
|
-
this.proxyConfigs = proxyConfigsArg;
|
|
1449
|
-
this.router.setNewProxyConfigs(proxyConfigsArg);
|
|
1450
|
-
|
|
1451
|
-
// Collect all hostnames for cleanup later
|
|
1452
|
-
const currentHostNames = new Set<string>();
|
|
1453
|
-
|
|
1454
|
-
// Add/update SSL contexts for each host
|
|
1455
|
-
for (const config of proxyConfigsArg) {
|
|
1456
|
-
currentHostNames.add(config.hostName);
|
|
1457
|
-
|
|
1458
|
-
try {
|
|
1459
|
-
// Check if we need to update the cert
|
|
1460
|
-
const currentCert = this.certificateCache.get(config.hostName);
|
|
1461
|
-
const shouldUpdate = !currentCert ||
|
|
1462
|
-
currentCert.key !== config.privateKey ||
|
|
1463
|
-
currentCert.cert !== config.publicKey;
|
|
1464
|
-
|
|
1465
|
-
if (shouldUpdate) {
|
|
1466
|
-
this.log('debug', `Updating SSL context for ${config.hostName}`);
|
|
1467
|
-
|
|
1468
|
-
// Update the HTTPS server context
|
|
1469
|
-
this.httpsServer.addContext(config.hostName, {
|
|
1470
|
-
key: config.privateKey,
|
|
1471
|
-
cert: config.publicKey
|
|
1472
|
-
});
|
|
1473
|
-
|
|
1474
|
-
// Update the cache
|
|
1475
|
-
this.certificateCache.set(config.hostName, {
|
|
1476
|
-
key: config.privateKey,
|
|
1477
|
-
cert: config.publicKey
|
|
1478
|
-
});
|
|
1479
|
-
|
|
1480
|
-
this.activeContexts.add(config.hostName);
|
|
1481
|
-
}
|
|
1482
|
-
} catch (error) {
|
|
1483
|
-
this.log('error', `Failed to add SSL context for ${config.hostName}`, error);
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
// Clean up removed contexts
|
|
1488
|
-
// Note: Node.js doesn't officially support removing contexts
|
|
1489
|
-
// This would require server restart in production
|
|
1490
|
-
for (const hostname of this.activeContexts) {
|
|
1491
|
-
if (!currentHostNames.has(hostname)) {
|
|
1492
|
-
this.log('info', `Hostname ${hostname} removed from configuration`);
|
|
1493
|
-
this.activeContexts.delete(hostname);
|
|
1494
|
-
this.certificateCache.delete(hostname);
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
/**
|
|
1500
|
-
* Converts PortProxy domain configurations to NetworkProxy configs
|
|
1501
|
-
* @param domainConfigs PortProxy domain configs
|
|
1502
|
-
* @param sslKeyPair Default SSL key pair to use if not specified
|
|
1503
|
-
* @returns Array of NetworkProxy configs
|
|
1504
|
-
*/
|
|
1505
|
-
public convertPortProxyConfigs(
|
|
1506
|
-
domainConfigs: Array<{
|
|
1507
|
-
domains: string[];
|
|
1508
|
-
targetIPs?: string[];
|
|
1509
|
-
allowedIPs?: string[];
|
|
1510
|
-
}>,
|
|
1511
|
-
sslKeyPair?: { key: string; cert: string }
|
|
1512
|
-
): plugins.tsclass.network.IReverseProxyConfig[] {
|
|
1513
|
-
const proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
|
1514
|
-
|
|
1515
|
-
// Use default certificates if not provided
|
|
1516
|
-
const sslKey = sslKeyPair?.key || this.defaultCertificates.key;
|
|
1517
|
-
const sslCert = sslKeyPair?.cert || this.defaultCertificates.cert;
|
|
1518
|
-
|
|
1519
|
-
for (const domainConfig of domainConfigs) {
|
|
1520
|
-
// Each domain in the domains array gets its own config
|
|
1521
|
-
for (const domain of domainConfig.domains) {
|
|
1522
|
-
// Skip non-hostname patterns (like IP addresses)
|
|
1523
|
-
if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
|
|
1524
|
-
continue;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
proxyConfigs.push({
|
|
1528
|
-
hostName: domain,
|
|
1529
|
-
destinationIps: domainConfig.targetIPs || ['localhost'],
|
|
1530
|
-
destinationPorts: [this.options.port], // Use the NetworkProxy port
|
|
1531
|
-
privateKey: sslKey,
|
|
1532
|
-
publicKey: sslCert
|
|
1533
|
-
});
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
this.log('info', `Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
|
|
1538
|
-
return proxyConfigs;
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
/**
|
|
1542
|
-
* Adds default headers to be included in all responses
|
|
1543
|
-
*/
|
|
1544
|
-
public async addDefaultHeaders(headersArg: { [key: string]: string }): Promise<void> {
|
|
1545
|
-
this.log('info', 'Adding default headers', headersArg);
|
|
1546
|
-
this.defaultHeaders = {
|
|
1547
|
-
...this.defaultHeaders,
|
|
1548
|
-
...headersArg
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
/**
|
|
1553
|
-
* Stops the proxy server
|
|
1554
|
-
*/
|
|
1555
|
-
public async stop(): Promise<void> {
|
|
1556
|
-
this.log('info', 'Stopping NetworkProxy server');
|
|
1557
|
-
|
|
1558
|
-
// Clear intervals
|
|
1559
|
-
if (this.heartbeatInterval) {
|
|
1560
|
-
clearInterval(this.heartbeatInterval);
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
if (this.metricsInterval) {
|
|
1564
|
-
clearInterval(this.metricsInterval);
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
if (this.connectionPoolCleanupInterval) {
|
|
1568
|
-
clearInterval(this.connectionPoolCleanupInterval);
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
// Close WebSocket server if exists
|
|
1572
|
-
if (this.wsServer) {
|
|
1573
|
-
for (const client of this.wsServer.clients) {
|
|
1574
|
-
try {
|
|
1575
|
-
client.terminate();
|
|
1576
|
-
} catch (error) {
|
|
1577
|
-
this.log('error', 'Error terminating WebSocket client', error);
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
// Close all tracked sockets
|
|
1583
|
-
for (const socket of this.socketMap.getArray()) {
|
|
1584
|
-
try {
|
|
1585
|
-
socket.destroy();
|
|
1586
|
-
} catch (error) {
|
|
1587
|
-
this.log('error', 'Error destroying socket', error);
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
// Close all connection pool connections
|
|
1592
|
-
for (const [host, connections] of this.connectionPool.entries()) {
|
|
1593
|
-
for (const connection of connections) {
|
|
1594
|
-
try {
|
|
1595
|
-
if (!connection.socket.destroyed) {
|
|
1596
|
-
connection.socket.destroy();
|
|
1597
|
-
}
|
|
1598
|
-
} catch (error) {
|
|
1599
|
-
this.log('error', `Error destroying pooled connection to ${host}`, error);
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
this.connectionPool.clear();
|
|
1604
|
-
|
|
1605
|
-
// Stop Port80Handler if it's running
|
|
1606
|
-
if (this.port80Handler) {
|
|
1607
|
-
try {
|
|
1608
|
-
await this.port80Handler.stop();
|
|
1609
|
-
this.log('info', 'Port80Handler stopped');
|
|
1610
|
-
} catch (error) {
|
|
1611
|
-
this.log('error', 'Error stopping Port80Handler', error);
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
// Close the HTTPS server
|
|
1616
|
-
return new Promise((resolve) => {
|
|
1617
|
-
this.httpsServer.close(() => {
|
|
1618
|
-
this.log('info', 'NetworkProxy server stopped successfully');
|
|
1619
|
-
resolve();
|
|
1620
|
-
});
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
/**
|
|
1625
|
-
* Requests a new certificate for a domain
|
|
1626
|
-
* This can be used to manually trigger certificate issuance
|
|
1627
|
-
* @param domain The domain to request a certificate for
|
|
1628
|
-
* @returns A promise that resolves when the request is submitted (not when the certificate is issued)
|
|
1629
|
-
*/
|
|
1630
|
-
public async requestCertificate(domain: string): Promise<boolean> {
|
|
1631
|
-
if (!this.options.acme.enabled) {
|
|
1632
|
-
this.log('warn', 'ACME certificate management is not enabled');
|
|
1633
|
-
return false;
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
if (!this.port80Handler) {
|
|
1637
|
-
this.log('error', 'Port80Handler is not initialized');
|
|
1638
|
-
return false;
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
|
|
1642
|
-
if (domain.includes('*')) {
|
|
1643
|
-
this.log('error', `Cannot request certificate for wildcard domain: ${domain}`);
|
|
1644
|
-
return false;
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
try {
|
|
1648
|
-
// Use the new domain options format
|
|
1649
|
-
const domainOptions: IDomainOptions = {
|
|
1650
|
-
domainName: domain,
|
|
1651
|
-
sslRedirect: true,
|
|
1652
|
-
acmeMaintenance: true
|
|
1653
|
-
};
|
|
1654
|
-
|
|
1655
|
-
this.port80Handler.addDomain(domainOptions);
|
|
1656
|
-
this.log('info', `Certificate request submitted for domain: ${domain}`);
|
|
1657
|
-
return true;
|
|
1658
|
-
} catch (error) {
|
|
1659
|
-
this.log('error', `Error requesting certificate for domain ${domain}:`, error);
|
|
1660
|
-
return false;
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
/**
|
|
1665
|
-
* Updates the certificate cache for a domain
|
|
1666
|
-
* @param domain The domain name
|
|
1667
|
-
* @param certificate The certificate (PEM format)
|
|
1668
|
-
* @param privateKey The private key (PEM format)
|
|
1669
|
-
* @param expiryDate Optional expiry date
|
|
1670
|
-
*/
|
|
1671
|
-
private updateCertificateCache(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
|
|
1672
|
-
// Update certificate context in HTTPS server if it's running
|
|
1673
|
-
if (this.httpsServer) {
|
|
1674
|
-
try {
|
|
1675
|
-
this.httpsServer.addContext(domain, {
|
|
1676
|
-
key: privateKey,
|
|
1677
|
-
cert: certificate
|
|
1678
|
-
});
|
|
1679
|
-
this.log('debug', `Updated SSL context for domain: ${domain}`);
|
|
1680
|
-
} catch (error) {
|
|
1681
|
-
this.log('error', `Error updating SSL context for domain ${domain}:`, error);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
// Update certificate in cache
|
|
1686
|
-
this.certificateCache.set(domain, {
|
|
1687
|
-
key: privateKey,
|
|
1688
|
-
cert: certificate,
|
|
1689
|
-
expires: expiryDate
|
|
1690
|
-
});
|
|
1691
|
-
|
|
1692
|
-
// Add to active contexts set
|
|
1693
|
-
this.activeContexts.add(domain);
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
/**
|
|
1697
|
-
* Logs a message according to the configured log level
|
|
1698
|
-
*/
|
|
1699
|
-
private log(level: 'error' | 'warn' | 'info' | 'debug', message: string, data?: any): void {
|
|
1700
|
-
const logLevels = {
|
|
1701
|
-
error: 0,
|
|
1702
|
-
warn: 1,
|
|
1703
|
-
info: 2,
|
|
1704
|
-
debug: 3
|
|
1705
|
-
};
|
|
1706
|
-
|
|
1707
|
-
// Skip if log level is higher than configured
|
|
1708
|
-
if (logLevels[level] > logLevels[this.options.logLevel]) {
|
|
1709
|
-
return;
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
const timestamp = new Date().toISOString();
|
|
1713
|
-
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
|
1714
|
-
|
|
1715
|
-
switch (level) {
|
|
1716
|
-
case 'error':
|
|
1717
|
-
console.error(`${prefix} ${message}`, data || '');
|
|
1718
|
-
break;
|
|
1719
|
-
case 'warn':
|
|
1720
|
-
console.warn(`${prefix} ${message}`, data || '');
|
|
1721
|
-
break;
|
|
1722
|
-
case 'info':
|
|
1723
|
-
console.log(`${prefix} ${message}`, data || '');
|
|
1724
|
-
break;
|
|
1725
|
-
case 'debug':
|
|
1726
|
-
console.log(`${prefix} ${message}`, data || '');
|
|
1727
|
-
break;
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
}
|