@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
package/readme.md
CHANGED
|
@@ -16,7 +16,7 @@ flowchart TB
|
|
|
16
16
|
HTTP80[HTTP Port 80\nSslRedirect]
|
|
17
17
|
HTTPS443[HTTPS Port 443\nNetworkProxy]
|
|
18
18
|
PortProxy[TCP Port Proxy\nwith SNI routing]
|
|
19
|
-
|
|
19
|
+
NfTables[NfTablesProxy]
|
|
20
20
|
Router[ProxyRouter]
|
|
21
21
|
ACME[Port80Handler\nACME/Let's Encrypt]
|
|
22
22
|
Certs[(SSL Certificates)]
|
|
@@ -40,7 +40,7 @@ flowchart TB
|
|
|
40
40
|
PortProxy -->|Direct TCP| Service2
|
|
41
41
|
PortProxy -->|Direct TCP| Service3
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
NfTables -.->|Low-level forwarding| PortProxy
|
|
44
44
|
|
|
45
45
|
HTTP80 -.->|Challenge Response| ACME
|
|
46
46
|
ACME -.->|Generate/Manage| Certs
|
|
@@ -197,7 +197,7 @@ sequenceDiagram
|
|
|
197
197
|
- **HTTP to HTTPS Redirection** - Automatically redirect HTTP requests to HTTPS
|
|
198
198
|
- **Let's Encrypt Integration** - Automatic certificate management using ACME protocol
|
|
199
199
|
- **IP Filtering** - Control access with IP allow/block lists using glob patterns
|
|
200
|
-
- **
|
|
200
|
+
- **NfTables Integration** - Direct manipulation of nftables for advanced low-level port forwarding
|
|
201
201
|
- **Basic Authentication** - Support for basic auth on proxied routes
|
|
202
202
|
- **Connection Management** - Intelligent connection tracking and cleanup with configurable timeouts
|
|
203
203
|
- **Browser Compatibility** - Optimized for modern browsers with fixes for common TLS handshake issues
|
|
@@ -315,13 +315,13 @@ const portProxy = new PortProxy({
|
|
|
315
315
|
portProxy.start();
|
|
316
316
|
```
|
|
317
317
|
|
|
318
|
-
###
|
|
318
|
+
### NfTables Port Forwarding
|
|
319
319
|
|
|
320
320
|
```typescript
|
|
321
|
-
import {
|
|
321
|
+
import { NfTablesProxy } from '@push.rocks/smartproxy';
|
|
322
322
|
|
|
323
323
|
// Basic usage - forward single port
|
|
324
|
-
const basicProxy = new
|
|
324
|
+
const basicProxy = new NfTablesProxy({
|
|
325
325
|
fromPort: 80,
|
|
326
326
|
toPort: 8080,
|
|
327
327
|
toHost: 'localhost',
|
|
@@ -330,7 +330,7 @@ const basicProxy = new IPTablesProxy({
|
|
|
330
330
|
});
|
|
331
331
|
|
|
332
332
|
// Forward port ranges
|
|
333
|
-
const rangeProxy = new
|
|
333
|
+
const rangeProxy = new NfTablesProxy({
|
|
334
334
|
fromPort: { from: 3000, to: 3010 }, // Forward ports 3000-3010
|
|
335
335
|
toPort: { from: 8000, to: 8010 }, // To ports 8000-8010
|
|
336
336
|
protocol: 'tcp', // TCP protocol (default)
|
|
@@ -339,19 +339,26 @@ const rangeProxy = new IPTablesProxy({
|
|
|
339
339
|
});
|
|
340
340
|
|
|
341
341
|
// Multiple port specifications with IP filtering
|
|
342
|
-
const advancedProxy = new
|
|
342
|
+
const advancedProxy = new NfTablesProxy({
|
|
343
343
|
fromPort: [80, 443, { from: 8000, to: 8010 }], // Multiple ports/ranges
|
|
344
344
|
toPort: [8080, 8443, { from: 18000, to: 18010 }],
|
|
345
345
|
allowedSourceIPs: ['10.0.0.0/8', '192.168.1.0/24'], // Only allow these IPs
|
|
346
346
|
bannedSourceIPs: ['192.168.1.100'], // Explicitly block these IPs
|
|
347
|
-
|
|
348
|
-
|
|
347
|
+
useIPSets: true, // Use IP sets for efficient IP management
|
|
348
|
+
forceCleanSlate: false // Clean all NfTablesProxy rules before starting
|
|
349
349
|
});
|
|
350
350
|
|
|
351
|
-
//
|
|
352
|
-
const
|
|
351
|
+
// Advanced features: QoS, connection tracking, and NetworkProxy integration
|
|
352
|
+
const advancedProxy = new NfTablesProxy({
|
|
353
353
|
fromPort: 443,
|
|
354
354
|
toPort: 8443,
|
|
355
|
+
toHost: 'localhost',
|
|
356
|
+
useAdvancedNAT: true, // Use connection tracking for stateful NAT
|
|
357
|
+
qos: {
|
|
358
|
+
enabled: true,
|
|
359
|
+
maxRate: '10mbps', // Limit bandwidth
|
|
360
|
+
priority: 1 // Set traffic priority (1-10)
|
|
361
|
+
},
|
|
355
362
|
netProxyIntegration: {
|
|
356
363
|
enabled: true,
|
|
357
364
|
redirectLocalhost: true, // Redirect localhost traffic to NetworkProxy
|
|
@@ -372,8 +379,25 @@ import { Port80Handler } from '@push.rocks/smartproxy';
|
|
|
372
379
|
const acmeHandler = new Port80Handler();
|
|
373
380
|
|
|
374
381
|
// Add domains to manage certificates for
|
|
375
|
-
acmeHandler.addDomain(
|
|
376
|
-
|
|
382
|
+
acmeHandler.addDomain({
|
|
383
|
+
domainName: 'example.com',
|
|
384
|
+
sslRedirect: true,
|
|
385
|
+
acmeMaintenance: true
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
acmeHandler.addDomain({
|
|
389
|
+
domainName: 'api.example.com',
|
|
390
|
+
sslRedirect: true,
|
|
391
|
+
acmeMaintenance: true
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Support for glob pattern domains for routing (certificates not issued for glob patterns)
|
|
395
|
+
acmeHandler.addDomain({
|
|
396
|
+
domainName: '*.example.com',
|
|
397
|
+
sslRedirect: true,
|
|
398
|
+
acmeMaintenance: false, // Can't issue certificates for wildcard domains via HTTP-01
|
|
399
|
+
forward: { ip: '192.168.1.10', port: 8080 } // Forward requests to this target
|
|
400
|
+
});
|
|
377
401
|
```
|
|
378
402
|
|
|
379
403
|
## Configuration Options
|
|
@@ -412,7 +436,7 @@ acmeHandler.addDomain('api.example.com');
|
|
|
412
436
|
| `enableDetailedLogging` | Enable detailed connection logging | false |
|
|
413
437
|
| `enableRandomizedTimeouts`| Randomize timeouts slightly to prevent thundering herd | true |
|
|
414
438
|
|
|
415
|
-
###
|
|
439
|
+
### NfTablesProxy Settings
|
|
416
440
|
|
|
417
441
|
| Option | Description | Default |
|
|
418
442
|
|-----------------------|---------------------------------------------------|-------------|
|
|
@@ -420,18 +444,32 @@ acmeHandler.addDomain('api.example.com');
|
|
|
420
444
|
| `toPort` | Destination port(s) or range(s) to forward to | - |
|
|
421
445
|
| `toHost` | Destination host to forward to | 'localhost' |
|
|
422
446
|
| `preserveSourceIP` | Preserve the original client IP | false |
|
|
423
|
-
| `deleteOnExit` | Remove
|
|
447
|
+
| `deleteOnExit` | Remove nftables rules when process exits | false |
|
|
424
448
|
| `protocol` | Protocol to forward ('tcp', 'udp', or 'all') | 'tcp' |
|
|
425
449
|
| `enableLogging` | Enable detailed logging | false |
|
|
426
|
-
| `
|
|
450
|
+
| `logFormat` | Format for logs ('plain' or 'json') | 'plain' |
|
|
451
|
+
| `ipv6Support` | Enable IPv6 support | false |
|
|
427
452
|
| `allowedSourceIPs` | Array of IP addresses/CIDR allowed to connect | - |
|
|
428
453
|
| `bannedSourceIPs` | Array of IP addresses/CIDR blocked from connecting | - |
|
|
429
|
-
| `
|
|
430
|
-
| `
|
|
431
|
-
| `
|
|
454
|
+
| `useIPSets` | Use nftables sets for efficient IP management | true |
|
|
455
|
+
| `forceCleanSlate` | Clear all NfTablesProxy rules before starting | false |
|
|
456
|
+
| `tableName` | Custom table name | 'portproxy' |
|
|
457
|
+
| `maxRetries` | Maximum number of retries for failed commands | 3 |
|
|
458
|
+
| `retryDelayMs` | Delay between retries in milliseconds | 1000 |
|
|
459
|
+
| `useAdvancedNAT` | Use connection tracking for stateful NAT | false |
|
|
460
|
+
| `qos` | Quality of Service options (object) | - |
|
|
432
461
|
| `netProxyIntegration` | NetworkProxy integration options (object) | - |
|
|
433
462
|
|
|
434
|
-
####
|
|
463
|
+
#### NfTablesProxy QoS Options
|
|
464
|
+
|
|
465
|
+
| Option | Description | Default |
|
|
466
|
+
|----------------------|---------------------------------------------------|---------|
|
|
467
|
+
| `enabled` | Enable Quality of Service features | false |
|
|
468
|
+
| `maxRate` | Maximum bandwidth rate (e.g. "10mbps") | - |
|
|
469
|
+
| `priority` | Traffic priority (1-10, 1 is highest) | - |
|
|
470
|
+
| `markConnections` | Mark connections for easier management | false |
|
|
471
|
+
|
|
472
|
+
#### NfTablesProxy NetworkProxy Integration Options
|
|
435
473
|
|
|
436
474
|
| Option | Description | Default |
|
|
437
475
|
|----------------------|---------------------------------------------------|---------|
|
|
@@ -490,18 +528,30 @@ The `PortProxy` class can inspect the SNI (Server Name Indication) field in TLS
|
|
|
490
528
|
- Domain-specific allowed IP ranges
|
|
491
529
|
- Protection against SNI renegotiation attacks
|
|
492
530
|
|
|
493
|
-
### Enhanced
|
|
531
|
+
### Enhanced NfTables Management
|
|
494
532
|
|
|
495
|
-
The
|
|
533
|
+
The `NfTablesProxy` class offers advanced capabilities compared to the previous IPTablesProxy:
|
|
496
534
|
|
|
497
535
|
- Support for multiple port ranges and individual ports
|
|
498
|
-
-
|
|
499
|
-
-
|
|
500
|
-
-
|
|
536
|
+
- More efficient IP filtering using nftables sets
|
|
537
|
+
- IPv6 support with full feature parity
|
|
538
|
+
- Quality of Service (QoS) features including bandwidth limiting and traffic prioritization
|
|
539
|
+
- Advanced connection tracking for stateful NAT
|
|
540
|
+
- Robust error handling with retry mechanisms
|
|
541
|
+
- Structured logging with JSON support
|
|
501
542
|
- NetworkProxy integration for SSL termination
|
|
502
|
-
- Automatic rule existence checking to prevent duplicates
|
|
503
543
|
- Comprehensive cleanup on shutdown
|
|
504
544
|
|
|
545
|
+
### Port80Handler with Glob Pattern Support
|
|
546
|
+
|
|
547
|
+
The `Port80Handler` class now includes support for glob pattern domain matching:
|
|
548
|
+
|
|
549
|
+
- Supports wildcard domains like `*.example.com` for HTTP request routing
|
|
550
|
+
- Detects glob patterns and skips certificate issuance for them
|
|
551
|
+
- Smart routing that first attempts exact matches, then tries pattern matching
|
|
552
|
+
- Supports forwarding HTTP requests to backend services
|
|
553
|
+
- Separate forwarding configuration for ACME challenges
|
|
554
|
+
|
|
505
555
|
## Troubleshooting
|
|
506
556
|
|
|
507
557
|
### Browser Certificate Errors
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartproxy',
|
|
6
|
-
version: '5.
|
|
6
|
+
version: '5.1.0',
|
|
7
7
|
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
|
|
8
8
|
}
|
package/ts/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export * from './classes.nftablesproxy.js';
|
|
2
|
-
export * from './classes.networkproxy.js';
|
|
3
|
-
export * from './classes.port80handler.js';
|
|
1
|
+
export * from './nfttablesproxy/classes.nftablesproxy.js';
|
|
2
|
+
export * from './networkproxy/classes.np.networkproxy.js';
|
|
3
|
+
export * from './port80handler/classes.port80handler.js';
|
|
4
4
|
export * from './classes.sslredirect.js';
|
|
5
|
-
export * from './classes.
|
|
6
|
-
export * from './classes.pp.snihandler.js';
|
|
7
|
-
export * from './classes.pp.interfaces.js';
|
|
5
|
+
export * from './smartproxy/classes.smartproxy.js';
|
|
6
|
+
export * from './smartproxy/classes.pp.snihandler.js';
|
|
7
|
+
export * from './smartproxy/classes.pp.interfaces.js';
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './classes.np.types.js';
|
|
6
|
+
import { Port80Handler, Port80HandlerEvents, type IDomainOptions } from '../port80handler/classes.port80handler.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Manages SSL certificates for NetworkProxy including ACME integration
|
|
10
|
+
*/
|
|
11
|
+
export class CertificateManager {
|
|
12
|
+
private defaultCertificates: { key: string; cert: string };
|
|
13
|
+
private certificateCache: Map<string, ICertificateEntry> = new Map();
|
|
14
|
+
private port80Handler: Port80Handler | null = null;
|
|
15
|
+
private externalPort80Handler: boolean = false;
|
|
16
|
+
private certificateStoreDir: string;
|
|
17
|
+
private logger: ILogger;
|
|
18
|
+
private httpsServer: plugins.https.Server | null = null;
|
|
19
|
+
|
|
20
|
+
constructor(private options: INetworkProxyOptions) {
|
|
21
|
+
this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs');
|
|
22
|
+
this.logger = createLogger(options.logLevel || 'info');
|
|
23
|
+
|
|
24
|
+
// Ensure certificate store directory exists
|
|
25
|
+
try {
|
|
26
|
+
if (!fs.existsSync(this.certificateStoreDir)) {
|
|
27
|
+
fs.mkdirSync(this.certificateStoreDir, { recursive: true });
|
|
28
|
+
this.logger.info(`Created certificate store directory: ${this.certificateStoreDir}`);
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
this.logger.warn(`Failed to create certificate store directory: ${error}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.loadDefaultCertificates();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Loads default certificates from the filesystem
|
|
39
|
+
*/
|
|
40
|
+
public loadDefaultCertificates(): void {
|
|
41
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
const certPath = path.join(__dirname, '..', '..', 'assets', 'certs');
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
this.defaultCertificates = {
|
|
46
|
+
key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'),
|
|
47
|
+
cert: fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8')
|
|
48
|
+
};
|
|
49
|
+
this.logger.info('Default certificates loaded successfully');
|
|
50
|
+
} catch (error) {
|
|
51
|
+
this.logger.error('Error loading default certificates', error);
|
|
52
|
+
|
|
53
|
+
// Generate self-signed fallback certificates
|
|
54
|
+
try {
|
|
55
|
+
// This is a placeholder for actual certificate generation code
|
|
56
|
+
// In a real implementation, you would use a library like selfsigned to generate certs
|
|
57
|
+
this.defaultCertificates = {
|
|
58
|
+
key: "FALLBACK_KEY_CONTENT",
|
|
59
|
+
cert: "FALLBACK_CERT_CONTENT"
|
|
60
|
+
};
|
|
61
|
+
this.logger.warn('Using fallback self-signed certificates');
|
|
62
|
+
} catch (fallbackError) {
|
|
63
|
+
this.logger.error('Failed to generate fallback certificates', fallbackError);
|
|
64
|
+
throw new Error('Could not load or generate SSL certificates');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set the HTTPS server reference for context updates
|
|
71
|
+
*/
|
|
72
|
+
public setHttpsServer(server: plugins.https.Server): void {
|
|
73
|
+
this.httpsServer = server;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get default certificates
|
|
78
|
+
*/
|
|
79
|
+
public getDefaultCertificates(): { key: string; cert: string } {
|
|
80
|
+
return { ...this.defaultCertificates };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sets an external Port80Handler for certificate management
|
|
85
|
+
*/
|
|
86
|
+
public setExternalPort80Handler(handler: Port80Handler): void {
|
|
87
|
+
if (this.port80Handler && !this.externalPort80Handler) {
|
|
88
|
+
this.logger.warn('Replacing existing internal Port80Handler with external handler');
|
|
89
|
+
|
|
90
|
+
// Clean up existing handler if needed
|
|
91
|
+
if (this.port80Handler !== handler) {
|
|
92
|
+
// Unregister event handlers to avoid memory leaks
|
|
93
|
+
this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_ISSUED);
|
|
94
|
+
this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_RENEWED);
|
|
95
|
+
this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_FAILED);
|
|
96
|
+
this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_EXPIRING);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Set the external handler
|
|
101
|
+
this.port80Handler = handler;
|
|
102
|
+
this.externalPort80Handler = true;
|
|
103
|
+
|
|
104
|
+
// Register event handlers
|
|
105
|
+
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
|
|
106
|
+
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
|
|
107
|
+
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
|
|
108
|
+
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
|
|
109
|
+
this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
this.logger.info('External Port80Handler connected to CertificateManager');
|
|
113
|
+
|
|
114
|
+
// Register domains with Port80Handler if we have any certificates cached
|
|
115
|
+
if (this.certificateCache.size > 0) {
|
|
116
|
+
const domains = Array.from(this.certificateCache.keys())
|
|
117
|
+
.filter(domain => !domain.includes('*')); // Skip wildcard domains
|
|
118
|
+
|
|
119
|
+
this.registerDomainsWithPort80Handler(domains);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handle newly issued or renewed certificates from Port80Handler
|
|
125
|
+
*/
|
|
126
|
+
private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
|
|
127
|
+
const { domain, certificate, privateKey, expiryDate } = data;
|
|
128
|
+
|
|
129
|
+
this.logger.info(`Certificate ${this.certificateCache.has(domain) ? 'renewed' : 'issued'} for ${domain}, valid until ${expiryDate.toISOString()}`);
|
|
130
|
+
|
|
131
|
+
// Update certificate in HTTPS server
|
|
132
|
+
this.updateCertificateCache(domain, certificate, privateKey, expiryDate);
|
|
133
|
+
|
|
134
|
+
// Save the certificate to the filesystem if not using external handler
|
|
135
|
+
if (!this.externalPort80Handler && this.options.acme?.certificateStore) {
|
|
136
|
+
this.saveCertificateToStore(domain, certificate, privateKey);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Handle certificate issuance failures
|
|
142
|
+
*/
|
|
143
|
+
private handleCertificateFailed(data: { domain: string; error: string }): void {
|
|
144
|
+
this.logger.error(`Certificate issuance failed for ${data.domain}: ${data.error}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Saves certificate and private key to the filesystem
|
|
149
|
+
*/
|
|
150
|
+
private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
|
|
151
|
+
try {
|
|
152
|
+
const certPath = path.join(this.certificateStoreDir, `${domain}.cert.pem`);
|
|
153
|
+
const keyPath = path.join(this.certificateStoreDir, `${domain}.key.pem`);
|
|
154
|
+
|
|
155
|
+
fs.writeFileSync(certPath, certificate);
|
|
156
|
+
fs.writeFileSync(keyPath, privateKey);
|
|
157
|
+
|
|
158
|
+
// Ensure private key has restricted permissions
|
|
159
|
+
try {
|
|
160
|
+
fs.chmodSync(keyPath, 0o600);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
this.logger.warn(`Failed to set permissions on private key for ${domain}: ${error}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.logger.info(`Saved certificate for ${domain} to ${certPath}`);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.logger.error(`Failed to save certificate for ${domain}: ${error}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Handles SNI (Server Name Indication) for TLS connections
|
|
173
|
+
* Used by the HTTPS server to select the correct certificate for each domain
|
|
174
|
+
*/
|
|
175
|
+
public handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
|
|
176
|
+
this.logger.debug(`SNI request for domain: ${domain}`);
|
|
177
|
+
|
|
178
|
+
// Check if we have a certificate for this domain
|
|
179
|
+
const certs = this.certificateCache.get(domain);
|
|
180
|
+
|
|
181
|
+
if (certs) {
|
|
182
|
+
try {
|
|
183
|
+
// Create TLS context with the cached certificate
|
|
184
|
+
const context = plugins.tls.createSecureContext({
|
|
185
|
+
key: certs.key,
|
|
186
|
+
cert: certs.cert
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
this.logger.debug(`Using cached certificate for ${domain}`);
|
|
190
|
+
cb(null, context);
|
|
191
|
+
return;
|
|
192
|
+
} catch (err) {
|
|
193
|
+
this.logger.error(`Error creating secure context for ${domain}:`, err);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check if we should trigger certificate issuance
|
|
198
|
+
if (this.options.acme?.enabled && this.port80Handler && !domain.includes('*')) {
|
|
199
|
+
// Check if this domain is already registered
|
|
200
|
+
const certData = this.port80Handler.getCertificate(domain);
|
|
201
|
+
|
|
202
|
+
if (!certData) {
|
|
203
|
+
this.logger.info(`No certificate found for ${domain}, registering for issuance`);
|
|
204
|
+
|
|
205
|
+
// Register with new domain options format
|
|
206
|
+
const domainOptions: IDomainOptions = {
|
|
207
|
+
domainName: domain,
|
|
208
|
+
sslRedirect: true,
|
|
209
|
+
acmeMaintenance: true
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
this.port80Handler.addDomain(domainOptions);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Fall back to default certificate
|
|
217
|
+
try {
|
|
218
|
+
const context = plugins.tls.createSecureContext({
|
|
219
|
+
key: this.defaultCertificates.key,
|
|
220
|
+
cert: this.defaultCertificates.cert
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
this.logger.debug(`Using default certificate for ${domain}`);
|
|
224
|
+
cb(null, context);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
this.logger.error(`Error creating default secure context:`, err);
|
|
227
|
+
cb(new Error('Cannot create secure context'), null);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Updates certificate in cache
|
|
233
|
+
*/
|
|
234
|
+
public updateCertificateCache(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
|
|
235
|
+
// Update certificate context in HTTPS server if it's running
|
|
236
|
+
if (this.httpsServer) {
|
|
237
|
+
try {
|
|
238
|
+
this.httpsServer.addContext(domain, {
|
|
239
|
+
key: privateKey,
|
|
240
|
+
cert: certificate
|
|
241
|
+
});
|
|
242
|
+
this.logger.debug(`Updated SSL context for domain: ${domain}`);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.logger.error(`Error updating SSL context for domain ${domain}:`, error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Update certificate in cache
|
|
249
|
+
this.certificateCache.set(domain, {
|
|
250
|
+
key: privateKey,
|
|
251
|
+
cert: certificate,
|
|
252
|
+
expires: expiryDate
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Gets a certificate for a domain
|
|
258
|
+
*/
|
|
259
|
+
public getCertificate(domain: string): ICertificateEntry | undefined {
|
|
260
|
+
return this.certificateCache.get(domain);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Requests a new certificate for a domain
|
|
265
|
+
*/
|
|
266
|
+
public async requestCertificate(domain: string): Promise<boolean> {
|
|
267
|
+
if (!this.options.acme?.enabled && !this.externalPort80Handler) {
|
|
268
|
+
this.logger.warn('ACME certificate management is not enabled');
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!this.port80Handler) {
|
|
273
|
+
this.logger.error('Port80Handler is not initialized');
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
|
|
278
|
+
if (domain.includes('*')) {
|
|
279
|
+
this.logger.error(`Cannot request certificate for wildcard domain: ${domain}`);
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
// Use the new domain options format
|
|
285
|
+
const domainOptions: IDomainOptions = {
|
|
286
|
+
domainName: domain,
|
|
287
|
+
sslRedirect: true,
|
|
288
|
+
acmeMaintenance: true
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
this.port80Handler.addDomain(domainOptions);
|
|
292
|
+
this.logger.info(`Certificate request submitted for domain: ${domain}`);
|
|
293
|
+
return true;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
this.logger.error(`Error requesting certificate for domain ${domain}:`, error);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Registers domains with Port80Handler for ACME certificate management
|
|
302
|
+
*/
|
|
303
|
+
public registerDomainsWithPort80Handler(domains: string[]): void {
|
|
304
|
+
if (!this.port80Handler) {
|
|
305
|
+
this.logger.warn('Port80Handler is not initialized');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const domain of domains) {
|
|
310
|
+
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
|
|
311
|
+
if (domain.includes('*')) {
|
|
312
|
+
this.logger.info(`Skipping wildcard domain for ACME: ${domain}`);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Skip domains already with certificates if configured to do so
|
|
317
|
+
if (this.options.acme?.skipConfiguredCerts) {
|
|
318
|
+
const cachedCert = this.certificateCache.get(domain);
|
|
319
|
+
if (cachedCert) {
|
|
320
|
+
this.logger.info(`Skipping domain with existing certificate: ${domain}`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Register the domain for certificate issuance with new domain options format
|
|
326
|
+
const domainOptions: IDomainOptions = {
|
|
327
|
+
domainName: domain,
|
|
328
|
+
sslRedirect: true,
|
|
329
|
+
acmeMaintenance: true
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
this.port80Handler.addDomain(domainOptions);
|
|
333
|
+
this.logger.info(`Registered domain for ACME certificate issuance: ${domain}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Initialize internal Port80Handler
|
|
339
|
+
*/
|
|
340
|
+
public async initializePort80Handler(): Promise<Port80Handler | null> {
|
|
341
|
+
// Skip if using external handler
|
|
342
|
+
if (this.externalPort80Handler) {
|
|
343
|
+
this.logger.info('Using external Port80Handler, skipping initialization');
|
|
344
|
+
return this.port80Handler;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!this.options.acme?.enabled) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Create certificate manager
|
|
352
|
+
this.port80Handler = new Port80Handler({
|
|
353
|
+
port: this.options.acme.port,
|
|
354
|
+
contactEmail: this.options.acme.contactEmail,
|
|
355
|
+
useProduction: this.options.acme.useProduction,
|
|
356
|
+
renewThresholdDays: this.options.acme.renewThresholdDays,
|
|
357
|
+
httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
|
|
358
|
+
renewCheckIntervalHours: 24, // Check daily for renewals
|
|
359
|
+
enabled: this.options.acme.enabled,
|
|
360
|
+
autoRenew: this.options.acme.autoRenew,
|
|
361
|
+
certificateStore: this.options.acme.certificateStore,
|
|
362
|
+
skipConfiguredCerts: this.options.acme.skipConfiguredCerts
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Register event handlers
|
|
366
|
+
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
|
|
367
|
+
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
|
|
368
|
+
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
|
|
369
|
+
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
|
|
370
|
+
this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Start the handler
|
|
374
|
+
try {
|
|
375
|
+
await this.port80Handler.start();
|
|
376
|
+
this.logger.info(`Port80Handler started on port ${this.options.acme.port}`);
|
|
377
|
+
return this.port80Handler;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
this.logger.error(`Failed to start Port80Handler: ${error}`);
|
|
380
|
+
this.port80Handler = null;
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Stop the Port80Handler if it was internally created
|
|
387
|
+
*/
|
|
388
|
+
public async stopPort80Handler(): Promise<void> {
|
|
389
|
+
if (this.port80Handler && !this.externalPort80Handler) {
|
|
390
|
+
try {
|
|
391
|
+
await this.port80Handler.stop();
|
|
392
|
+
this.logger.info('Port80Handler stopped');
|
|
393
|
+
} catch (error) {
|
|
394
|
+
this.logger.error('Error stopping Port80Handler', error);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|