Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
ce3d0feb77 | |||
04abab505b | |||
e69c55de3b | |||
9a9bcd2df0 | |||
b27cb8988c | |||
0de7531e17 |
21
changelog.md
21
changelog.md
@ -1,5 +1,26 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-04-19 - 7.1.0 - feat(core)
|
||||||
|
Add backendProtocol option to support HTTP/2 client sessions alongside HTTP/1. This update enhances NetworkProxy's core functionality by integrating HTTP/2 support in server creation and request handling, while updating plugin exports and documentation accordingly.
|
||||||
|
|
||||||
|
- Introduced 'backendProtocol' configuration option (http1 | http2) with default 'http1'.
|
||||||
|
- Updated creation of secure server to use http2.createSecureServer with HTTP/1 fallback.
|
||||||
|
- Enhanced request handling to establish HTTP/2 client sessions when backendProtocol is set to 'http2'.
|
||||||
|
- Exported http2 module in plugins.
|
||||||
|
- Updated readme.md to document backendProtocol usage with example code.
|
||||||
|
|
||||||
|
## 2025-04-05 - 7.0.1 - fix(package.json)
|
||||||
|
Update packageManager field in package.json to specify the pnpm version for improved reproducibility.
|
||||||
|
|
||||||
|
- Added the packageManager field to clearly specify the pnpm version and its checksum.
|
||||||
|
|
||||||
|
## 2025-04-04 - 7.0.0 - BREAKING CHANGE(redirect)
|
||||||
|
Remove deprecated SSL redirect implementation and update exports to use the new redirect module
|
||||||
|
|
||||||
|
- Deleted ts/classes.sslredirect.ts which contained the old SSL redirect logic
|
||||||
|
- Updated ts/index.ts to export 'redirect/classes.redirect.js' instead of the removed SSL redirect module
|
||||||
|
- Adopted a new redirect implementation that provides enhanced features and a more consistent API
|
||||||
|
|
||||||
## 2025-03-25 - 6.0.1 - fix(readme)
|
## 2025-03-25 - 6.0.1 - fix(readme)
|
||||||
Update README documentation: replace all outdated 'PortProxy' references with 'SmartProxy', adjust architecture diagrams, code examples, and configuration details (including correcting IPTables to NfTables) to reflect the new naming.
|
Update README documentation: replace all outdated 'PortProxy' references with 'SmartProxy', adjust architecture diagrams, code examples, and configuration details (including correcting IPTables to NfTables) to reflect the new naming.
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "6.0.1",
|
"version": "7.1.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"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.",
|
"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.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@ -83,5 +83,6 @@
|
|||||||
"mongodb-memory-server",
|
"mongodb-memory-server",
|
||||||
"puppeteer"
|
"puppeteer"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||||
}
|
}
|
||||||
|
22
readme.md
22
readme.md
@ -197,7 +197,27 @@ sequenceDiagram
|
|||||||
- **HTTP to HTTPS Redirection** - Automatically redirect HTTP requests to HTTPS
|
- **HTTP to HTTPS Redirection** - Automatically redirect HTTP requests to HTTPS
|
||||||
- **Let's Encrypt Integration** - Automatic certificate management using ACME protocol
|
- **Let's Encrypt Integration** - Automatic certificate management using ACME protocol
|
||||||
- **IP Filtering** - Control access with IP allow/block lists using glob patterns
|
- **IP Filtering** - Control access with IP allow/block lists using glob patterns
|
||||||
- **NfTables Integration** - Direct manipulation of nftables for advanced low-level port forwarding
|
- **NfTables Integration** - Direct manipulation of nftables for advanced low-level port forwarding
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### backendProtocol
|
||||||
|
|
||||||
|
Type: 'http1' | 'http2' (default: 'http1')
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```js
|
||||||
|
import { NetworkProxy } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
|
const proxy = new NetworkProxy({
|
||||||
|
port: 8443,
|
||||||
|
backendProtocol: 'http2',
|
||||||
|
// other options...
|
||||||
|
});
|
||||||
|
proxy.start();
|
||||||
|
```
|
||||||
- **Basic Authentication** - Support for basic auth on proxied routes
|
- **Basic Authentication** - Support for basic auth on proxied routes
|
||||||
- **Connection Management** - Intelligent connection tracking and cleanup with configurable timeouts
|
- **Connection Management** - Intelligent connection tracking and cleanup with configurable timeouts
|
||||||
- **Browser Compatibility** - Optimized for modern browsers with fixes for common TLS handshake issues
|
- **Browser Compatibility** - Optimized for modern browsers with fixes for common TLS handshake issues
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '6.0.1',
|
version: '7.1.0',
|
||||||
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.'
|
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.'
|
||||||
}
|
}
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import * as plugins from './plugins.js';
|
|
||||||
|
|
||||||
export class SslRedirect {
|
|
||||||
httpServer: plugins.http.Server;
|
|
||||||
port: number;
|
|
||||||
constructor(portArg: number) {
|
|
||||||
this.port = portArg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async start() {
|
|
||||||
this.httpServer = plugins.http.createServer((request, response) => {
|
|
||||||
const requestUrl = new URL(request.url, `http://${request.headers.host}`);
|
|
||||||
const completeUrlWithoutProtocol = `${requestUrl.host}${requestUrl.pathname}${requestUrl.search}`;
|
|
||||||
const redirectUrl = `https://${completeUrlWithoutProtocol}`;
|
|
||||||
console.log(`Got http request for http://${completeUrlWithoutProtocol}`);
|
|
||||||
console.log(`Redirecting to ${redirectUrl}`);
|
|
||||||
response.writeHead(302, {
|
|
||||||
Location: redirectUrl,
|
|
||||||
});
|
|
||||||
response.end();
|
|
||||||
});
|
|
||||||
this.httpServer.listen(this.port);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async stop() {
|
|
||||||
const done = plugins.smartpromise.defer();
|
|
||||||
this.httpServer.close(() => {
|
|
||||||
done.resolve();
|
|
||||||
});
|
|
||||||
await done.promise;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
export * from './nfttablesproxy/classes.nftablesproxy.js';
|
export * from './nfttablesproxy/classes.nftablesproxy.js';
|
||||||
export * from './networkproxy/classes.np.networkproxy.js';
|
export * from './networkproxy/classes.np.networkproxy.js';
|
||||||
export * from './port80handler/classes.port80handler.js';
|
export * from './port80handler/classes.port80handler.js';
|
||||||
export * from './classes.sslredirect.js';
|
export * from './redirect/classes.redirect.js';
|
||||||
export * from './smartproxy/classes.smartproxy.js';
|
export * from './smartproxy/classes.smartproxy.js';
|
||||||
export * from './smartproxy/classes.pp.snihandler.js';
|
export * from './smartproxy/classes.pp.snihandler.js';
|
||||||
export * from './smartproxy/classes.pp.interfaces.js';
|
export * from './smartproxy/classes.pp.interfaces.js';
|
||||||
|
@ -16,8 +16,8 @@ export class NetworkProxy implements IMetricsTracker {
|
|||||||
public options: INetworkProxyOptions;
|
public options: INetworkProxyOptions;
|
||||||
public proxyConfigs: IReverseProxyConfig[] = [];
|
public proxyConfigs: IReverseProxyConfig[] = [];
|
||||||
|
|
||||||
// Server instances
|
// Server instances (HTTP/2 with HTTP/1 fallback)
|
||||||
public httpsServer: plugins.https.Server;
|
public httpsServer: any;
|
||||||
|
|
||||||
// Core components
|
// Core components
|
||||||
private certificateManager: CertificateManager;
|
private certificateManager: CertificateManager;
|
||||||
@ -66,6 +66,8 @@ export class NetworkProxy implements IMetricsTracker {
|
|||||||
connectionPoolSize: optionsArg.connectionPoolSize || 50,
|
connectionPoolSize: optionsArg.connectionPoolSize || 50,
|
||||||
portProxyIntegration: optionsArg.portProxyIntegration || false,
|
portProxyIntegration: optionsArg.portProxyIntegration || false,
|
||||||
useExternalPort80Handler: optionsArg.useExternalPort80Handler || false,
|
useExternalPort80Handler: optionsArg.useExternalPort80Handler || false,
|
||||||
|
// Backend protocol (http1 or http2)
|
||||||
|
backendProtocol: optionsArg.backendProtocol || 'http1',
|
||||||
// Default ACME options
|
// Default ACME options
|
||||||
acme: {
|
acme: {
|
||||||
enabled: optionsArg.acme?.enabled || false,
|
enabled: optionsArg.acme?.enabled || false,
|
||||||
@ -185,33 +187,35 @@ export class NetworkProxy implements IMetricsTracker {
|
|||||||
await this.certificateManager.initializePort80Handler();
|
await this.certificateManager.initializePort80Handler();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the HTTPS server
|
// Create HTTP/2 server with HTTP/1 fallback
|
||||||
this.httpsServer = plugins.https.createServer(
|
this.httpsServer = plugins.http2.createSecureServer(
|
||||||
{
|
{
|
||||||
key: this.certificateManager.getDefaultCertificates().key,
|
key: this.certificateManager.getDefaultCertificates().key,
|
||||||
cert: this.certificateManager.getDefaultCertificates().cert,
|
cert: this.certificateManager.getDefaultCertificates().cert,
|
||||||
SNICallback: (domain, cb) => this.certificateManager.handleSNI(domain, cb)
|
allowHTTP1: true,
|
||||||
},
|
ALPNProtocols: ['h2', 'http/1.1']
|
||||||
(req, res) => this.requestHandler.handleRequest(req, res)
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Configure server timeouts
|
// Track raw TCP connections for metrics and limits
|
||||||
this.httpsServer.keepAliveTimeout = this.options.keepAliveTimeout;
|
|
||||||
this.httpsServer.headersTimeout = this.options.headersTimeout;
|
|
||||||
|
|
||||||
// Setup connection tracking
|
|
||||||
this.setupConnectionTracking();
|
this.setupConnectionTracking();
|
||||||
|
|
||||||
// Share HTTPS server with certificate manager
|
// Handle incoming HTTP/2 streams
|
||||||
|
this.httpsServer.on('stream', (stream: any, headers: any) => {
|
||||||
|
this.requestHandler.handleHttp2(stream, headers);
|
||||||
|
});
|
||||||
|
// Handle HTTP/1.x fallback requests
|
||||||
|
this.httpsServer.on('request', (req: any, res: any) => {
|
||||||
|
this.requestHandler.handleRequest(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Share server with certificate manager for dynamic contexts
|
||||||
this.certificateManager.setHttpsServer(this.httpsServer);
|
this.certificateManager.setHttpsServer(this.httpsServer);
|
||||||
|
// Setup WebSocket support on HTTP/1 fallback
|
||||||
// Setup WebSocket support
|
|
||||||
this.webSocketHandler.initialize(this.httpsServer);
|
this.webSocketHandler.initialize(this.httpsServer);
|
||||||
|
// Start metrics logging
|
||||||
// Start metrics collection
|
|
||||||
this.setupMetricsCollection();
|
this.setupMetricsCollection();
|
||||||
|
// Start periodic connection pool cleanup
|
||||||
// Setup connection pool cleanup interval
|
|
||||||
this.connectionPoolCleanupInterval = this.connectionPool.setupPeriodicCleanup();
|
this.connectionPoolCleanupInterval = this.connectionPool.setupPeriodicCleanup();
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
|
@ -18,6 +18,8 @@ export class RequestHandler {
|
|||||||
private defaultHeaders: { [key: string]: string } = {};
|
private defaultHeaders: { [key: string]: string } = {};
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
private metricsTracker: IMetricsTracker | null = null;
|
private metricsTracker: IMetricsTracker | null = null;
|
||||||
|
// HTTP/2 client sessions for backend proxying
|
||||||
|
private h2Sessions: Map<string, plugins.http2.ClientHttp2Session> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private options: INetworkProxyOptions,
|
private options: INetworkProxyOptions,
|
||||||
@ -130,6 +132,70 @@ export class RequestHandler {
|
|||||||
|
|
||||||
// Apply default headers
|
// Apply default headers
|
||||||
this.applyDefaultHeaders(res);
|
this.applyDefaultHeaders(res);
|
||||||
|
// If configured to proxy to backends over HTTP/2, use HTTP/2 client sessions
|
||||||
|
if (this.options.backendProtocol === 'http2') {
|
||||||
|
// Route and validate config
|
||||||
|
const proxyConfig = this.router.routeReq(req);
|
||||||
|
if (!proxyConfig) {
|
||||||
|
this.logger.warn(`No proxy configuration for host: ${req.headers.host}`);
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('Not Found: No proxy configuration for this host');
|
||||||
|
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Determine backend target
|
||||||
|
const destination = this.connectionPool.getNextTarget(
|
||||||
|
proxyConfig.destinationIps,
|
||||||
|
proxyConfig.destinationPorts[0]
|
||||||
|
);
|
||||||
|
// Obtain or create HTTP/2 session
|
||||||
|
const key = `${destination.host}:${destination.port}`;
|
||||||
|
let session = this.h2Sessions.get(key);
|
||||||
|
if (!session || session.closed || (session as any).destroyed) {
|
||||||
|
session = plugins.http2.connect(`http://${destination.host}:${destination.port}`);
|
||||||
|
this.h2Sessions.set(key, session);
|
||||||
|
session.on('error', () => this.h2Sessions.delete(key));
|
||||||
|
session.on('close', () => this.h2Sessions.delete(key));
|
||||||
|
}
|
||||||
|
// Build headers for HTTP/2 request
|
||||||
|
const h2Headers: Record<string, string> = {
|
||||||
|
':method': req.method || 'GET',
|
||||||
|
':path': req.url || '/',
|
||||||
|
':authority': `${destination.host}:${destination.port}`
|
||||||
|
};
|
||||||
|
for (const [k, v] of Object.entries(req.headers)) {
|
||||||
|
if (typeof v === 'string' && !k.startsWith(':')) {
|
||||||
|
h2Headers[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Open HTTP/2 stream
|
||||||
|
const h2Stream = session.request(h2Headers);
|
||||||
|
// Pipe client request body to backend
|
||||||
|
req.pipe(h2Stream);
|
||||||
|
// Handle backend response
|
||||||
|
h2Stream.on('response', (headers, flags) => {
|
||||||
|
const status = headers[':status'] as number || 502;
|
||||||
|
// Map headers
|
||||||
|
for (const [hk, hv] of Object.entries(headers)) {
|
||||||
|
if (!hk.startsWith(':') && hv) {
|
||||||
|
res.setHeader(hk, hv as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.statusCode = status;
|
||||||
|
h2Stream.pipe(res);
|
||||||
|
});
|
||||||
|
h2Stream.on('error', (err) => {
|
||||||
|
this.logger.error(`HTTP/2 proxy error: ${err.message}`);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.statusCode = 502;
|
||||||
|
res.end(`Bad Gateway: ${err.message}`);
|
||||||
|
} else {
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find target based on hostname
|
// Find target based on hostname
|
||||||
@ -275,4 +341,119 @@ export class RequestHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle HTTP/2 stream requests by proxying to HTTP/1 backends
|
||||||
|
*/
|
||||||
|
public async handleHttp2(stream: any, headers: any): Promise<void> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const method = headers[':method'] || 'GET';
|
||||||
|
const path = headers[':path'] || '/';
|
||||||
|
// If configured to proxy to backends over HTTP/2, use HTTP/2 client sessions
|
||||||
|
if (this.options.backendProtocol === 'http2') {
|
||||||
|
const authority = headers[':authority'] as string || '';
|
||||||
|
const host = authority.split(':')[0];
|
||||||
|
const fakeReq: any = { headers: { host }, method: headers[':method'], url: headers[':path'], socket: (stream.session as any).socket };
|
||||||
|
const proxyConfig = this.router.routeReq(fakeReq);
|
||||||
|
if (!proxyConfig) {
|
||||||
|
stream.respond({ ':status': 404 });
|
||||||
|
stream.end('Not Found');
|
||||||
|
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const destination = this.connectionPool.getNextTarget(proxyConfig.destinationIps, proxyConfig.destinationPorts[0]);
|
||||||
|
const key = `${destination.host}:${destination.port}`;
|
||||||
|
let session = this.h2Sessions.get(key);
|
||||||
|
if (!session || session.closed || (session as any).destroyed) {
|
||||||
|
session = plugins.http2.connect(`http://${destination.host}:${destination.port}`);
|
||||||
|
this.h2Sessions.set(key, session);
|
||||||
|
session.on('error', () => this.h2Sessions.delete(key));
|
||||||
|
session.on('close', () => this.h2Sessions.delete(key));
|
||||||
|
}
|
||||||
|
// Build headers for backend HTTP/2 request
|
||||||
|
const h2Headers: Record<string, any> = {
|
||||||
|
':method': headers[':method'],
|
||||||
|
':path': headers[':path'],
|
||||||
|
':authority': `${destination.host}:${destination.port}`
|
||||||
|
};
|
||||||
|
for (const [k, v] of Object.entries(headers)) {
|
||||||
|
if (!k.startsWith(':') && typeof v === 'string') {
|
||||||
|
h2Headers[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const h2Stream2 = session.request(h2Headers);
|
||||||
|
stream.pipe(h2Stream2);
|
||||||
|
h2Stream2.on('response', (hdrs: any) => {
|
||||||
|
// Map status and headers to client
|
||||||
|
const resp: Record<string, any> = { ':status': hdrs[':status'] as number };
|
||||||
|
for (const [hk, hv] of Object.entries(hdrs)) {
|
||||||
|
if (!hk.startsWith(':') && hv) resp[hk] = hv;
|
||||||
|
}
|
||||||
|
stream.respond(resp);
|
||||||
|
h2Stream2.pipe(stream);
|
||||||
|
});
|
||||||
|
h2Stream2.on('error', (err) => {
|
||||||
|
stream.respond({ ':status': 502 });
|
||||||
|
stream.end(`Bad Gateway: ${err.message}`);
|
||||||
|
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Determine host for routing
|
||||||
|
const authority = headers[':authority'] as string || '';
|
||||||
|
const host = authority.split(':')[0];
|
||||||
|
// Fake request object for routing
|
||||||
|
const fakeReq: any = { headers: { host }, method, url: path, socket: (stream.session as any).socket };
|
||||||
|
const proxyConfig = this.router.routeReq(fakeReq as any);
|
||||||
|
if (!proxyConfig) {
|
||||||
|
stream.respond({ ':status': 404 });
|
||||||
|
stream.end('Not Found');
|
||||||
|
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Select backend target
|
||||||
|
const destination = this.connectionPool.getNextTarget(
|
||||||
|
proxyConfig.destinationIps,
|
||||||
|
proxyConfig.destinationPorts[0]
|
||||||
|
);
|
||||||
|
// Build headers for HTTP/1 proxy
|
||||||
|
const outboundHeaders: Record<string,string> = {};
|
||||||
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
|
if (typeof key === 'string' && typeof value === 'string' && !key.startsWith(':')) {
|
||||||
|
outboundHeaders[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outboundHeaders.host && (proxyConfig as any).rewriteHostHeader) {
|
||||||
|
outboundHeaders.host = `${destination.host}:${destination.port}`;
|
||||||
|
}
|
||||||
|
// Create HTTP/1 proxy request
|
||||||
|
const proxyReq = plugins.http.request(
|
||||||
|
{ hostname: destination.host, port: destination.port, path, method, headers: outboundHeaders },
|
||||||
|
(proxyRes) => {
|
||||||
|
// Map status and headers back to HTTP/2
|
||||||
|
const responseHeaders: Record<string, number|string|string[]> = {};
|
||||||
|
for (const [k, v] of Object.entries(proxyRes.headers)) {
|
||||||
|
if (v !== undefined) responseHeaders[k] = v;
|
||||||
|
}
|
||||||
|
stream.respond({ ':status': proxyRes.statusCode || 500, ...responseHeaders });
|
||||||
|
proxyRes.pipe(stream);
|
||||||
|
stream.on('close', () => proxyReq.destroy());
|
||||||
|
stream.on('error', () => proxyReq.destroy());
|
||||||
|
if (this.metricsTracker) stream.on('end', () => this.metricsTracker.incrementRequestsServed());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
proxyReq.on('error', (err) => {
|
||||||
|
stream.respond({ ':status': 502 });
|
||||||
|
stream.end(`Bad Gateway: ${err.message}`);
|
||||||
|
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
|
||||||
|
});
|
||||||
|
// Pipe client stream to backend
|
||||||
|
stream.pipe(proxyReq);
|
||||||
|
} catch (err: any) {
|
||||||
|
stream.respond({ ':status': 500 });
|
||||||
|
stream.end('Internal Server Error');
|
||||||
|
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -20,6 +20,8 @@ export interface INetworkProxyOptions {
|
|||||||
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
|
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
|
||||||
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
|
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
|
||||||
useExternalPort80Handler?: boolean; // Flag to indicate using external Port80Handler
|
useExternalPort80Handler?: boolean; // Flag to indicate using external Port80Handler
|
||||||
|
// Protocol to use when proxying to backends: HTTP/1.x or HTTP/2
|
||||||
|
backendProtocol?: 'http1' | 'http2';
|
||||||
|
|
||||||
// ACME certificate management options
|
// ACME certificate management options
|
||||||
acme?: {
|
acme?: {
|
||||||
|
@ -5,9 +5,10 @@ import * as https from 'https';
|
|||||||
import * as net from 'net';
|
import * as net from 'net';
|
||||||
import * as tls from 'tls';
|
import * as tls from 'tls';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
|
import * as http2 from 'http2';
|
||||||
|
|
||||||
|
|
||||||
export { EventEmitter, http, https, net, tls, url };
|
export { EventEmitter, http, https, net, tls, url, http2 };
|
||||||
|
|
||||||
// tsclass scope
|
// tsclass scope
|
||||||
import * as tsclass from '@tsclass/tsclass';
|
import * as tsclass from '@tsclass/tsclass';
|
||||||
|
295
ts/redirect/classes.redirect.ts
Normal file
295
ts/redirect/classes.redirect.ts
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface RedirectRule {
|
||||||
|
/**
|
||||||
|
* Optional protocol to match (http or https). If not specified, matches both.
|
||||||
|
*/
|
||||||
|
fromProtocol?: 'http' | 'https';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional hostname pattern to match. Can use * as wildcard.
|
||||||
|
* If not specified, matches all hosts.
|
||||||
|
*/
|
||||||
|
fromHost?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional path prefix to match. If not specified, matches all paths.
|
||||||
|
*/
|
||||||
|
fromPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target protocol for the redirect (http or https)
|
||||||
|
*/
|
||||||
|
toProtocol: 'http' | 'https';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target hostname for the redirect. Can use $1, $2, etc. to reference
|
||||||
|
* captured groups from wildcard matches in fromHost.
|
||||||
|
*/
|
||||||
|
toHost: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional target path prefix. If not specified, keeps original path.
|
||||||
|
* Can use $path to reference the original path.
|
||||||
|
*/
|
||||||
|
toPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP status code for the redirect (301 for permanent, 302 for temporary)
|
||||||
|
*/
|
||||||
|
statusCode?: 301 | 302 | 307 | 308;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Redirect {
|
||||||
|
private httpServer?: plugins.http.Server;
|
||||||
|
private httpsServer?: plugins.https.Server;
|
||||||
|
private rules: RedirectRule[] = [];
|
||||||
|
private httpPort: number = 80;
|
||||||
|
private httpsPort: number = 443;
|
||||||
|
private sslOptions?: {
|
||||||
|
key: Buffer;
|
||||||
|
cert: Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Redirect instance
|
||||||
|
* @param options Configuration options
|
||||||
|
*/
|
||||||
|
constructor(options: {
|
||||||
|
httpPort?: number;
|
||||||
|
httpsPort?: number;
|
||||||
|
sslOptions?: {
|
||||||
|
key: Buffer;
|
||||||
|
cert: Buffer;
|
||||||
|
};
|
||||||
|
rules?: RedirectRule[];
|
||||||
|
} = {}) {
|
||||||
|
if (options.httpPort) this.httpPort = options.httpPort;
|
||||||
|
if (options.httpsPort) this.httpsPort = options.httpsPort;
|
||||||
|
if (options.sslOptions) this.sslOptions = options.sslOptions;
|
||||||
|
if (options.rules) this.rules = options.rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a redirect rule
|
||||||
|
*/
|
||||||
|
public addRule(rule: RedirectRule): void {
|
||||||
|
this.rules.push(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all redirect rules
|
||||||
|
*/
|
||||||
|
public clearRules(): void {
|
||||||
|
this.rules = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set SSL options for HTTPS redirects
|
||||||
|
*/
|
||||||
|
public setSslOptions(options: { key: Buffer; cert: Buffer }): void {
|
||||||
|
this.sslOptions = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a request according to the configured rules
|
||||||
|
*/
|
||||||
|
private handleRequest(
|
||||||
|
request: plugins.http.IncomingMessage,
|
||||||
|
response: plugins.http.ServerResponse,
|
||||||
|
protocol: 'http' | 'https'
|
||||||
|
): void {
|
||||||
|
const requestUrl = new URL(
|
||||||
|
request.url || '/',
|
||||||
|
`${protocol}://${request.headers.host || 'localhost'}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const host = requestUrl.hostname;
|
||||||
|
const path = requestUrl.pathname + requestUrl.search;
|
||||||
|
|
||||||
|
// Find matching rule
|
||||||
|
const matchedRule = this.findMatchingRule(protocol, host, path);
|
||||||
|
|
||||||
|
if (matchedRule) {
|
||||||
|
const targetUrl = this.buildTargetUrl(matchedRule, host, path);
|
||||||
|
|
||||||
|
console.log(`Redirecting ${protocol}://${host}${path} to ${targetUrl}`);
|
||||||
|
|
||||||
|
response.writeHead(matchedRule.statusCode || 302, {
|
||||||
|
Location: targetUrl,
|
||||||
|
});
|
||||||
|
response.end();
|
||||||
|
} else {
|
||||||
|
// No matching rule, send 404
|
||||||
|
response.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||||
|
response.end('Not Found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a matching redirect rule for the given request
|
||||||
|
*/
|
||||||
|
private findMatchingRule(
|
||||||
|
protocol: 'http' | 'https',
|
||||||
|
host: string,
|
||||||
|
path: string
|
||||||
|
): RedirectRule | undefined {
|
||||||
|
return this.rules.find((rule) => {
|
||||||
|
// Check protocol match
|
||||||
|
if (rule.fromProtocol && rule.fromProtocol !== protocol) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check host match
|
||||||
|
if (rule.fromHost) {
|
||||||
|
const pattern = rule.fromHost.replace(/\*/g, '(.*)');
|
||||||
|
const regex = new RegExp(`^${pattern}$`);
|
||||||
|
if (!regex.test(host)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check path match
|
||||||
|
if (rule.fromPath && !path.startsWith(rule.fromPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the target URL for a redirect
|
||||||
|
*/
|
||||||
|
private buildTargetUrl(rule: RedirectRule, originalHost: string, originalPath: string): string {
|
||||||
|
let targetHost = rule.toHost;
|
||||||
|
|
||||||
|
// Replace wildcards in host
|
||||||
|
if (rule.fromHost && rule.fromHost.includes('*')) {
|
||||||
|
const pattern = rule.fromHost.replace(/\*/g, '(.*)');
|
||||||
|
const regex = new RegExp(`^${pattern}$`);
|
||||||
|
const matches = originalHost.match(regex);
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
for (let i = 1; i < matches.length; i++) {
|
||||||
|
targetHost = targetHost.replace(`$${i}`, matches[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build target path
|
||||||
|
let targetPath = originalPath;
|
||||||
|
if (rule.toPath) {
|
||||||
|
if (rule.toPath.includes('$path')) {
|
||||||
|
// Replace $path with original path, optionally removing the fromPath prefix
|
||||||
|
const pathSuffix = rule.fromPath ?
|
||||||
|
originalPath.substring(rule.fromPath.length) :
|
||||||
|
originalPath;
|
||||||
|
|
||||||
|
targetPath = rule.toPath.replace('$path', pathSuffix);
|
||||||
|
} else {
|
||||||
|
targetPath = rule.toPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${rule.toProtocol}://${targetHost}${targetPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the redirect server(s)
|
||||||
|
*/
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
const tasks = [];
|
||||||
|
|
||||||
|
// Create and start HTTP server if we have a port
|
||||||
|
if (this.httpPort) {
|
||||||
|
this.httpServer = plugins.http.createServer((req, res) =>
|
||||||
|
this.handleRequest(req, res, 'http')
|
||||||
|
);
|
||||||
|
|
||||||
|
const httpStartPromise = new Promise<void>((resolve) => {
|
||||||
|
this.httpServer?.listen(this.httpPort, () => {
|
||||||
|
console.log(`HTTP redirect server started on port ${this.httpPort}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tasks.push(httpStartPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and start HTTPS server if we have SSL options and a port
|
||||||
|
if (this.httpsPort && this.sslOptions) {
|
||||||
|
this.httpsServer = plugins.https.createServer(this.sslOptions, (req, res) =>
|
||||||
|
this.handleRequest(req, res, 'https')
|
||||||
|
);
|
||||||
|
|
||||||
|
const httpsStartPromise = new Promise<void>((resolve) => {
|
||||||
|
this.httpsServer?.listen(this.httpsPort, () => {
|
||||||
|
console.log(`HTTPS redirect server started on port ${this.httpsPort}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tasks.push(httpsStartPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all servers to start
|
||||||
|
await Promise.all(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the redirect server(s)
|
||||||
|
*/
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
const tasks = [];
|
||||||
|
|
||||||
|
if (this.httpServer) {
|
||||||
|
const httpStopPromise = new Promise<void>((resolve) => {
|
||||||
|
this.httpServer?.close(() => {
|
||||||
|
console.log('HTTP redirect server stopped');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
tasks.push(httpStopPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.httpsServer) {
|
||||||
|
const httpsStopPromise = new Promise<void>((resolve) => {
|
||||||
|
this.httpsServer?.close(() => {
|
||||||
|
console.log('HTTPS redirect server stopped');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
tasks.push(httpsStopPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For backward compatibility
|
||||||
|
export class SslRedirect {
|
||||||
|
private redirect: Redirect;
|
||||||
|
port: number;
|
||||||
|
|
||||||
|
constructor(portArg: number) {
|
||||||
|
this.port = portArg;
|
||||||
|
this.redirect = new Redirect({
|
||||||
|
httpPort: portArg,
|
||||||
|
rules: [{
|
||||||
|
fromProtocol: 'http',
|
||||||
|
toProtocol: 'https',
|
||||||
|
toHost: '$1',
|
||||||
|
statusCode: 302
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
await this.redirect.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
await this.redirect.stop();
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user