Compare commits

...

12 Commits

Author SHA1 Message Date
2616b24d61 7.1.2
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Failing after 1m6s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-19 18:42:36 +00:00
46214f5380 fix(networkproxy/requesthandler): Improve HTTP/2 request handling and error management in the proxy request handler; add try-catch around routing and update header processing to support per-backend protocol overrides. 2025-04-19 18:42:36 +00:00
d8383311be 7.1.1
Some checks failed
Default (tags) / security (push) Successful in 23s
Default (tags) / test (push) Failing after 1m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-19 18:32:46 +00:00
578d11344f fix(commit-info): Update commit metadata and synchronize project configuration (no code changes) 2025-04-19 18:32:46 +00:00
ce3d0feb77 7.1.0
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 1m8s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-19 18:31:31 +00:00
04abab505b 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. 2025-04-19 18:31:10 +00:00
e69c55de3b 7.0.1
Some checks failed
Default (tags) / security (push) Successful in 41s
Default (tags) / test (push) Failing after 1m5s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-05 08:54:35 +00:00
9a9bcd2df0 fix(package.json): Update packageManager field in package.json to specify the pnpm version for improved reproducibility. 2025-04-05 08:54:34 +00:00
b27cb8988c 7.0.0
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 1m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-04 17:15:50 +00:00
0de7531e17 BREAKING CHANGE(redirect): Remove deprecated SSL redirect implementation and update exports to use the new redirect module 2025-04-04 17:15:50 +00:00
c0002fee38 6.0.1
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 1m8s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-25 22:35:36 +00:00
27f9b1eac1 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. 2025-03-25 22:35:36 +00:00
11 changed files with 640 additions and 126 deletions

View File

@ -1,5 +1,47 @@
# Changelog
## 2025-04-19 - 7.1.2 - fix(networkproxy/requesthandler)
Improve HTTP/2 request handling and error management in the proxy request handler; add try-catch around routing and update header processing to support per-backend protocol overrides.
- Wrapped the routing call (router.routeReq) in a try-catch block to better handle errors and missing host headers.
- Returns a 500 error and increments failure metrics if routing fails.
- Refactored HTTP/2 branch to copy all headers appropriately and map response headers into HTTP/1 response.
- Added support for per-backend protocol override via the new backendProtocol option in IReverseProxyConfig.
## 2025-04-19 - 7.1.1 - fix(commit-info)
Update commit metadata and synchronize project configuration (no code changes)
- Verified that all files remain unchanged
- Commit reflects a metadata or build system sync without functional modifications
## 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)
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.
- Renamed 'PortProxy' to 'SmartProxy' in diagrams, flow sequences, and descriptive text
- Updated code examples and installation instructions accordingly
- Corrected references from IPTables to NfTables for modern system support
## 2025-03-18 - 5.1.0 - feat(docs)
docs: replace IPTablesProxy references with NfTablesProxy in README and examples, updating configuration options and diagrams for advanced nftables features

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartproxy",
"version": "6.0.0",
"version": "7.1.2",
"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.",
"main": "dist_ts/index.js",
@ -83,5 +83,6 @@
"mongodb-memory-server",
"puppeteer"
]
}
},
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
}

154
readme.md
View File

@ -15,7 +15,7 @@ flowchart TB
direction TB
HTTP80[HTTP Port 80\nSslRedirect]
HTTPS443[HTTPS Port 443\nNetworkProxy]
PortProxy[TCP Port Proxy\nwith SNI routing]
SmartProxy[SmartProxy\nwith SNI routing]
NfTables[NfTablesProxy]
Router[ProxyRouter]
ACME[Port80Handler\nACME/Let's Encrypt]
@ -31,16 +31,16 @@ flowchart TB
Client -->|HTTP Request| HTTP80
HTTP80 -->|Redirect| Client
Client -->|HTTPS Request| HTTPS443
Client -->|TLS/TCP| PortProxy
Client -->|TLS/TCP| SmartProxy
HTTPS443 -->|Route Request| Router
Router -->|Proxy Request| Service1
Router -->|Proxy Request| Service2
PortProxy -->|Direct TCP| Service2
PortProxy -->|Direct TCP| Service3
SmartProxy -->|Direct TCP| Service2
SmartProxy -->|Direct TCP| Service3
NfTables -.->|Low-level forwarding| PortProxy
NfTables -.->|Low-level forwarding| SmartProxy
HTTP80 -.->|Challenge Response| ACME
ACME -.->|Generate/Manage| Certs
@ -51,7 +51,7 @@ flowchart TB
classDef client fill:#dfd,stroke:#333,stroke-width:2px;
class Client client;
class HTTP80,HTTPS443,PortProxy,IPTables,Router,ACME component;
class HTTP80,HTTPS443,SmartProxy,NfTables,Router,ACME component;
class Service1,Service2,Service3 backend;
```
@ -98,49 +98,49 @@ sequenceDiagram
end
```
### Port Proxy with SNI-based Routing
### SNI-based Connection Handling
This diagram illustrates how TCP connections with SNI (Server Name Indication) are processed and forwarded:
```mermaid
sequenceDiagram
participant Client
participant PortProxy
participant SmartProxy
participant Backend
Client->>PortProxy: TLS Connection
Client->>SmartProxy: TLS Connection
alt SNI Enabled
PortProxy->>Client: Accept Connection
Client->>PortProxy: TLS ClientHello with SNI
PortProxy->>PortProxy: Extract SNI Hostname
PortProxy->>PortProxy: Match Domain Config
PortProxy->>PortProxy: Validate Client IP
SmartProxy->>Client: Accept Connection
Client->>SmartProxy: TLS ClientHello with SNI
SmartProxy->>SmartProxy: Extract SNI Hostname
SmartProxy->>SmartProxy: Match Domain Config
SmartProxy->>SmartProxy: Validate Client IP
alt IP Allowed
PortProxy->>Backend: Forward Connection
Note over PortProxy,Backend: Bidirectional Data Flow
SmartProxy->>Backend: Forward Connection
Note over SmartProxy,Backend: Bidirectional Data Flow
else IP Rejected
PortProxy->>Client: Close Connection
SmartProxy->>Client: Close Connection
end
else Port-based Routing
PortProxy->>PortProxy: Match Port Range
PortProxy->>PortProxy: Find Domain Config
PortProxy->>PortProxy: Validate Client IP
SmartProxy->>SmartProxy: Match Port Range
SmartProxy->>SmartProxy: Find Domain Config
SmartProxy->>SmartProxy: Validate Client IP
alt IP Allowed
PortProxy->>Backend: Forward Connection
Note over PortProxy,Backend: Bidirectional Data Flow
SmartProxy->>Backend: Forward Connection
Note over SmartProxy,Backend: Bidirectional Data Flow
else IP Rejected
PortProxy->>Client: Close Connection
SmartProxy->>Client: Close Connection
end
end
loop Connection Active
PortProxy-->>PortProxy: Monitor Activity
PortProxy-->>PortProxy: Check Max Lifetime
SmartProxy-->>SmartProxy: Monitor Activity
SmartProxy-->>SmartProxy: Check Max Lifetime
alt Inactivity or Max Lifetime Exceeded
PortProxy->>Client: Close Connection
PortProxy->>Backend: Close Connection
SmartProxy->>Client: Close Connection
SmartProxy->>Backend: Close Connection
end
end
```
@ -192,12 +192,32 @@ sequenceDiagram
- **HTTPS Reverse Proxy** - Route traffic to backend services based on hostname with TLS termination
- **WebSocket Support** - Full WebSocket proxying with heartbeat monitoring
- **TCP Port Forwarding** - Advanced port forwarding with SNI inspection and domain-based routing
- **TCP Connection Handling** - Advanced connection handling with SNI inspection and domain-based routing
- **Enhanced TLS Handling** - Robust TLS handshake processing with improved certificate error handling
- **HTTP to HTTPS Redirection** - Automatically redirect HTTP requests to HTTPS
- **Let's Encrypt Integration** - Automatic certificate management using ACME protocol
- **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
- **Connection Management** - Intelligent connection tracking and cleanup with configurable timeouts
- **Browser Compatibility** - Optimized for modern browsers with fixes for common TLS handshake issues
@ -224,15 +244,16 @@ const proxy = new NetworkProxy({
const proxyConfigs = [
{
hostName: 'example.com',
destinationIp: '127.0.0.1',
destinationPort: 3000,
destinationIps: ['127.0.0.1'],
destinationPorts: [3000],
publicKey: 'your-cert-content',
privateKey: 'your-key-content'
privateKey: 'your-key-content',
rewriteHostHeader: true
},
{
hostName: 'api.example.com',
destinationIp: '127.0.0.1',
destinationPort: 4000,
destinationIps: ['127.0.0.1'],
destinationPorts: [4000],
publicKey: 'your-cert-content',
privateKey: 'your-key-content',
// Optional basic auth
@ -266,13 +287,13 @@ const redirector = new SslRedirect(80);
redirector.start();
```
### TCP Port Forwarding with Domain-based Routing
### TCP Connection Handling with Domain-based Routing
```typescript
import { PortProxy } from '@push.rocks/smartproxy';
import { SmartProxy } from '@push.rocks/smartproxy';
// Configure port proxy with domain-based routing
const portProxy = new PortProxy({
// Configure SmartProxy with domain-based routing
const smartProxy = new SmartProxy({
fromPort: 443,
toPort: 8443,
targetIP: 'localhost', // Default target host
@ -312,7 +333,7 @@ const portProxy = new PortProxy({
preserveSourceIP: true
});
portProxy.start();
smartProxy.start();
```
### NfTables Port Forwarding
@ -376,7 +397,13 @@ await basicProxy.start();
import { Port80Handler } from '@push.rocks/smartproxy';
// Create an ACME handler for Let's Encrypt
const acmeHandler = new Port80Handler();
const acmeHandler = new Port80Handler({
port: 80,
contactEmail: 'admin@example.com',
useProduction: true, // Use Let's Encrypt production servers (default is staging)
renewThresholdDays: 30, // Renew certificates 30 days before expiry
httpsRedirectPort: 443 // Redirect HTTP to HTTPS on this port
});
// Add domains to manage certificates for
acmeHandler.addDomain({
@ -407,8 +434,14 @@ acmeHandler.addDomain({
| Option | Description | Default |
|----------------|---------------------------------------------------|---------|
| `port` | Port to listen on for HTTPS connections | - |
| `maxConnections` | Maximum concurrent connections | 10000 |
| `keepAliveTimeout` | Keep-alive timeout in milliseconds | 60000 |
| `headersTimeout` | Headers timeout in milliseconds | 60000 |
| `logLevel` | Logging level ('error', 'warn', 'info', 'debug') | 'info' |
| `cors` | CORS configuration object | - |
| `rewriteHostHeader` | Whether to rewrite the Host header | false |
### PortProxy Settings
### SmartProxy Settings
| Option | Description | Default |
|---------------------------|--------------------------------------------------------|-------------|
@ -460,28 +493,11 @@ acmeHandler.addDomain({
| `qos` | Quality of Service options (object) | - |
| `netProxyIntegration` | NetworkProxy integration options (object) | - |
#### NfTablesProxy QoS Options
| Option | Description | Default |
|----------------------|---------------------------------------------------|---------|
| `enabled` | Enable Quality of Service features | false |
| `maxRate` | Maximum bandwidth rate (e.g. "10mbps") | - |
| `priority` | Traffic priority (1-10, 1 is highest) | - |
| `markConnections` | Mark connections for easier management | false |
#### NfTablesProxy NetworkProxy Integration Options
| Option | Description | Default |
|----------------------|---------------------------------------------------|---------|
| `enabled` | Enable NetworkProxy integration | false |
| `redirectLocalhost` | Redirect localhost traffic to NetworkProxy | false |
| `sslTerminationPort` | Port where NetworkProxy handles SSL termination | - |
## Advanced Features
### TLS Handshake Optimization
The enhanced `PortProxy` implementation includes significant improvements for TLS handshake handling:
The enhanced `SmartProxy` implementation includes significant improvements for TLS handshake handling:
- Robust SNI extraction with improved error handling
- Increased buffer size for complex TLS handshakes (10MB)
@ -492,7 +508,7 @@ The enhanced `PortProxy` implementation includes significant improvements for TL
```typescript
// Example configuration to solve Chrome certificate errors
const portProxy = new PortProxy({
const portProxy = new SmartProxy({
// ... other settings
initialDataTimeout: 60000, // Give browser more time for handshake
maxPendingDataSize: 10 * 1024 * 1024, // Larger buffer for complex handshakes
@ -502,7 +518,7 @@ const portProxy = new PortProxy({
### Connection Management and Monitoring
The `PortProxy` class includes built-in connection tracking and monitoring:
The `SmartProxy` class includes built-in connection tracking and monitoring:
- Automatic cleanup of idle connections with configurable timeouts
- Timeouts for connections that exceed maximum lifetime
@ -521,7 +537,7 @@ The `NetworkProxy` class provides WebSocket support with:
### SNI-based Routing
The `PortProxy` class can inspect the SNI (Server Name Indication) field in TLS handshakes to route connections based on the requested domain:
The `SmartProxy` class can inspect the SNI (Server Name Indication) field in TLS handshakes to route connections based on the requested domain:
- Multiple backend targets per domain
- Round-robin load balancing
@ -530,7 +546,7 @@ The `PortProxy` class can inspect the SNI (Server Name Indication) field in TLS
### Enhanced NfTables Management
The `NfTablesProxy` class offers advanced capabilities compared to the previous IPTablesProxy:
The `NfTablesProxy` class offers advanced capabilities:
- Support for multiple port ranges and individual ports
- More efficient IP filtering using nftables sets
@ -544,7 +560,7 @@ The `NfTablesProxy` class offers advanced capabilities compared to the previous
### Port80Handler with Glob Pattern Support
The `Port80Handler` class now includes support for glob pattern domain matching:
The `Port80Handler` class includes support for glob pattern domain matching:
- Supports wildcard domains like `*.example.com` for HTTP request routing
- Detects glob patterns and skips certificate issuance for them
@ -566,7 +582,7 @@ If you experience certificate errors in browsers, especially in Chrome, try thes
```typescript
// Configuration to fix Chrome certificate errors
const portProxy = new PortProxy({
const smartProxy = new SmartProxy({
// ... other settings
initialDataTimeout: 60000,
maxPendingDataSize: 10 * 1024 * 1024,
@ -585,14 +601,14 @@ For improved connection stability in high-traffic environments:
4. **Monitor Connection Statistics**: Enable detailed logging to track termination reasons
5. **Fine-tune Inactivity Checks**: Adjust `inactivityCheckInterval` based on your traffic patterns
### IPTables Troubleshooting
### NfTables Troubleshooting
If you're experiencing issues with IPTablesProxy:
If you're experiencing issues with NfTablesProxy:
1. **Enable Detailed Logging**: Set `enableLogging: true` to see all rule operations
2. **Force Clean Slate**: Use `forceCleanSlate: true` to remove any lingering rules
3. **Use Custom Chains**: Enable `addJumpRule: true` for cleaner rule management
4. **Check Permissions**: Ensure your process has sufficient permissions to modify iptables
3. **Use IP Sets**: Enable `useIPSets: true` for cleaner rule management
4. **Check Permissions**: Ensure your process has sufficient permissions to modify nftables
5. **Verify IPv6 Support**: If using `ipv6Support: true`, ensure ip6tables is available
## License and Legal Information

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '5.1.0',
version: '7.1.2',
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.'
}

View File

@ -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;
}
}

View File

@ -1,7 +1,7 @@
export * from './nfttablesproxy/classes.nftablesproxy.js';
export * from './networkproxy/classes.np.networkproxy.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.pp.snihandler.js';
export * from './smartproxy/classes.pp.interfaces.js';

View File

@ -16,8 +16,8 @@ export class NetworkProxy implements IMetricsTracker {
public options: INetworkProxyOptions;
public proxyConfigs: IReverseProxyConfig[] = [];
// Server instances
public httpsServer: plugins.https.Server;
// Server instances (HTTP/2 with HTTP/1 fallback)
public httpsServer: any;
// Core components
private certificateManager: CertificateManager;
@ -66,6 +66,8 @@ export class NetworkProxy implements IMetricsTracker {
connectionPoolSize: optionsArg.connectionPoolSize || 50,
portProxyIntegration: optionsArg.portProxyIntegration || false,
useExternalPort80Handler: optionsArg.useExternalPort80Handler || false,
// Backend protocol (http1 or http2)
backendProtocol: optionsArg.backendProtocol || 'http1',
// Default ACME options
acme: {
enabled: optionsArg.acme?.enabled || false,
@ -185,33 +187,35 @@ export class NetworkProxy implements IMetricsTracker {
await this.certificateManager.initializePort80Handler();
}
// Create the HTTPS server
this.httpsServer = plugins.https.createServer(
// Create HTTP/2 server with HTTP/1 fallback
this.httpsServer = plugins.http2.createSecureServer(
{
key: this.certificateManager.getDefaultCertificates().key,
cert: this.certificateManager.getDefaultCertificates().cert,
SNICallback: (domain, cb) => this.certificateManager.handleSNI(domain, cb)
},
(req, res) => this.requestHandler.handleRequest(req, res)
allowHTTP1: true,
ALPNProtocols: ['h2', 'http/1.1']
}
);
// Configure server timeouts
this.httpsServer.keepAliveTimeout = this.options.keepAliveTimeout;
this.httpsServer.headersTimeout = this.options.headersTimeout;
// Setup connection tracking
// Track raw TCP connections for metrics and limits
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);
// Setup WebSocket support
// Setup WebSocket support on HTTP/1 fallback
this.webSocketHandler.initialize(this.httpsServer);
// Start metrics collection
// Start metrics logging
this.setupMetricsCollection();
// Setup connection pool cleanup interval
// Start periodic connection pool cleanup
this.connectionPoolCleanupInterval = this.connectionPool.setupPeriodicCleanup();
// Start the server

View File

@ -18,6 +18,8 @@ export class RequestHandler {
private defaultHeaders: { [key: string]: string } = {};
private logger: ILogger;
private metricsTracker: IMetricsTracker | null = null;
// HTTP/2 client sessions for backend proxying
private h2Sessions: Map<string, plugins.http2.ClientHttp2Session> = new Map();
constructor(
private options: INetworkProxyOptions,
@ -131,6 +133,69 @@ export class RequestHandler {
// Apply default headers
this.applyDefaultHeaders(res);
// Determine routing configuration
let proxyConfig: IReverseProxyConfig | undefined;
try {
proxyConfig = this.router.routeReq(req);
} catch (err) {
this.logger.error('Error routing request', err);
res.statusCode = 500;
res.end('Internal Server Error');
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
return;
}
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 protocol to backend (per-domain override or global)
const backendProto = proxyConfig.backendProtocol || this.options.backendProtocol;
if (backendProto === 'http2') {
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 HTTP/2 request
const hdrs: Record<string, any> = {
':method': req.method,
':path': req.url,
':authority': `${destination.host}:${destination.port}`
};
for (const [hk, hv] of Object.entries(req.headers)) {
if (typeof hv === 'string') hdrs[hk] = hv;
}
const h2Stream = session.request(hdrs);
req.pipe(h2Stream);
h2Stream.on('response', (hdrs2: any) => {
const status = (hdrs2[':status'] as number) || 502;
res.statusCode = status;
// Copy headers from HTTP/2 response to HTTP/1 response
for (const [hk, hv] of Object.entries(hdrs2)) {
if (!hk.startsWith(':') && hv != null) {
res.setHeader(hk, hv as string | string[]);
}
}
h2Stream.pipe(res);
});
h2Stream.on('error', (err) => {
res.statusCode = 502;
res.end(`Bad Gateway: ${err.message}`);
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
});
return;
}
try {
// Find target based on hostname
const proxyConfig = this.router.routeReq(req);
@ -275,4 +340,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();
}
}
}

View File

@ -20,6 +20,8 @@ export interface INetworkProxyOptions {
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
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?: {
@ -58,6 +60,11 @@ export interface IReverseProxyConfig {
pass: string;
};
rewriteHostHeader?: boolean;
/**
* Protocol to use when proxying to this backend: 'http1' or 'http2'.
* Overrides the global backendProtocol option if set.
*/
backendProtocol?: 'http1' | 'http2';
}
/**

View File

@ -5,9 +5,10 @@ import * as https from 'https';
import * as net from 'net';
import * as tls from 'tls';
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
import * as tsclass from '@tsclass/tsclass';

View 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();
}
}