@push.rocks/smartproxy 5.0.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.pp.interfaces.d.ts +23 -0
- package/dist_ts/classes.pp.networkproxybridge.d.ts +15 -1
- package/dist_ts/classes.pp.networkproxybridge.js +116 -21
- package/dist_ts/classes.pp.portproxy.d.ts +20 -4
- package/dist_ts/classes.pp.portproxy.js +321 -22
- package/dist_ts/index.d.ts +6 -6
- package/dist_ts/index.js +7 -7
- package/dist_ts/networkproxy/classes.np.certificatemanager.d.ts +77 -0
- package/dist_ts/networkproxy/classes.np.certificatemanager.js +354 -0
- package/dist_ts/networkproxy/classes.np.connectionpool.d.ts +47 -0
- package/dist_ts/networkproxy/classes.np.connectionpool.js +210 -0
- package/dist_ts/networkproxy/classes.np.networkproxy.d.ts +117 -0
- package/dist_ts/networkproxy/classes.np.networkproxy.js +375 -0
- package/dist_ts/networkproxy/classes.np.requesthandler.d.ts +51 -0
- package/dist_ts/networkproxy/classes.np.requesthandler.js +210 -0
- package/dist_ts/networkproxy/classes.np.types.d.ts +82 -0
- package/dist_ts/networkproxy/classes.np.types.js +35 -0
- package/dist_ts/networkproxy/classes.np.websockethandler.d.ts +38 -0
- package/dist_ts/networkproxy/classes.np.websockethandler.js +188 -0
- package/dist_ts/networkproxy/index.d.ts +6 -0
- package/dist_ts/networkproxy/index.js +8 -0
- package/dist_ts/nfttablesproxy/classes.nftablesproxy.d.ts +219 -0
- package/dist_ts/nfttablesproxy/classes.nftablesproxy.js +1542 -0
- package/dist_ts/port80handler/classes.port80handler.d.ts +260 -0
- package/dist_ts/port80handler/classes.port80handler.js +928 -0
- package/dist_ts/smartproxy/classes.pp.connectionhandler.d.ts +39 -0
- package/dist_ts/smartproxy/classes.pp.connectionhandler.js +754 -0
- package/dist_ts/smartproxy/classes.pp.connectionmanager.d.ts +78 -0
- package/dist_ts/smartproxy/classes.pp.connectionmanager.js +378 -0
- package/dist_ts/smartproxy/classes.pp.domainconfigmanager.d.ts +55 -0
- package/dist_ts/smartproxy/classes.pp.domainconfigmanager.js +103 -0
- package/dist_ts/smartproxy/classes.pp.interfaces.d.ts +133 -0
- package/dist_ts/smartproxy/classes.pp.interfaces.js +2 -0
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +57 -0
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +306 -0
- package/dist_ts/smartproxy/classes.pp.portrangemanager.d.ts +56 -0
- package/dist_ts/smartproxy/classes.pp.portrangemanager.js +179 -0
- package/dist_ts/smartproxy/classes.pp.securitymanager.d.ts +47 -0
- package/dist_ts/smartproxy/classes.pp.securitymanager.js +126 -0
- package/dist_ts/smartproxy/classes.pp.snihandler.d.ts +153 -0
- package/dist_ts/smartproxy/classes.pp.snihandler.js +1053 -0
- package/dist_ts/smartproxy/classes.pp.timeoutmanager.d.ts +47 -0
- package/dist_ts/smartproxy/classes.pp.timeoutmanager.js +154 -0
- package/dist_ts/smartproxy/classes.pp.tlsalert.d.ts +149 -0
- package/dist_ts/smartproxy/classes.pp.tlsalert.js +225 -0
- package/dist_ts/smartproxy/classes.pp.tlsmanager.d.ts +57 -0
- package/dist_ts/smartproxy/classes.pp.tlsmanager.js +132 -0
- package/dist_ts/smartproxy/classes.smartproxy.d.ts +64 -0
- package/dist_ts/smartproxy/classes.smartproxy.js +567 -0
- package/package.json +1 -1
- package/readme.md +77 -27
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +6 -6
- package/ts/networkproxy/classes.np.certificatemanager.ts +398 -0
- package/ts/networkproxy/classes.np.connectionpool.ts +241 -0
- package/ts/networkproxy/classes.np.networkproxy.ts +469 -0
- package/ts/networkproxy/classes.np.requesthandler.ts +278 -0
- package/ts/networkproxy/classes.np.types.ts +123 -0
- package/ts/networkproxy/classes.np.websockethandler.ts +226 -0
- package/ts/networkproxy/index.ts +7 -0
- package/ts/{classes.port80handler.ts → port80handler/classes.port80handler.ts} +249 -1
- package/ts/{classes.pp.connectionhandler.ts → smartproxy/classes.pp.connectionhandler.ts} +1 -1
- package/ts/{classes.pp.connectionmanager.ts → smartproxy/classes.pp.connectionmanager.ts} +1 -1
- package/ts/{classes.pp.domainconfigmanager.ts → smartproxy/classes.pp.domainconfigmanager.ts} +1 -1
- package/ts/{classes.pp.interfaces.ts → smartproxy/classes.pp.interfaces.ts} +31 -5
- package/ts/{classes.pp.networkproxybridge.ts → smartproxy/classes.pp.networkproxybridge.ts} +129 -28
- package/ts/{classes.pp.securitymanager.ts → smartproxy/classes.pp.securitymanager.ts} +1 -1
- package/ts/{classes.pp.tlsmanager.ts → smartproxy/classes.pp.tlsmanager.ts} +1 -1
- package/ts/smartproxy/classes.smartproxy.ts +679 -0
- package/ts/classes.networkproxy.ts +0 -1730
- package/ts/classes.pp.acmemanager.ts +0 -149
- package/ts/classes.pp.portproxy.ts +0 -344
- /package/ts/{classes.nftablesproxy.ts → nfttablesproxy/classes.nftablesproxy.ts} +0 -0
- /package/ts/{classes.pp.portrangemanager.ts → smartproxy/classes.pp.portrangemanager.ts} +0 -0
- /package/ts/{classes.pp.snihandler.ts → smartproxy/classes.pp.snihandler.ts} +0 -0
- /package/ts/{classes.pp.timeoutmanager.ts → smartproxy/classes.pp.timeoutmanager.ts} +0 -0
- /package/ts/{classes.pp.tlsalert.ts → smartproxy/classes.pp.tlsalert.ts} +0 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { type INetworkProxyOptions, type IConnectionEntry, type ILogger, createLogger } from './classes.np.types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Manages a pool of backend connections for efficient reuse
|
|
6
|
+
*/
|
|
7
|
+
export class ConnectionPool {
|
|
8
|
+
private connectionPool: Map<string, Array<IConnectionEntry>> = new Map();
|
|
9
|
+
private roundRobinPositions: Map<string, number> = new Map();
|
|
10
|
+
private logger: ILogger;
|
|
11
|
+
|
|
12
|
+
constructor(private options: INetworkProxyOptions) {
|
|
13
|
+
this.logger = createLogger(options.logLevel || 'info');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get a connection from the pool or create a new one
|
|
18
|
+
*/
|
|
19
|
+
public getConnection(host: string, port: number): Promise<plugins.net.Socket> {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const poolKey = `${host}:${port}`;
|
|
22
|
+
const connectionList = this.connectionPool.get(poolKey) || [];
|
|
23
|
+
|
|
24
|
+
// Look for an idle connection
|
|
25
|
+
const idleConnectionIndex = connectionList.findIndex(c => c.isIdle);
|
|
26
|
+
|
|
27
|
+
if (idleConnectionIndex >= 0) {
|
|
28
|
+
// Get existing connection from pool
|
|
29
|
+
const connection = connectionList[idleConnectionIndex];
|
|
30
|
+
connection.isIdle = false;
|
|
31
|
+
connection.lastUsed = Date.now();
|
|
32
|
+
this.logger.debug(`Reusing connection from pool for ${poolKey}`);
|
|
33
|
+
|
|
34
|
+
// Update the pool
|
|
35
|
+
this.connectionPool.set(poolKey, connectionList);
|
|
36
|
+
|
|
37
|
+
resolve(connection.socket);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// No idle connection available, create a new one if pool isn't full
|
|
42
|
+
const poolSize = this.options.connectionPoolSize || 50;
|
|
43
|
+
if (connectionList.length < poolSize) {
|
|
44
|
+
this.logger.debug(`Creating new connection to ${host}:${port}`);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const socket = plugins.net.connect({
|
|
48
|
+
host,
|
|
49
|
+
port,
|
|
50
|
+
keepAlive: true,
|
|
51
|
+
keepAliveInitialDelay: 30000 // 30 seconds
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
socket.once('connect', () => {
|
|
55
|
+
// Add to connection pool
|
|
56
|
+
const connection = {
|
|
57
|
+
socket,
|
|
58
|
+
lastUsed: Date.now(),
|
|
59
|
+
isIdle: false
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
connectionList.push(connection);
|
|
63
|
+
this.connectionPool.set(poolKey, connectionList);
|
|
64
|
+
|
|
65
|
+
// Setup cleanup when the connection is closed
|
|
66
|
+
socket.once('close', () => {
|
|
67
|
+
const idx = connectionList.findIndex(c => c.socket === socket);
|
|
68
|
+
if (idx >= 0) {
|
|
69
|
+
connectionList.splice(idx, 1);
|
|
70
|
+
this.connectionPool.set(poolKey, connectionList);
|
|
71
|
+
this.logger.debug(`Removed closed connection from pool for ${poolKey}`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
resolve(socket);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
socket.once('error', (err) => {
|
|
79
|
+
this.logger.error(`Error creating connection to ${host}:${port}`, err);
|
|
80
|
+
reject(err);
|
|
81
|
+
});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
this.logger.error(`Failed to create connection to ${host}:${port}`, err);
|
|
84
|
+
reject(err);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
// Pool is full, wait for an idle connection or reject
|
|
88
|
+
this.logger.warn(`Connection pool for ${poolKey} is full (${connectionList.length})`);
|
|
89
|
+
reject(new Error(`Connection pool for ${poolKey} is full`));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Return a connection to the pool for reuse
|
|
96
|
+
*/
|
|
97
|
+
public returnConnection(socket: plugins.net.Socket, host: string, port: number): void {
|
|
98
|
+
const poolKey = `${host}:${port}`;
|
|
99
|
+
const connectionList = this.connectionPool.get(poolKey) || [];
|
|
100
|
+
|
|
101
|
+
// Find this connection in the pool
|
|
102
|
+
const connectionIndex = connectionList.findIndex(c => c.socket === socket);
|
|
103
|
+
|
|
104
|
+
if (connectionIndex >= 0) {
|
|
105
|
+
// Mark as idle and update last used time
|
|
106
|
+
connectionList[connectionIndex].isIdle = true;
|
|
107
|
+
connectionList[connectionIndex].lastUsed = Date.now();
|
|
108
|
+
|
|
109
|
+
this.logger.debug(`Returned connection to pool for ${poolKey}`);
|
|
110
|
+
} else {
|
|
111
|
+
this.logger.warn(`Attempted to return unknown connection to pool for ${poolKey}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Cleanup the connection pool by removing idle connections
|
|
117
|
+
* or reducing pool size if it exceeds the configured maximum
|
|
118
|
+
*/
|
|
119
|
+
public cleanupConnectionPool(): void {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const idleTimeout = this.options.keepAliveTimeout || 120000; // 2 minutes default
|
|
122
|
+
|
|
123
|
+
for (const [host, connections] of this.connectionPool.entries()) {
|
|
124
|
+
// Sort by last used time (oldest first)
|
|
125
|
+
connections.sort((a, b) => a.lastUsed - b.lastUsed);
|
|
126
|
+
|
|
127
|
+
// Remove idle connections older than the idle timeout
|
|
128
|
+
let removed = 0;
|
|
129
|
+
while (connections.length > 0) {
|
|
130
|
+
const connection = connections[0];
|
|
131
|
+
|
|
132
|
+
// Remove if idle and exceeds timeout, or if pool is too large
|
|
133
|
+
if ((connection.isIdle && now - connection.lastUsed > idleTimeout) ||
|
|
134
|
+
connections.length > (this.options.connectionPoolSize || 50)) {
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
if (!connection.socket.destroyed) {
|
|
138
|
+
connection.socket.end();
|
|
139
|
+
connection.socket.destroy();
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
this.logger.error(`Error destroying pooled connection to ${host}`, err);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
connections.shift(); // Remove from pool
|
|
146
|
+
removed++;
|
|
147
|
+
} else {
|
|
148
|
+
break; // Stop removing if we've reached active or recent connections
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (removed > 0) {
|
|
153
|
+
this.logger.debug(`Removed ${removed} idle connections from pool for ${host}, ${connections.length} remaining`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Update the pool with the remaining connections
|
|
157
|
+
if (connections.length === 0) {
|
|
158
|
+
this.connectionPool.delete(host);
|
|
159
|
+
} else {
|
|
160
|
+
this.connectionPool.set(host, connections);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Close all connections in the pool
|
|
167
|
+
*/
|
|
168
|
+
public closeAllConnections(): void {
|
|
169
|
+
for (const [host, connections] of this.connectionPool.entries()) {
|
|
170
|
+
this.logger.debug(`Closing ${connections.length} connections to ${host}`);
|
|
171
|
+
|
|
172
|
+
for (const connection of connections) {
|
|
173
|
+
try {
|
|
174
|
+
if (!connection.socket.destroyed) {
|
|
175
|
+
connection.socket.end();
|
|
176
|
+
connection.socket.destroy();
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
this.logger.error(`Error closing connection to ${host}:`, error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.connectionPool.clear();
|
|
185
|
+
this.roundRobinPositions.clear();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get load balancing target using round-robin
|
|
190
|
+
*/
|
|
191
|
+
public getNextTarget(targets: string[], port: number): { host: string, port: number } {
|
|
192
|
+
const targetKey = targets.join(',');
|
|
193
|
+
|
|
194
|
+
// Initialize position if not exists
|
|
195
|
+
if (!this.roundRobinPositions.has(targetKey)) {
|
|
196
|
+
this.roundRobinPositions.set(targetKey, 0);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Get current position and increment for next time
|
|
200
|
+
const currentPosition = this.roundRobinPositions.get(targetKey)!;
|
|
201
|
+
const nextPosition = (currentPosition + 1) % targets.length;
|
|
202
|
+
this.roundRobinPositions.set(targetKey, nextPosition);
|
|
203
|
+
|
|
204
|
+
// Return the selected target
|
|
205
|
+
return {
|
|
206
|
+
host: targets[currentPosition],
|
|
207
|
+
port
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Gets the connection pool status
|
|
213
|
+
*/
|
|
214
|
+
public getPoolStatus(): Record<string, { total: number, idle: number }> {
|
|
215
|
+
return Object.fromEntries(
|
|
216
|
+
Array.from(this.connectionPool.entries()).map(([host, connections]) => [
|
|
217
|
+
host,
|
|
218
|
+
{
|
|
219
|
+
total: connections.length,
|
|
220
|
+
idle: connections.filter(c => c.isIdle).length
|
|
221
|
+
}
|
|
222
|
+
])
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Setup a periodic cleanup task
|
|
228
|
+
*/
|
|
229
|
+
public setupPeriodicCleanup(interval: number = 60000): NodeJS.Timeout {
|
|
230
|
+
const timer = setInterval(() => {
|
|
231
|
+
this.cleanupConnectionPool();
|
|
232
|
+
}, interval);
|
|
233
|
+
|
|
234
|
+
// Don't prevent process exit
|
|
235
|
+
if (timer.unref) {
|
|
236
|
+
timer.unref();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return timer;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { type INetworkProxyOptions, type ILogger, createLogger, type IReverseProxyConfig } from './classes.np.types.js';
|
|
3
|
+
import { CertificateManager } from './classes.np.certificatemanager.js';
|
|
4
|
+
import { ConnectionPool } from './classes.np.connectionpool.js';
|
|
5
|
+
import { RequestHandler, type IMetricsTracker } from './classes.np.requesthandler.js';
|
|
6
|
+
import { WebSocketHandler } from './classes.np.websockethandler.js';
|
|
7
|
+
import { ProxyRouter } from '../classes.router.js';
|
|
8
|
+
import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* NetworkProxy provides a reverse proxy with TLS termination, WebSocket support,
|
|
12
|
+
* automatic certificate management, and high-performance connection pooling.
|
|
13
|
+
*/
|
|
14
|
+
export class NetworkProxy implements IMetricsTracker {
|
|
15
|
+
// Configuration
|
|
16
|
+
public options: INetworkProxyOptions;
|
|
17
|
+
public proxyConfigs: IReverseProxyConfig[] = [];
|
|
18
|
+
|
|
19
|
+
// Server instances
|
|
20
|
+
public httpsServer: plugins.https.Server;
|
|
21
|
+
|
|
22
|
+
// Core components
|
|
23
|
+
private certificateManager: CertificateManager;
|
|
24
|
+
private connectionPool: ConnectionPool;
|
|
25
|
+
private requestHandler: RequestHandler;
|
|
26
|
+
private webSocketHandler: WebSocketHandler;
|
|
27
|
+
private router = new ProxyRouter();
|
|
28
|
+
|
|
29
|
+
// State tracking
|
|
30
|
+
public socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
|
|
31
|
+
public activeContexts: Set<string> = new Set();
|
|
32
|
+
public connectedClients: number = 0;
|
|
33
|
+
public startTime: number = 0;
|
|
34
|
+
public requestsServed: number = 0;
|
|
35
|
+
public failedRequests: number = 0;
|
|
36
|
+
|
|
37
|
+
// Tracking for PortProxy integration
|
|
38
|
+
private portProxyConnections: number = 0;
|
|
39
|
+
private tlsTerminatedConnections: number = 0;
|
|
40
|
+
|
|
41
|
+
// Timers
|
|
42
|
+
private metricsInterval: NodeJS.Timeout;
|
|
43
|
+
private connectionPoolCleanupInterval: NodeJS.Timeout;
|
|
44
|
+
|
|
45
|
+
// Logger
|
|
46
|
+
private logger: ILogger;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a new NetworkProxy instance
|
|
50
|
+
*/
|
|
51
|
+
constructor(optionsArg: INetworkProxyOptions) {
|
|
52
|
+
// Set default options
|
|
53
|
+
this.options = {
|
|
54
|
+
port: optionsArg.port,
|
|
55
|
+
maxConnections: optionsArg.maxConnections || 10000,
|
|
56
|
+
keepAliveTimeout: optionsArg.keepAliveTimeout || 120000, // 2 minutes
|
|
57
|
+
headersTimeout: optionsArg.headersTimeout || 60000, // 1 minute
|
|
58
|
+
logLevel: optionsArg.logLevel || 'info',
|
|
59
|
+
cors: optionsArg.cors || {
|
|
60
|
+
allowOrigin: '*',
|
|
61
|
+
allowMethods: 'GET, POST, PUT, DELETE, OPTIONS',
|
|
62
|
+
allowHeaders: 'Content-Type, Authorization',
|
|
63
|
+
maxAge: 86400
|
|
64
|
+
},
|
|
65
|
+
// Defaults for PortProxy integration
|
|
66
|
+
connectionPoolSize: optionsArg.connectionPoolSize || 50,
|
|
67
|
+
portProxyIntegration: optionsArg.portProxyIntegration || false,
|
|
68
|
+
useExternalPort80Handler: optionsArg.useExternalPort80Handler || false,
|
|
69
|
+
// Default ACME options
|
|
70
|
+
acme: {
|
|
71
|
+
enabled: optionsArg.acme?.enabled || false,
|
|
72
|
+
port: optionsArg.acme?.port || 80,
|
|
73
|
+
contactEmail: optionsArg.acme?.contactEmail || 'admin@example.com',
|
|
74
|
+
useProduction: optionsArg.acme?.useProduction || false, // Default to staging for safety
|
|
75
|
+
renewThresholdDays: optionsArg.acme?.renewThresholdDays || 30,
|
|
76
|
+
autoRenew: optionsArg.acme?.autoRenew !== false, // Default to true
|
|
77
|
+
certificateStore: optionsArg.acme?.certificateStore || './certs',
|
|
78
|
+
skipConfiguredCerts: optionsArg.acme?.skipConfiguredCerts || false
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Initialize logger
|
|
83
|
+
this.logger = createLogger(this.options.logLevel);
|
|
84
|
+
|
|
85
|
+
// Initialize components
|
|
86
|
+
this.certificateManager = new CertificateManager(this.options);
|
|
87
|
+
this.connectionPool = new ConnectionPool(this.options);
|
|
88
|
+
this.requestHandler = new RequestHandler(this.options, this.connectionPool, this.router);
|
|
89
|
+
this.webSocketHandler = new WebSocketHandler(this.options, this.connectionPool, this.router);
|
|
90
|
+
|
|
91
|
+
// Connect request handler to this metrics tracker
|
|
92
|
+
this.requestHandler.setMetricsTracker(this);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Implements IMetricsTracker interface to increment request counters
|
|
97
|
+
*/
|
|
98
|
+
public incrementRequestsServed(): void {
|
|
99
|
+
this.requestsServed++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Implements IMetricsTracker interface to increment failed request counters
|
|
104
|
+
*/
|
|
105
|
+
public incrementFailedRequests(): void {
|
|
106
|
+
this.failedRequests++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns the port number this NetworkProxy is listening on
|
|
111
|
+
* Useful for PortProxy to determine where to forward connections
|
|
112
|
+
*/
|
|
113
|
+
public getListeningPort(): number {
|
|
114
|
+
return this.options.port;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Updates the server capacity settings
|
|
119
|
+
* @param maxConnections Maximum number of simultaneous connections
|
|
120
|
+
* @param keepAliveTimeout Keep-alive timeout in milliseconds
|
|
121
|
+
* @param connectionPoolSize Size of the connection pool per backend
|
|
122
|
+
*/
|
|
123
|
+
public updateCapacity(maxConnections?: number, keepAliveTimeout?: number, connectionPoolSize?: number): void {
|
|
124
|
+
if (maxConnections !== undefined) {
|
|
125
|
+
this.options.maxConnections = maxConnections;
|
|
126
|
+
this.logger.info(`Updated max connections to ${maxConnections}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (keepAliveTimeout !== undefined) {
|
|
130
|
+
this.options.keepAliveTimeout = keepAliveTimeout;
|
|
131
|
+
|
|
132
|
+
if (this.httpsServer) {
|
|
133
|
+
this.httpsServer.keepAliveTimeout = keepAliveTimeout;
|
|
134
|
+
this.logger.info(`Updated keep-alive timeout to ${keepAliveTimeout}ms`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (connectionPoolSize !== undefined) {
|
|
139
|
+
this.options.connectionPoolSize = connectionPoolSize;
|
|
140
|
+
this.logger.info(`Updated connection pool size to ${connectionPoolSize}`);
|
|
141
|
+
|
|
142
|
+
// Clean up excess connections in the pool
|
|
143
|
+
this.connectionPool.cleanupConnectionPool();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns current server metrics
|
|
149
|
+
* Useful for PortProxy to determine which NetworkProxy to use for load balancing
|
|
150
|
+
*/
|
|
151
|
+
public getMetrics(): any {
|
|
152
|
+
return {
|
|
153
|
+
activeConnections: this.connectedClients,
|
|
154
|
+
totalRequests: this.requestsServed,
|
|
155
|
+
failedRequests: this.failedRequests,
|
|
156
|
+
portProxyConnections: this.portProxyConnections,
|
|
157
|
+
tlsTerminatedConnections: this.tlsTerminatedConnections,
|
|
158
|
+
connectionPoolSize: this.connectionPool.getPoolStatus(),
|
|
159
|
+
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
|
160
|
+
memoryUsage: process.memoryUsage(),
|
|
161
|
+
activeWebSockets: this.webSocketHandler.getConnectionInfo().activeConnections
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Sets an external Port80Handler for certificate management
|
|
167
|
+
* This allows the NetworkProxy to use a centrally managed Port80Handler
|
|
168
|
+
* instead of creating its own
|
|
169
|
+
*
|
|
170
|
+
* @param handler The Port80Handler instance to use
|
|
171
|
+
*/
|
|
172
|
+
public setExternalPort80Handler(handler: Port80Handler): void {
|
|
173
|
+
// Connect it to the certificate manager
|
|
174
|
+
this.certificateManager.setExternalPort80Handler(handler);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Starts the proxy server
|
|
179
|
+
*/
|
|
180
|
+
public async start(): Promise<void> {
|
|
181
|
+
this.startTime = Date.now();
|
|
182
|
+
|
|
183
|
+
// Initialize Port80Handler if enabled and not using external handler
|
|
184
|
+
if (this.options.acme?.enabled && !this.options.useExternalPort80Handler) {
|
|
185
|
+
await this.certificateManager.initializePort80Handler();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Create the HTTPS server
|
|
189
|
+
this.httpsServer = plugins.https.createServer(
|
|
190
|
+
{
|
|
191
|
+
key: this.certificateManager.getDefaultCertificates().key,
|
|
192
|
+
cert: this.certificateManager.getDefaultCertificates().cert,
|
|
193
|
+
SNICallback: (domain, cb) => this.certificateManager.handleSNI(domain, cb)
|
|
194
|
+
},
|
|
195
|
+
(req, res) => this.requestHandler.handleRequest(req, res)
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Configure server timeouts
|
|
199
|
+
this.httpsServer.keepAliveTimeout = this.options.keepAliveTimeout;
|
|
200
|
+
this.httpsServer.headersTimeout = this.options.headersTimeout;
|
|
201
|
+
|
|
202
|
+
// Setup connection tracking
|
|
203
|
+
this.setupConnectionTracking();
|
|
204
|
+
|
|
205
|
+
// Share HTTPS server with certificate manager
|
|
206
|
+
this.certificateManager.setHttpsServer(this.httpsServer);
|
|
207
|
+
|
|
208
|
+
// Setup WebSocket support
|
|
209
|
+
this.webSocketHandler.initialize(this.httpsServer);
|
|
210
|
+
|
|
211
|
+
// Start metrics collection
|
|
212
|
+
this.setupMetricsCollection();
|
|
213
|
+
|
|
214
|
+
// Setup connection pool cleanup interval
|
|
215
|
+
this.connectionPoolCleanupInterval = this.connectionPool.setupPeriodicCleanup();
|
|
216
|
+
|
|
217
|
+
// Start the server
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
this.httpsServer.listen(this.options.port, () => {
|
|
220
|
+
this.logger.info(`NetworkProxy started on port ${this.options.port}`);
|
|
221
|
+
resolve();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Sets up tracking of TCP connections
|
|
228
|
+
*/
|
|
229
|
+
private setupConnectionTracking(): void {
|
|
230
|
+
this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
|
|
231
|
+
// Check if max connections reached
|
|
232
|
+
if (this.socketMap.getArray().length >= this.options.maxConnections) {
|
|
233
|
+
this.logger.warn(`Max connections (${this.options.maxConnections}) reached, rejecting new connection`);
|
|
234
|
+
connection.destroy();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Add connection to tracking
|
|
239
|
+
this.socketMap.add(connection);
|
|
240
|
+
this.connectedClients = this.socketMap.getArray().length;
|
|
241
|
+
|
|
242
|
+
// Check for connection from PortProxy by inspecting the source port
|
|
243
|
+
const localPort = connection.localPort || 0;
|
|
244
|
+
const remotePort = connection.remotePort || 0;
|
|
245
|
+
|
|
246
|
+
// If this connection is from a PortProxy (usually indicated by it coming from localhost)
|
|
247
|
+
if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
|
|
248
|
+
this.portProxyConnections++;
|
|
249
|
+
this.logger.debug(`New connection from PortProxy (local: ${localPort}, remote: ${remotePort})`);
|
|
250
|
+
} else {
|
|
251
|
+
this.logger.debug(`New direct connection (local: ${localPort}, remote: ${remotePort})`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Setup connection cleanup handlers
|
|
255
|
+
const cleanupConnection = () => {
|
|
256
|
+
if (this.socketMap.checkForObject(connection)) {
|
|
257
|
+
this.socketMap.remove(connection);
|
|
258
|
+
this.connectedClients = this.socketMap.getArray().length;
|
|
259
|
+
|
|
260
|
+
// If this was a PortProxy connection, decrement the counter
|
|
261
|
+
if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
|
|
262
|
+
this.portProxyConnections--;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.logger.debug(`Connection closed. ${this.connectedClients} connections remaining`);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
connection.on('close', cleanupConnection);
|
|
270
|
+
connection.on('error', (err) => {
|
|
271
|
+
this.logger.debug('Connection error', err);
|
|
272
|
+
cleanupConnection();
|
|
273
|
+
});
|
|
274
|
+
connection.on('end', cleanupConnection);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Track TLS handshake completions
|
|
278
|
+
this.httpsServer.on('secureConnection', (tlsSocket) => {
|
|
279
|
+
this.tlsTerminatedConnections++;
|
|
280
|
+
this.logger.debug('TLS handshake completed, connection secured');
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Sets up metrics collection
|
|
286
|
+
*/
|
|
287
|
+
private setupMetricsCollection(): void {
|
|
288
|
+
this.metricsInterval = setInterval(() => {
|
|
289
|
+
const uptime = Math.floor((Date.now() - this.startTime) / 1000);
|
|
290
|
+
const metrics = {
|
|
291
|
+
uptime,
|
|
292
|
+
activeConnections: this.connectedClients,
|
|
293
|
+
totalRequests: this.requestsServed,
|
|
294
|
+
failedRequests: this.failedRequests,
|
|
295
|
+
portProxyConnections: this.portProxyConnections,
|
|
296
|
+
tlsTerminatedConnections: this.tlsTerminatedConnections,
|
|
297
|
+
activeWebSockets: this.webSocketHandler.getConnectionInfo().activeConnections,
|
|
298
|
+
memoryUsage: process.memoryUsage(),
|
|
299
|
+
activeContexts: Array.from(this.activeContexts),
|
|
300
|
+
connectionPool: this.connectionPool.getPoolStatus()
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
this.logger.debug('Proxy metrics', metrics);
|
|
304
|
+
}, 60000); // Log metrics every minute
|
|
305
|
+
|
|
306
|
+
// Don't keep process alive just for metrics
|
|
307
|
+
if (this.metricsInterval.unref) {
|
|
308
|
+
this.metricsInterval.unref();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Updates proxy configurations
|
|
314
|
+
*/
|
|
315
|
+
public async updateProxyConfigs(
|
|
316
|
+
proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[]
|
|
317
|
+
): Promise<void> {
|
|
318
|
+
this.logger.info(`Updating proxy configurations (${proxyConfigsArg.length} configs)`);
|
|
319
|
+
|
|
320
|
+
// Update internal configs
|
|
321
|
+
this.proxyConfigs = proxyConfigsArg;
|
|
322
|
+
this.router.setNewProxyConfigs(proxyConfigsArg);
|
|
323
|
+
|
|
324
|
+
// Collect all hostnames for cleanup later
|
|
325
|
+
const currentHostNames = new Set<string>();
|
|
326
|
+
|
|
327
|
+
// Add/update SSL contexts for each host
|
|
328
|
+
for (const config of proxyConfigsArg) {
|
|
329
|
+
currentHostNames.add(config.hostName);
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
// Update certificate in cache
|
|
333
|
+
this.certificateManager.updateCertificateCache(
|
|
334
|
+
config.hostName,
|
|
335
|
+
config.publicKey,
|
|
336
|
+
config.privateKey
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
this.activeContexts.add(config.hostName);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
this.logger.error(`Failed to add SSL context for ${config.hostName}`, error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Clean up removed contexts
|
|
346
|
+
for (const hostname of this.activeContexts) {
|
|
347
|
+
if (!currentHostNames.has(hostname)) {
|
|
348
|
+
this.logger.info(`Hostname ${hostname} removed from configuration`);
|
|
349
|
+
this.activeContexts.delete(hostname);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Register domains with Port80Handler if available
|
|
354
|
+
const domainsForACME = Array.from(currentHostNames)
|
|
355
|
+
.filter(domain => !domain.includes('*')); // Skip wildcard domains
|
|
356
|
+
|
|
357
|
+
this.certificateManager.registerDomainsWithPort80Handler(domainsForACME);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Converts PortProxy domain configurations to NetworkProxy configs
|
|
362
|
+
* @param domainConfigs PortProxy domain configs
|
|
363
|
+
* @param sslKeyPair Default SSL key pair to use if not specified
|
|
364
|
+
* @returns Array of NetworkProxy configs
|
|
365
|
+
*/
|
|
366
|
+
public convertPortProxyConfigs(
|
|
367
|
+
domainConfigs: Array<{
|
|
368
|
+
domains: string[];
|
|
369
|
+
targetIPs?: string[];
|
|
370
|
+
allowedIPs?: string[];
|
|
371
|
+
}>,
|
|
372
|
+
sslKeyPair?: { key: string; cert: string }
|
|
373
|
+
): plugins.tsclass.network.IReverseProxyConfig[] {
|
|
374
|
+
const proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
|
375
|
+
|
|
376
|
+
// Use default certificates if not provided
|
|
377
|
+
const defaultCerts = this.certificateManager.getDefaultCertificates();
|
|
378
|
+
const sslKey = sslKeyPair?.key || defaultCerts.key;
|
|
379
|
+
const sslCert = sslKeyPair?.cert || defaultCerts.cert;
|
|
380
|
+
|
|
381
|
+
for (const domainConfig of domainConfigs) {
|
|
382
|
+
// Each domain in the domains array gets its own config
|
|
383
|
+
for (const domain of domainConfig.domains) {
|
|
384
|
+
// Skip non-hostname patterns (like IP addresses)
|
|
385
|
+
if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
proxyConfigs.push({
|
|
390
|
+
hostName: domain,
|
|
391
|
+
destinationIps: domainConfig.targetIPs || ['localhost'],
|
|
392
|
+
destinationPorts: [this.options.port], // Use the NetworkProxy port
|
|
393
|
+
privateKey: sslKey,
|
|
394
|
+
publicKey: sslCert
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
this.logger.info(`Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
|
|
400
|
+
return proxyConfigs;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Adds default headers to be included in all responses
|
|
405
|
+
*/
|
|
406
|
+
public async addDefaultHeaders(headersArg: { [key: string]: string }): Promise<void> {
|
|
407
|
+
this.logger.info('Adding default headers', headersArg);
|
|
408
|
+
this.requestHandler.setDefaultHeaders(headersArg);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Stops the proxy server
|
|
413
|
+
*/
|
|
414
|
+
public async stop(): Promise<void> {
|
|
415
|
+
this.logger.info('Stopping NetworkProxy server');
|
|
416
|
+
|
|
417
|
+
// Clear intervals
|
|
418
|
+
if (this.metricsInterval) {
|
|
419
|
+
clearInterval(this.metricsInterval);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (this.connectionPoolCleanupInterval) {
|
|
423
|
+
clearInterval(this.connectionPoolCleanupInterval);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Stop WebSocket handler
|
|
427
|
+
this.webSocketHandler.shutdown();
|
|
428
|
+
|
|
429
|
+
// Close all tracked sockets
|
|
430
|
+
for (const socket of this.socketMap.getArray()) {
|
|
431
|
+
try {
|
|
432
|
+
socket.destroy();
|
|
433
|
+
} catch (error) {
|
|
434
|
+
this.logger.error('Error destroying socket', error);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Close all connection pool connections
|
|
439
|
+
this.connectionPool.closeAllConnections();
|
|
440
|
+
|
|
441
|
+
// Stop Port80Handler if internally managed
|
|
442
|
+
await this.certificateManager.stopPort80Handler();
|
|
443
|
+
|
|
444
|
+
// Close the HTTPS server
|
|
445
|
+
return new Promise((resolve) => {
|
|
446
|
+
this.httpsServer.close(() => {
|
|
447
|
+
this.logger.info('NetworkProxy server stopped successfully');
|
|
448
|
+
resolve();
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Requests a new certificate for a domain
|
|
455
|
+
* This can be used to manually trigger certificate issuance
|
|
456
|
+
* @param domain The domain to request a certificate for
|
|
457
|
+
* @returns A promise that resolves when the request is submitted (not when the certificate is issued)
|
|
458
|
+
*/
|
|
459
|
+
public async requestCertificate(domain: string): Promise<boolean> {
|
|
460
|
+
return this.certificateManager.requestCertificate(domain);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Gets all proxy configurations currently in use
|
|
465
|
+
*/
|
|
466
|
+
public getProxyConfigs(): plugins.tsclass.network.IReverseProxyConfig[] {
|
|
467
|
+
return [...this.proxyConfigs];
|
|
468
|
+
}
|
|
469
|
+
}
|