@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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { type INetworkProxyOptions, type ILogger, createLogger, type IReverseProxyConfig } from './classes.np.types.js';
|
|
3
|
+
import { ConnectionPool } from './classes.np.connectionpool.js';
|
|
4
|
+
import { ProxyRouter } from '../classes.router.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interface for tracking metrics
|
|
8
|
+
*/
|
|
9
|
+
export interface IMetricsTracker {
|
|
10
|
+
incrementRequestsServed(): void;
|
|
11
|
+
incrementFailedRequests(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handles HTTP request processing and proxying
|
|
16
|
+
*/
|
|
17
|
+
export class RequestHandler {
|
|
18
|
+
private defaultHeaders: { [key: string]: string } = {};
|
|
19
|
+
private logger: ILogger;
|
|
20
|
+
private metricsTracker: IMetricsTracker | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private options: INetworkProxyOptions,
|
|
24
|
+
private connectionPool: ConnectionPool,
|
|
25
|
+
private router: ProxyRouter
|
|
26
|
+
) {
|
|
27
|
+
this.logger = createLogger(options.logLevel || 'info');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set the metrics tracker instance
|
|
32
|
+
*/
|
|
33
|
+
public setMetricsTracker(tracker: IMetricsTracker): void {
|
|
34
|
+
this.metricsTracker = tracker;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set default headers to be included in all responses
|
|
39
|
+
*/
|
|
40
|
+
public setDefaultHeaders(headers: { [key: string]: string }): void {
|
|
41
|
+
this.defaultHeaders = {
|
|
42
|
+
...this.defaultHeaders,
|
|
43
|
+
...headers
|
|
44
|
+
};
|
|
45
|
+
this.logger.info('Updated default response headers');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get all default headers
|
|
50
|
+
*/
|
|
51
|
+
public getDefaultHeaders(): { [key: string]: string } {
|
|
52
|
+
return { ...this.defaultHeaders };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Apply CORS headers to response if configured
|
|
57
|
+
*/
|
|
58
|
+
private applyCorsHeaders(
|
|
59
|
+
res: plugins.http.ServerResponse,
|
|
60
|
+
req: plugins.http.IncomingMessage
|
|
61
|
+
): void {
|
|
62
|
+
if (!this.options.cors) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Apply CORS headers
|
|
67
|
+
if (this.options.cors.allowOrigin) {
|
|
68
|
+
res.setHeader('Access-Control-Allow-Origin', this.options.cors.allowOrigin);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (this.options.cors.allowMethods) {
|
|
72
|
+
res.setHeader('Access-Control-Allow-Methods', this.options.cors.allowMethods);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (this.options.cors.allowHeaders) {
|
|
76
|
+
res.setHeader('Access-Control-Allow-Headers', this.options.cors.allowHeaders);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.options.cors.maxAge) {
|
|
80
|
+
res.setHeader('Access-Control-Max-Age', this.options.cors.maxAge.toString());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle CORS preflight requests
|
|
84
|
+
if (req.method === 'OPTIONS') {
|
|
85
|
+
res.statusCode = 204; // No content
|
|
86
|
+
res.end();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Apply default headers to response
|
|
93
|
+
*/
|
|
94
|
+
private applyDefaultHeaders(res: plugins.http.ServerResponse): void {
|
|
95
|
+
// Apply default headers
|
|
96
|
+
for (const [key, value] of Object.entries(this.defaultHeaders)) {
|
|
97
|
+
if (!res.hasHeader(key)) {
|
|
98
|
+
res.setHeader(key, value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add server identifier if not already set
|
|
103
|
+
if (!res.hasHeader('Server')) {
|
|
104
|
+
res.setHeader('Server', 'NetworkProxy');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle an HTTP request
|
|
110
|
+
*/
|
|
111
|
+
public async handleRequest(
|
|
112
|
+
req: plugins.http.IncomingMessage,
|
|
113
|
+
res: plugins.http.ServerResponse
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
// Record start time for logging
|
|
116
|
+
const startTime = Date.now();
|
|
117
|
+
|
|
118
|
+
// Apply CORS headers if configured
|
|
119
|
+
this.applyCorsHeaders(res, req);
|
|
120
|
+
|
|
121
|
+
// If this is an OPTIONS request, the response has already been ended in applyCorsHeaders
|
|
122
|
+
// so we should return early to avoid trying to set more headers
|
|
123
|
+
if (req.method === 'OPTIONS') {
|
|
124
|
+
// Increment metrics for OPTIONS requests too
|
|
125
|
+
if (this.metricsTracker) {
|
|
126
|
+
this.metricsTracker.incrementRequestsServed();
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Apply default headers
|
|
132
|
+
this.applyDefaultHeaders(res);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Find target based on hostname
|
|
136
|
+
const proxyConfig = this.router.routeReq(req);
|
|
137
|
+
|
|
138
|
+
if (!proxyConfig) {
|
|
139
|
+
// No matching proxy configuration
|
|
140
|
+
this.logger.warn(`No proxy configuration for host: ${req.headers.host}`);
|
|
141
|
+
res.statusCode = 404;
|
|
142
|
+
res.end('Not Found: No proxy configuration for this host');
|
|
143
|
+
|
|
144
|
+
// Increment failed requests counter
|
|
145
|
+
if (this.metricsTracker) {
|
|
146
|
+
this.metricsTracker.incrementFailedRequests();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Get destination IP using round-robin if multiple IPs configured
|
|
153
|
+
const destination = this.connectionPool.getNextTarget(
|
|
154
|
+
proxyConfig.destinationIps,
|
|
155
|
+
proxyConfig.destinationPorts[0]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Create options for the proxy request
|
|
159
|
+
const options: plugins.http.RequestOptions = {
|
|
160
|
+
hostname: destination.host,
|
|
161
|
+
port: destination.port,
|
|
162
|
+
path: req.url,
|
|
163
|
+
method: req.method,
|
|
164
|
+
headers: { ...req.headers }
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Remove host header to avoid issues with virtual hosts on target server
|
|
168
|
+
// The host header should match the target server's expected hostname
|
|
169
|
+
if (options.headers && options.headers.host) {
|
|
170
|
+
if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) {
|
|
171
|
+
options.headers.host = `${destination.host}:${destination.port}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.logger.debug(
|
|
176
|
+
`Proxying request to ${destination.host}:${destination.port}${req.url}`,
|
|
177
|
+
{ method: req.method }
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Create proxy request
|
|
181
|
+
const proxyReq = plugins.http.request(options, (proxyRes) => {
|
|
182
|
+
// Copy status code
|
|
183
|
+
res.statusCode = proxyRes.statusCode || 500;
|
|
184
|
+
|
|
185
|
+
// Copy headers from proxy response to client response
|
|
186
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
187
|
+
if (value !== undefined) {
|
|
188
|
+
res.setHeader(key, value);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Pipe proxy response to client response
|
|
193
|
+
proxyRes.pipe(res);
|
|
194
|
+
|
|
195
|
+
// Increment served requests counter when the response finishes
|
|
196
|
+
res.on('finish', () => {
|
|
197
|
+
if (this.metricsTracker) {
|
|
198
|
+
this.metricsTracker.incrementRequestsServed();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Log the completed request
|
|
202
|
+
const duration = Date.now() - startTime;
|
|
203
|
+
this.logger.debug(
|
|
204
|
+
`Request completed in ${duration}ms: ${req.method} ${req.url} ${res.statusCode}`,
|
|
205
|
+
{ duration, statusCode: res.statusCode }
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Handle proxy request errors
|
|
211
|
+
proxyReq.on('error', (error) => {
|
|
212
|
+
const duration = Date.now() - startTime;
|
|
213
|
+
this.logger.error(
|
|
214
|
+
`Proxy error for ${req.method} ${req.url}: ${error.message}`,
|
|
215
|
+
{ duration, error: error.message }
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Increment failed requests counter
|
|
219
|
+
if (this.metricsTracker) {
|
|
220
|
+
this.metricsTracker.incrementFailedRequests();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check if headers have already been sent
|
|
224
|
+
if (!res.headersSent) {
|
|
225
|
+
res.statusCode = 502;
|
|
226
|
+
res.end(`Bad Gateway: ${error.message}`);
|
|
227
|
+
} else {
|
|
228
|
+
// If headers already sent, just close the connection
|
|
229
|
+
res.end();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Pipe request body to proxy request and handle client-side errors
|
|
234
|
+
req.pipe(proxyReq);
|
|
235
|
+
|
|
236
|
+
// Handle client disconnection
|
|
237
|
+
req.on('error', (error) => {
|
|
238
|
+
this.logger.debug(`Client connection error: ${error.message}`);
|
|
239
|
+
proxyReq.destroy();
|
|
240
|
+
|
|
241
|
+
// Increment failed requests counter on client errors
|
|
242
|
+
if (this.metricsTracker) {
|
|
243
|
+
this.metricsTracker.incrementFailedRequests();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Handle response errors
|
|
248
|
+
res.on('error', (error) => {
|
|
249
|
+
this.logger.debug(`Response error: ${error.message}`);
|
|
250
|
+
proxyReq.destroy();
|
|
251
|
+
|
|
252
|
+
// Increment failed requests counter on response errors
|
|
253
|
+
if (this.metricsTracker) {
|
|
254
|
+
this.metricsTracker.incrementFailedRequests();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
} catch (error) {
|
|
259
|
+
// Handle any unexpected errors
|
|
260
|
+
this.logger.error(
|
|
261
|
+
`Unexpected error handling request: ${error.message}`,
|
|
262
|
+
{ error: error.stack }
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Increment failed requests counter
|
|
266
|
+
if (this.metricsTracker) {
|
|
267
|
+
this.metricsTracker.incrementFailedRequests();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!res.headersSent) {
|
|
271
|
+
res.statusCode = 500;
|
|
272
|
+
res.end('Internal Server Error');
|
|
273
|
+
} else {
|
|
274
|
+
res.end();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for NetworkProxy
|
|
5
|
+
*/
|
|
6
|
+
export interface INetworkProxyOptions {
|
|
7
|
+
port: number;
|
|
8
|
+
maxConnections?: number;
|
|
9
|
+
keepAliveTimeout?: number;
|
|
10
|
+
headersTimeout?: number;
|
|
11
|
+
logLevel?: 'error' | 'warn' | 'info' | 'debug';
|
|
12
|
+
cors?: {
|
|
13
|
+
allowOrigin?: string;
|
|
14
|
+
allowMethods?: string;
|
|
15
|
+
allowHeaders?: string;
|
|
16
|
+
maxAge?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Settings for PortProxy integration
|
|
20
|
+
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
|
|
21
|
+
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
|
|
22
|
+
useExternalPort80Handler?: boolean; // Flag to indicate using external Port80Handler
|
|
23
|
+
|
|
24
|
+
// ACME certificate management options
|
|
25
|
+
acme?: {
|
|
26
|
+
enabled?: boolean; // Whether to enable automatic certificate management
|
|
27
|
+
port?: number; // Port to listen on for ACME challenges (default: 80)
|
|
28
|
+
contactEmail?: string; // Email for Let's Encrypt account
|
|
29
|
+
useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
|
|
30
|
+
renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
|
|
31
|
+
autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
|
|
32
|
+
certificateStore?: string; // Directory to store certificates (default: ./certs)
|
|
33
|
+
skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Interface for a certificate entry in the cache
|
|
39
|
+
*/
|
|
40
|
+
export interface ICertificateEntry {
|
|
41
|
+
key: string;
|
|
42
|
+
cert: string;
|
|
43
|
+
expires?: Date;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Interface for reverse proxy configuration
|
|
48
|
+
*/
|
|
49
|
+
export interface IReverseProxyConfig {
|
|
50
|
+
destinationIps: string[];
|
|
51
|
+
destinationPorts: number[];
|
|
52
|
+
hostName: string;
|
|
53
|
+
privateKey: string;
|
|
54
|
+
publicKey: string;
|
|
55
|
+
authentication?: {
|
|
56
|
+
type: 'Basic';
|
|
57
|
+
user: string;
|
|
58
|
+
pass: string;
|
|
59
|
+
};
|
|
60
|
+
rewriteHostHeader?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Interface for connection tracking in the pool
|
|
65
|
+
*/
|
|
66
|
+
export interface IConnectionEntry {
|
|
67
|
+
socket: plugins.net.Socket;
|
|
68
|
+
lastUsed: number;
|
|
69
|
+
isIdle: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* WebSocket with heartbeat interface
|
|
74
|
+
*/
|
|
75
|
+
export interface IWebSocketWithHeartbeat extends plugins.wsDefault {
|
|
76
|
+
lastPong: number;
|
|
77
|
+
isAlive: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Logger interface for consistent logging across components
|
|
82
|
+
*/
|
|
83
|
+
export interface ILogger {
|
|
84
|
+
debug(message: string, data?: any): void;
|
|
85
|
+
info(message: string, data?: any): void;
|
|
86
|
+
warn(message: string, data?: any): void;
|
|
87
|
+
error(message: string, data?: any): void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates a logger based on the specified log level
|
|
92
|
+
*/
|
|
93
|
+
export function createLogger(logLevel: string = 'info'): ILogger {
|
|
94
|
+
const logLevels = {
|
|
95
|
+
error: 0,
|
|
96
|
+
warn: 1,
|
|
97
|
+
info: 2,
|
|
98
|
+
debug: 3
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
debug: (message: string, data?: any) => {
|
|
103
|
+
if (logLevels[logLevel] >= logLevels.debug) {
|
|
104
|
+
console.log(`[DEBUG] ${message}`, data || '');
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
info: (message: string, data?: any) => {
|
|
108
|
+
if (logLevels[logLevel] >= logLevels.info) {
|
|
109
|
+
console.log(`[INFO] ${message}`, data || '');
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
warn: (message: string, data?: any) => {
|
|
113
|
+
if (logLevels[logLevel] >= logLevels.warn) {
|
|
114
|
+
console.warn(`[WARN] ${message}`, data || '');
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
error: (message: string, data?: any) => {
|
|
118
|
+
if (logLevels[logLevel] >= logLevels.error) {
|
|
119
|
+
console.error(`[ERROR] ${message}`, data || '');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { type INetworkProxyOptions, type IWebSocketWithHeartbeat, type ILogger, createLogger, type IReverseProxyConfig } from './classes.np.types.js';
|
|
3
|
+
import { ConnectionPool } from './classes.np.connectionpool.js';
|
|
4
|
+
import { ProxyRouter } from '../classes.router.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles WebSocket connections and proxying
|
|
8
|
+
*/
|
|
9
|
+
export class WebSocketHandler {
|
|
10
|
+
private heartbeatInterval: NodeJS.Timeout | null = null;
|
|
11
|
+
private wsServer: plugins.ws.WebSocketServer | null = null;
|
|
12
|
+
private logger: ILogger;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private options: INetworkProxyOptions,
|
|
16
|
+
private connectionPool: ConnectionPool,
|
|
17
|
+
private router: ProxyRouter
|
|
18
|
+
) {
|
|
19
|
+
this.logger = createLogger(options.logLevel || 'info');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize WebSocket server on an existing HTTPS server
|
|
24
|
+
*/
|
|
25
|
+
public initialize(server: plugins.https.Server): void {
|
|
26
|
+
// Create WebSocket server
|
|
27
|
+
this.wsServer = new plugins.ws.WebSocketServer({
|
|
28
|
+
server: server,
|
|
29
|
+
clientTracking: true
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Handle WebSocket connections
|
|
33
|
+
this.wsServer.on('connection', (wsIncoming: IWebSocketWithHeartbeat, req: plugins.http.IncomingMessage) => {
|
|
34
|
+
this.handleWebSocketConnection(wsIncoming, req);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Start the heartbeat interval
|
|
38
|
+
this.startHeartbeat();
|
|
39
|
+
|
|
40
|
+
this.logger.info('WebSocket handler initialized');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Start the heartbeat interval to check for inactive WebSocket connections
|
|
45
|
+
*/
|
|
46
|
+
private startHeartbeat(): void {
|
|
47
|
+
// Clean up existing interval if any
|
|
48
|
+
if (this.heartbeatInterval) {
|
|
49
|
+
clearInterval(this.heartbeatInterval);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Set up the heartbeat interval (check every 30 seconds)
|
|
53
|
+
this.heartbeatInterval = setInterval(() => {
|
|
54
|
+
if (!this.wsServer || this.wsServer.clients.size === 0) {
|
|
55
|
+
return; // Skip if no active connections
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.logger.debug(`WebSocket heartbeat check for ${this.wsServer.clients.size} clients`);
|
|
59
|
+
|
|
60
|
+
this.wsServer.clients.forEach((ws: plugins.wsDefault) => {
|
|
61
|
+
const wsWithHeartbeat = ws as IWebSocketWithHeartbeat;
|
|
62
|
+
|
|
63
|
+
if (wsWithHeartbeat.isAlive === false) {
|
|
64
|
+
this.logger.debug('Terminating inactive WebSocket connection');
|
|
65
|
+
return wsWithHeartbeat.terminate();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
wsWithHeartbeat.isAlive = false;
|
|
69
|
+
wsWithHeartbeat.ping();
|
|
70
|
+
});
|
|
71
|
+
}, 30000);
|
|
72
|
+
|
|
73
|
+
// Make sure the interval doesn't keep the process alive
|
|
74
|
+
if (this.heartbeatInterval.unref) {
|
|
75
|
+
this.heartbeatInterval.unref();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handle a new WebSocket connection
|
|
81
|
+
*/
|
|
82
|
+
private handleWebSocketConnection(wsIncoming: IWebSocketWithHeartbeat, req: plugins.http.IncomingMessage): void {
|
|
83
|
+
try {
|
|
84
|
+
// Initialize heartbeat tracking
|
|
85
|
+
wsIncoming.isAlive = true;
|
|
86
|
+
wsIncoming.lastPong = Date.now();
|
|
87
|
+
|
|
88
|
+
// Handle pong messages to track liveness
|
|
89
|
+
wsIncoming.on('pong', () => {
|
|
90
|
+
wsIncoming.isAlive = true;
|
|
91
|
+
wsIncoming.lastPong = Date.now();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Find target configuration based on request
|
|
95
|
+
const proxyConfig = this.router.routeReq(req);
|
|
96
|
+
|
|
97
|
+
if (!proxyConfig) {
|
|
98
|
+
this.logger.warn(`No proxy configuration for WebSocket host: ${req.headers.host}`);
|
|
99
|
+
wsIncoming.close(1008, 'No proxy configuration for this host');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Get destination target using round-robin if multiple targets
|
|
104
|
+
const destination = this.connectionPool.getNextTarget(
|
|
105
|
+
proxyConfig.destinationIps,
|
|
106
|
+
proxyConfig.destinationPorts[0]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Build target URL
|
|
110
|
+
const protocol = (req.socket as any).encrypted ? 'wss' : 'ws';
|
|
111
|
+
const targetUrl = `${protocol}://${destination.host}:${destination.port}${req.url}`;
|
|
112
|
+
|
|
113
|
+
this.logger.debug(`WebSocket connection from ${req.socket.remoteAddress} to ${targetUrl}`);
|
|
114
|
+
|
|
115
|
+
// Create headers for outgoing WebSocket connection
|
|
116
|
+
const headers: { [key: string]: string } = {};
|
|
117
|
+
|
|
118
|
+
// Copy relevant headers from incoming request
|
|
119
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
120
|
+
if (value && typeof value === 'string' &&
|
|
121
|
+
key.toLowerCase() !== 'connection' &&
|
|
122
|
+
key.toLowerCase() !== 'upgrade' &&
|
|
123
|
+
key.toLowerCase() !== 'sec-websocket-key' &&
|
|
124
|
+
key.toLowerCase() !== 'sec-websocket-version') {
|
|
125
|
+
headers[key] = value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Override host header if needed
|
|
130
|
+
if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) {
|
|
131
|
+
headers['host'] = `${destination.host}:${destination.port}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create outgoing WebSocket connection
|
|
135
|
+
const wsOutgoing = new plugins.wsDefault(targetUrl, {
|
|
136
|
+
headers: headers,
|
|
137
|
+
followRedirects: true
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Handle connection errors
|
|
141
|
+
wsOutgoing.on('error', (err) => {
|
|
142
|
+
this.logger.error(`WebSocket target connection error: ${err.message}`);
|
|
143
|
+
if (wsIncoming.readyState === wsIncoming.OPEN) {
|
|
144
|
+
wsIncoming.close(1011, 'Internal server error');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Handle outgoing connection open
|
|
149
|
+
wsOutgoing.on('open', () => {
|
|
150
|
+
// Forward incoming messages to outgoing connection
|
|
151
|
+
wsIncoming.on('message', (data, isBinary) => {
|
|
152
|
+
if (wsOutgoing.readyState === wsOutgoing.OPEN) {
|
|
153
|
+
wsOutgoing.send(data, { binary: isBinary });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Forward outgoing messages to incoming connection
|
|
158
|
+
wsOutgoing.on('message', (data, isBinary) => {
|
|
159
|
+
if (wsIncoming.readyState === wsIncoming.OPEN) {
|
|
160
|
+
wsIncoming.send(data, { binary: isBinary });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Handle closing of connections
|
|
165
|
+
wsIncoming.on('close', (code, reason) => {
|
|
166
|
+
this.logger.debug(`WebSocket client connection closed: ${code} ${reason}`);
|
|
167
|
+
if (wsOutgoing.readyState === wsOutgoing.OPEN) {
|
|
168
|
+
wsOutgoing.close(code, reason);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
wsOutgoing.on('close', (code, reason) => {
|
|
173
|
+
this.logger.debug(`WebSocket target connection closed: ${code} ${reason}`);
|
|
174
|
+
if (wsIncoming.readyState === wsIncoming.OPEN) {
|
|
175
|
+
wsIncoming.close(code, reason);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
this.logger.debug(`WebSocket connection established: ${req.headers.host} -> ${destination.host}:${destination.port}`);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.logger.error(`Error handling WebSocket connection: ${error.message}`);
|
|
184
|
+
if (wsIncoming.readyState === wsIncoming.OPEN) {
|
|
185
|
+
wsIncoming.close(1011, 'Internal server error');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get information about active WebSocket connections
|
|
192
|
+
*/
|
|
193
|
+
public getConnectionInfo(): { activeConnections: number } {
|
|
194
|
+
return {
|
|
195
|
+
activeConnections: this.wsServer ? this.wsServer.clients.size : 0
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Shutdown the WebSocket handler
|
|
201
|
+
*/
|
|
202
|
+
public shutdown(): void {
|
|
203
|
+
// Stop heartbeat interval
|
|
204
|
+
if (this.heartbeatInterval) {
|
|
205
|
+
clearInterval(this.heartbeatInterval);
|
|
206
|
+
this.heartbeatInterval = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Close all WebSocket connections
|
|
210
|
+
if (this.wsServer) {
|
|
211
|
+
this.logger.info(`Closing ${this.wsServer.clients.size} WebSocket connections`);
|
|
212
|
+
|
|
213
|
+
for (const client of this.wsServer.clients) {
|
|
214
|
+
try {
|
|
215
|
+
client.terminate();
|
|
216
|
+
} catch (error) {
|
|
217
|
+
this.logger.error('Error terminating WebSocket client', error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Close the server
|
|
222
|
+
this.wsServer.close();
|
|
223
|
+
this.wsServer = null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Re-export all components for easier imports
|
|
2
|
+
export * from './classes.np.types.js';
|
|
3
|
+
export * from './classes.np.certificatemanager.js';
|
|
4
|
+
export * from './classes.np.connectionpool.js';
|
|
5
|
+
export * from './classes.np.requesthandler.js';
|
|
6
|
+
export * from './classes.np.websockethandler.js';
|
|
7
|
+
export * from './classes.np.networkproxy.js';
|