@push.rocks/smartproxy 6.0.1 → 7.1.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.
@@ -0,0 +1,194 @@
1
+ import * as plugins from '../plugins.js';
2
+ export class Redirect {
3
+ /**
4
+ * Create a new Redirect instance
5
+ * @param options Configuration options
6
+ */
7
+ constructor(options = {}) {
8
+ this.rules = [];
9
+ this.httpPort = 80;
10
+ this.httpsPort = 443;
11
+ if (options.httpPort)
12
+ this.httpPort = options.httpPort;
13
+ if (options.httpsPort)
14
+ this.httpsPort = options.httpsPort;
15
+ if (options.sslOptions)
16
+ this.sslOptions = options.sslOptions;
17
+ if (options.rules)
18
+ this.rules = options.rules;
19
+ }
20
+ /**
21
+ * Add a redirect rule
22
+ */
23
+ addRule(rule) {
24
+ this.rules.push(rule);
25
+ }
26
+ /**
27
+ * Remove all redirect rules
28
+ */
29
+ clearRules() {
30
+ this.rules = [];
31
+ }
32
+ /**
33
+ * Set SSL options for HTTPS redirects
34
+ */
35
+ setSslOptions(options) {
36
+ this.sslOptions = options;
37
+ }
38
+ /**
39
+ * Process a request according to the configured rules
40
+ */
41
+ handleRequest(request, response, protocol) {
42
+ const requestUrl = new URL(request.url || '/', `${protocol}://${request.headers.host || 'localhost'}`);
43
+ const host = requestUrl.hostname;
44
+ const path = requestUrl.pathname + requestUrl.search;
45
+ // Find matching rule
46
+ const matchedRule = this.findMatchingRule(protocol, host, path);
47
+ if (matchedRule) {
48
+ const targetUrl = this.buildTargetUrl(matchedRule, host, path);
49
+ console.log(`Redirecting ${protocol}://${host}${path} to ${targetUrl}`);
50
+ response.writeHead(matchedRule.statusCode || 302, {
51
+ Location: targetUrl,
52
+ });
53
+ response.end();
54
+ }
55
+ else {
56
+ // No matching rule, send 404
57
+ response.writeHead(404, { 'Content-Type': 'text/plain' });
58
+ response.end('Not Found');
59
+ }
60
+ }
61
+ /**
62
+ * Find a matching redirect rule for the given request
63
+ */
64
+ findMatchingRule(protocol, host, path) {
65
+ return this.rules.find((rule) => {
66
+ // Check protocol match
67
+ if (rule.fromProtocol && rule.fromProtocol !== protocol) {
68
+ return false;
69
+ }
70
+ // Check host match
71
+ if (rule.fromHost) {
72
+ const pattern = rule.fromHost.replace(/\*/g, '(.*)');
73
+ const regex = new RegExp(`^${pattern}$`);
74
+ if (!regex.test(host)) {
75
+ return false;
76
+ }
77
+ }
78
+ // Check path match
79
+ if (rule.fromPath && !path.startsWith(rule.fromPath)) {
80
+ return false;
81
+ }
82
+ return true;
83
+ });
84
+ }
85
+ /**
86
+ * Build the target URL for a redirect
87
+ */
88
+ buildTargetUrl(rule, originalHost, originalPath) {
89
+ let targetHost = rule.toHost;
90
+ // Replace wildcards in host
91
+ if (rule.fromHost && rule.fromHost.includes('*')) {
92
+ const pattern = rule.fromHost.replace(/\*/g, '(.*)');
93
+ const regex = new RegExp(`^${pattern}$`);
94
+ const matches = originalHost.match(regex);
95
+ if (matches) {
96
+ for (let i = 1; i < matches.length; i++) {
97
+ targetHost = targetHost.replace(`$${i}`, matches[i]);
98
+ }
99
+ }
100
+ }
101
+ // Build target path
102
+ let targetPath = originalPath;
103
+ if (rule.toPath) {
104
+ if (rule.toPath.includes('$path')) {
105
+ // Replace $path with original path, optionally removing the fromPath prefix
106
+ const pathSuffix = rule.fromPath ?
107
+ originalPath.substring(rule.fromPath.length) :
108
+ originalPath;
109
+ targetPath = rule.toPath.replace('$path', pathSuffix);
110
+ }
111
+ else {
112
+ targetPath = rule.toPath;
113
+ }
114
+ }
115
+ return `${rule.toProtocol}://${targetHost}${targetPath}`;
116
+ }
117
+ /**
118
+ * Start the redirect server(s)
119
+ */
120
+ async start() {
121
+ const tasks = [];
122
+ // Create and start HTTP server if we have a port
123
+ if (this.httpPort) {
124
+ this.httpServer = plugins.http.createServer((req, res) => this.handleRequest(req, res, 'http'));
125
+ const httpStartPromise = new Promise((resolve) => {
126
+ this.httpServer?.listen(this.httpPort, () => {
127
+ console.log(`HTTP redirect server started on port ${this.httpPort}`);
128
+ resolve();
129
+ });
130
+ });
131
+ tasks.push(httpStartPromise);
132
+ }
133
+ // Create and start HTTPS server if we have SSL options and a port
134
+ if (this.httpsPort && this.sslOptions) {
135
+ this.httpsServer = plugins.https.createServer(this.sslOptions, (req, res) => this.handleRequest(req, res, 'https'));
136
+ const httpsStartPromise = new Promise((resolve) => {
137
+ this.httpsServer?.listen(this.httpsPort, () => {
138
+ console.log(`HTTPS redirect server started on port ${this.httpsPort}`);
139
+ resolve();
140
+ });
141
+ });
142
+ tasks.push(httpsStartPromise);
143
+ }
144
+ // Wait for all servers to start
145
+ await Promise.all(tasks);
146
+ }
147
+ /**
148
+ * Stop the redirect server(s)
149
+ */
150
+ async stop() {
151
+ const tasks = [];
152
+ if (this.httpServer) {
153
+ const httpStopPromise = new Promise((resolve) => {
154
+ this.httpServer?.close(() => {
155
+ console.log('HTTP redirect server stopped');
156
+ resolve();
157
+ });
158
+ });
159
+ tasks.push(httpStopPromise);
160
+ }
161
+ if (this.httpsServer) {
162
+ const httpsStopPromise = new Promise((resolve) => {
163
+ this.httpsServer?.close(() => {
164
+ console.log('HTTPS redirect server stopped');
165
+ resolve();
166
+ });
167
+ });
168
+ tasks.push(httpsStopPromise);
169
+ }
170
+ await Promise.all(tasks);
171
+ }
172
+ }
173
+ // For backward compatibility
174
+ export class SslRedirect {
175
+ constructor(portArg) {
176
+ this.port = portArg;
177
+ this.redirect = new Redirect({
178
+ httpPort: portArg,
179
+ rules: [{
180
+ fromProtocol: 'http',
181
+ toProtocol: 'https',
182
+ toHost: '$1',
183
+ statusCode: 302
184
+ }]
185
+ });
186
+ }
187
+ async start() {
188
+ await this.redirect.start();
189
+ }
190
+ async stop() {
191
+ await this.redirect.stop();
192
+ }
193
+ }
194
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5yZWRpcmVjdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3JlZGlyZWN0L2NsYXNzZXMucmVkaXJlY3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxlQUFlLENBQUM7QUEwQ3pDLE1BQU0sT0FBTyxRQUFRO0lBV25COzs7T0FHRztJQUNILFlBQVksVUFRUixFQUFFO1FBcEJFLFVBQUssR0FBbUIsRUFBRSxDQUFDO1FBQzNCLGFBQVEsR0FBVyxFQUFFLENBQUM7UUFDdEIsY0FBUyxHQUFXLEdBQUcsQ0FBQztRQW1COUIsSUFBSSxPQUFPLENBQUMsUUFBUTtZQUFFLElBQUksQ0FBQyxRQUFRLEdBQUcsT0FBTyxDQUFDLFFBQVEsQ0FBQztRQUN2RCxJQUFJLE9BQU8sQ0FBQyxTQUFTO1lBQUUsSUFBSSxDQUFDLFNBQVMsR0FBRyxPQUFPLENBQUMsU0FBUyxDQUFDO1FBQzFELElBQUksT0FBTyxDQUFDLFVBQVU7WUFBRSxJQUFJLENBQUMsVUFBVSxHQUFHLE9BQU8sQ0FBQyxVQUFVLENBQUM7UUFDN0QsSUFBSSxPQUFPLENBQUMsS0FBSztZQUFFLElBQUksQ0FBQyxLQUFLLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQztJQUNoRCxDQUFDO0lBRUQ7O09BRUc7SUFDSSxPQUFPLENBQUMsSUFBa0I7UUFDL0IsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDeEIsQ0FBQztJQUVEOztPQUVHO0lBQ0ksVUFBVTtRQUNmLElBQUksQ0FBQyxLQUFLLEdBQUcsRUFBRSxDQUFDO0lBQ2xCLENBQUM7SUFFRDs7T0FFRztJQUNJLGFBQWEsQ0FBQyxPQUFzQztRQUN6RCxJQUFJLENBQUMsVUFBVSxHQUFHLE9BQU8sQ0FBQztJQUM1QixDQUFDO0lBRUQ7O09BRUc7SUFDSyxhQUFhLENBQ25CLE9BQXFDLEVBQ3JDLFFBQXFDLEVBQ3JDLFFBQTBCO1FBRTFC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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartproxy",
3
- "version": "6.0.1",
3
+ "version": "7.1.0",
4
4
  "private": false,
5
5
  "description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
6
6
  "main": "dist_ts/index.js",
package/readme.md CHANGED
@@ -197,7 +197,27 @@ sequenceDiagram
197
197
  - **HTTP to HTTPS Redirection** - Automatically redirect HTTP requests to HTTPS
198
198
  - **Let's Encrypt Integration** - Automatic certificate management using ACME protocol
199
199
  - **IP Filtering** - Control access with IP allow/block lists using glob patterns
200
- - **NfTables Integration** - Direct manipulation of nftables for advanced low-level port forwarding
200
+ - **NfTables Integration** - Direct manipulation of nftables for advanced low-level port forwarding
201
+
202
+ ## Configuration Options
203
+
204
+ ### backendProtocol
205
+
206
+ Type: 'http1' | 'http2' (default: 'http1')
207
+
208
+ Controls the protocol used when proxying requests to backend services. By default, the proxy uses HTTP/1.x (`http.request`). Setting `backendProtocol: 'http2'` establishes HTTP/2 client sessions (`http2.connect`) to your backends for full end-to-end HTTP/2 support (assuming your backend servers support HTTP/2).
209
+
210
+ Example:
211
+ ```js
212
+ import { NetworkProxy } from '@push.rocks/smartproxy';
213
+
214
+ const proxy = new NetworkProxy({
215
+ port: 8443,
216
+ backendProtocol: 'http2',
217
+ // other options...
218
+ });
219
+ proxy.start();
220
+ ```
201
221
  - **Basic Authentication** - Support for basic auth on proxied routes
202
222
  - **Connection Management** - Intelligent connection tracking and cleanup with configurable timeouts
203
223
  - **Browser Compatibility** - Optimized for modern browsers with fixes for common TLS handshake issues
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '6.0.1',
6
+ version: '7.1.0',
7
7
  description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
8
8
  }
package/ts/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from './nfttablesproxy/classes.nftablesproxy.js';
2
2
  export * from './networkproxy/classes.np.networkproxy.js';
3
3
  export * from './port80handler/classes.port80handler.js';
4
- export * from './classes.sslredirect.js';
4
+ export * from './redirect/classes.redirect.js';
5
5
  export * from './smartproxy/classes.smartproxy.js';
6
6
  export * from './smartproxy/classes.pp.snihandler.js';
7
7
  export * from './smartproxy/classes.pp.interfaces.js';
@@ -16,8 +16,8 @@ export class NetworkProxy implements IMetricsTracker {
16
16
  public options: INetworkProxyOptions;
17
17
  public proxyConfigs: IReverseProxyConfig[] = [];
18
18
 
19
- // Server instances
20
- public httpsServer: plugins.https.Server;
19
+ // Server instances (HTTP/2 with HTTP/1 fallback)
20
+ public httpsServer: any;
21
21
 
22
22
  // Core components
23
23
  private certificateManager: CertificateManager;
@@ -66,6 +66,8 @@ export class NetworkProxy implements IMetricsTracker {
66
66
  connectionPoolSize: optionsArg.connectionPoolSize || 50,
67
67
  portProxyIntegration: optionsArg.portProxyIntegration || false,
68
68
  useExternalPort80Handler: optionsArg.useExternalPort80Handler || false,
69
+ // Backend protocol (http1 or http2)
70
+ backendProtocol: optionsArg.backendProtocol || 'http1',
69
71
  // Default ACME options
70
72
  acme: {
71
73
  enabled: optionsArg.acme?.enabled || false,
@@ -185,33 +187,35 @@ export class NetworkProxy implements IMetricsTracker {
185
187
  await this.certificateManager.initializePort80Handler();
186
188
  }
187
189
 
188
- // Create the HTTPS server
189
- this.httpsServer = plugins.https.createServer(
190
+ // Create HTTP/2 server with HTTP/1 fallback
191
+ this.httpsServer = plugins.http2.createSecureServer(
190
192
  {
191
193
  key: this.certificateManager.getDefaultCertificates().key,
192
194
  cert: this.certificateManager.getDefaultCertificates().cert,
193
- SNICallback: (domain, cb) => this.certificateManager.handleSNI(domain, cb)
194
- },
195
- (req, res) => this.requestHandler.handleRequest(req, res)
195
+ allowHTTP1: true,
196
+ ALPNProtocols: ['h2', 'http/1.1']
197
+ }
196
198
  );
197
199
 
198
- // Configure server timeouts
199
- this.httpsServer.keepAliveTimeout = this.options.keepAliveTimeout;
200
- this.httpsServer.headersTimeout = this.options.headersTimeout;
201
-
202
- // Setup connection tracking
200
+ // Track raw TCP connections for metrics and limits
203
201
  this.setupConnectionTracking();
204
-
205
- // Share HTTPS server with certificate manager
202
+
203
+ // Handle incoming HTTP/2 streams
204
+ this.httpsServer.on('stream', (stream: any, headers: any) => {
205
+ this.requestHandler.handleHttp2(stream, headers);
206
+ });
207
+ // Handle HTTP/1.x fallback requests
208
+ this.httpsServer.on('request', (req: any, res: any) => {
209
+ this.requestHandler.handleRequest(req, res);
210
+ });
211
+
212
+ // Share server with certificate manager for dynamic contexts
206
213
  this.certificateManager.setHttpsServer(this.httpsServer);
207
-
208
- // Setup WebSocket support
214
+ // Setup WebSocket support on HTTP/1 fallback
209
215
  this.webSocketHandler.initialize(this.httpsServer);
210
-
211
- // Start metrics collection
216
+ // Start metrics logging
212
217
  this.setupMetricsCollection();
213
-
214
- // Setup connection pool cleanup interval
218
+ // Start periodic connection pool cleanup
215
219
  this.connectionPoolCleanupInterval = this.connectionPool.setupPeriodicCleanup();
216
220
 
217
221
  // Start the server
@@ -18,6 +18,8 @@ export class RequestHandler {
18
18
  private defaultHeaders: { [key: string]: string } = {};
19
19
  private logger: ILogger;
20
20
  private metricsTracker: IMetricsTracker | null = null;
21
+ // HTTP/2 client sessions for backend proxying
22
+ private h2Sessions: Map<string, plugins.http2.ClientHttp2Session> = new Map();
21
23
 
22
24
  constructor(
23
25
  private options: INetworkProxyOptions,
@@ -130,6 +132,70 @@ export class RequestHandler {
130
132
 
131
133
  // Apply default headers
132
134
  this.applyDefaultHeaders(res);
135
+ // If configured to proxy to backends over HTTP/2, use HTTP/2 client sessions
136
+ if (this.options.backendProtocol === 'http2') {
137
+ // Route and validate config
138
+ const proxyConfig = this.router.routeReq(req);
139
+ if (!proxyConfig) {
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
+ if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
144
+ return;
145
+ }
146
+ // Determine backend target
147
+ const destination = this.connectionPool.getNextTarget(
148
+ proxyConfig.destinationIps,
149
+ proxyConfig.destinationPorts[0]
150
+ );
151
+ // Obtain or create HTTP/2 session
152
+ const key = `${destination.host}:${destination.port}`;
153
+ let session = this.h2Sessions.get(key);
154
+ if (!session || session.closed || (session as any).destroyed) {
155
+ session = plugins.http2.connect(`http://${destination.host}:${destination.port}`);
156
+ this.h2Sessions.set(key, session);
157
+ session.on('error', () => this.h2Sessions.delete(key));
158
+ session.on('close', () => this.h2Sessions.delete(key));
159
+ }
160
+ // Build headers for HTTP/2 request
161
+ const h2Headers: Record<string, string> = {
162
+ ':method': req.method || 'GET',
163
+ ':path': req.url || '/',
164
+ ':authority': `${destination.host}:${destination.port}`
165
+ };
166
+ for (const [k, v] of Object.entries(req.headers)) {
167
+ if (typeof v === 'string' && !k.startsWith(':')) {
168
+ h2Headers[k] = v;
169
+ }
170
+ }
171
+ // Open HTTP/2 stream
172
+ const h2Stream = session.request(h2Headers);
173
+ // Pipe client request body to backend
174
+ req.pipe(h2Stream);
175
+ // Handle backend response
176
+ h2Stream.on('response', (headers, flags) => {
177
+ const status = headers[':status'] as number || 502;
178
+ // Map headers
179
+ for (const [hk, hv] of Object.entries(headers)) {
180
+ if (!hk.startsWith(':') && hv) {
181
+ res.setHeader(hk, hv as string);
182
+ }
183
+ }
184
+ res.statusCode = status;
185
+ h2Stream.pipe(res);
186
+ });
187
+ h2Stream.on('error', (err) => {
188
+ this.logger.error(`HTTP/2 proxy error: ${err.message}`);
189
+ if (!res.headersSent) {
190
+ res.statusCode = 502;
191
+ res.end(`Bad Gateway: ${err.message}`);
192
+ } else {
193
+ res.end();
194
+ }
195
+ if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
196
+ });
197
+ return;
198
+ }
133
199
 
134
200
  try {
135
201
  // Find target based on hostname
@@ -275,4 +341,119 @@ export class RequestHandler {
275
341
  }
276
342
  }
277
343
  }
344
+
345
+ /**
346
+ * Handle HTTP/2 stream requests by proxying to HTTP/1 backends
347
+ */
348
+ public async handleHttp2(stream: any, headers: any): Promise<void> {
349
+ const startTime = Date.now();
350
+ const method = headers[':method'] || 'GET';
351
+ const path = headers[':path'] || '/';
352
+ // If configured to proxy to backends over HTTP/2, use HTTP/2 client sessions
353
+ if (this.options.backendProtocol === 'http2') {
354
+ const authority = headers[':authority'] as string || '';
355
+ const host = authority.split(':')[0];
356
+ const fakeReq: any = { headers: { host }, method: headers[':method'], url: headers[':path'], socket: (stream.session as any).socket };
357
+ const proxyConfig = this.router.routeReq(fakeReq);
358
+ if (!proxyConfig) {
359
+ stream.respond({ ':status': 404 });
360
+ stream.end('Not Found');
361
+ if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
362
+ return;
363
+ }
364
+ const destination = this.connectionPool.getNextTarget(proxyConfig.destinationIps, proxyConfig.destinationPorts[0]);
365
+ const key = `${destination.host}:${destination.port}`;
366
+ let session = this.h2Sessions.get(key);
367
+ if (!session || session.closed || (session as any).destroyed) {
368
+ session = plugins.http2.connect(`http://${destination.host}:${destination.port}`);
369
+ this.h2Sessions.set(key, session);
370
+ session.on('error', () => this.h2Sessions.delete(key));
371
+ session.on('close', () => this.h2Sessions.delete(key));
372
+ }
373
+ // Build headers for backend HTTP/2 request
374
+ const h2Headers: Record<string, any> = {
375
+ ':method': headers[':method'],
376
+ ':path': headers[':path'],
377
+ ':authority': `${destination.host}:${destination.port}`
378
+ };
379
+ for (const [k, v] of Object.entries(headers)) {
380
+ if (!k.startsWith(':') && typeof v === 'string') {
381
+ h2Headers[k] = v;
382
+ }
383
+ }
384
+ const h2Stream2 = session.request(h2Headers);
385
+ stream.pipe(h2Stream2);
386
+ h2Stream2.on('response', (hdrs: any) => {
387
+ // Map status and headers to client
388
+ const resp: Record<string, any> = { ':status': hdrs[':status'] as number };
389
+ for (const [hk, hv] of Object.entries(hdrs)) {
390
+ if (!hk.startsWith(':') && hv) resp[hk] = hv;
391
+ }
392
+ stream.respond(resp);
393
+ h2Stream2.pipe(stream);
394
+ });
395
+ h2Stream2.on('error', (err) => {
396
+ stream.respond({ ':status': 502 });
397
+ stream.end(`Bad Gateway: ${err.message}`);
398
+ if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
399
+ });
400
+ return;
401
+ }
402
+ try {
403
+ // Determine host for routing
404
+ const authority = headers[':authority'] as string || '';
405
+ const host = authority.split(':')[0];
406
+ // Fake request object for routing
407
+ const fakeReq: any = { headers: { host }, method, url: path, socket: (stream.session as any).socket };
408
+ const proxyConfig = this.router.routeReq(fakeReq as any);
409
+ if (!proxyConfig) {
410
+ stream.respond({ ':status': 404 });
411
+ stream.end('Not Found');
412
+ if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
413
+ return;
414
+ }
415
+ // Select backend target
416
+ const destination = this.connectionPool.getNextTarget(
417
+ proxyConfig.destinationIps,
418
+ proxyConfig.destinationPorts[0]
419
+ );
420
+ // Build headers for HTTP/1 proxy
421
+ const outboundHeaders: Record<string,string> = {};
422
+ for (const [key, value] of Object.entries(headers)) {
423
+ if (typeof key === 'string' && typeof value === 'string' && !key.startsWith(':')) {
424
+ outboundHeaders[key] = value;
425
+ }
426
+ }
427
+ if (outboundHeaders.host && (proxyConfig as any).rewriteHostHeader) {
428
+ outboundHeaders.host = `${destination.host}:${destination.port}`;
429
+ }
430
+ // Create HTTP/1 proxy request
431
+ const proxyReq = plugins.http.request(
432
+ { hostname: destination.host, port: destination.port, path, method, headers: outboundHeaders },
433
+ (proxyRes) => {
434
+ // Map status and headers back to HTTP/2
435
+ const responseHeaders: Record<string, number|string|string[]> = {};
436
+ for (const [k, v] of Object.entries(proxyRes.headers)) {
437
+ if (v !== undefined) responseHeaders[k] = v;
438
+ }
439
+ stream.respond({ ':status': proxyRes.statusCode || 500, ...responseHeaders });
440
+ proxyRes.pipe(stream);
441
+ stream.on('close', () => proxyReq.destroy());
442
+ stream.on('error', () => proxyReq.destroy());
443
+ if (this.metricsTracker) stream.on('end', () => this.metricsTracker.incrementRequestsServed());
444
+ }
445
+ );
446
+ proxyReq.on('error', (err) => {
447
+ stream.respond({ ':status': 502 });
448
+ stream.end(`Bad Gateway: ${err.message}`);
449
+ if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
450
+ });
451
+ // Pipe client stream to backend
452
+ stream.pipe(proxyReq);
453
+ } catch (err: any) {
454
+ stream.respond({ ':status': 500 });
455
+ stream.end('Internal Server Error');
456
+ if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
457
+ }
458
+ }
278
459
  }
@@ -20,6 +20,8 @@ export interface INetworkProxyOptions {
20
20
  connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
21
21
  portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
22
22
  useExternalPort80Handler?: boolean; // Flag to indicate using external Port80Handler
23
+ // Protocol to use when proxying to backends: HTTP/1.x or HTTP/2
24
+ backendProtocol?: 'http1' | 'http2';
23
25
 
24
26
  // ACME certificate management options
25
27
  acme?: {
package/ts/plugins.ts CHANGED
@@ -5,9 +5,10 @@ import * as https from 'https';
5
5
  import * as net from 'net';
6
6
  import * as tls from 'tls';
7
7
  import * as url from 'url';
8
+ import * as http2 from 'http2';
8
9
 
9
10
 
10
- export { EventEmitter, http, https, net, tls, url };
11
+ export { EventEmitter, http, https, net, tls, url, http2 };
11
12
 
12
13
  // tsclass scope
13
14
  import * as tsclass from '@tsclass/tsclass';