@push.rocks/smartproxy 7.1.2 → 10.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 +2 -2
- package/dist_ts/classes.router.d.ts +9 -10
- package/dist_ts/classes.router.js +3 -5
- package/dist_ts/common/acmeFactory.d.ts +9 -0
- package/dist_ts/common/acmeFactory.js +20 -0
- package/dist_ts/common/eventUtils.d.ts +15 -0
- package/dist_ts/common/eventUtils.js +19 -0
- package/dist_ts/common/types.d.ts +82 -0
- package/dist_ts/common/types.js +17 -0
- package/dist_ts/networkproxy/classes.np.certificatemanager.js +23 -19
- package/dist_ts/networkproxy/classes.np.networkproxy.d.ts +1 -0
- package/dist_ts/networkproxy/classes.np.networkproxy.js +5 -1
- package/dist_ts/networkproxy/classes.np.types.d.ts +5 -10
- package/dist_ts/networkproxy/classes.np.types.js +1 -1
- package/dist_ts/plugins.d.ts +6 -3
- package/dist_ts/plugins.js +7 -4
- package/dist_ts/port80handler/classes.port80handler.d.ts +14 -111
- package/dist_ts/port80handler/classes.port80handler.js +94 -373
- package/dist_ts/smartproxy/classes.pp.certprovisioner.d.ts +54 -0
- package/dist_ts/smartproxy/classes.pp.certprovisioner.js +166 -0
- package/dist_ts/smartproxy/classes.pp.interfaces.d.ts +11 -33
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +5 -0
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +21 -11
- package/dist_ts/smartproxy/classes.pp.portrangemanager.js +1 -11
- package/dist_ts/smartproxy/classes.smartproxy.d.ts +3 -5
- package/dist_ts/smartproxy/classes.smartproxy.js +94 -180
- package/package.json +12 -10
- package/readme.hints.md +64 -1
- package/readme.md +253 -408
- package/readme.plan.md +29 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.router.ts +13 -15
- package/ts/common/acmeFactory.ts +23 -0
- package/ts/common/eventUtils.ts +34 -0
- package/ts/common/types.ts +89 -0
- package/ts/networkproxy/classes.np.certificatemanager.ts +23 -19
- package/ts/networkproxy/classes.np.networkproxy.ts +4 -0
- package/ts/networkproxy/classes.np.types.ts +6 -10
- package/ts/plugins.ts +17 -4
- package/ts/port80handler/classes.port80handler.ts +108 -509
- package/ts/smartproxy/classes.pp.certprovisioner.ts +188 -0
- package/ts/smartproxy/classes.pp.interfaces.ts +13 -36
- package/ts/smartproxy/classes.pp.networkproxybridge.ts +22 -10
- package/ts/smartproxy/classes.pp.portrangemanager.ts +0 -10
- package/ts/smartproxy/classes.smartproxy.ts +103 -195
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { IDomainConfig, ISmartProxyCertProvisionObject } from './classes.pp.interfaces.js';
|
|
3
|
+
import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
|
4
|
+
import { Port80HandlerEvents } from '../common/types.js';
|
|
5
|
+
import { subscribeToPort80Handler } from '../common/eventUtils.js';
|
|
6
|
+
import type { ICertificateData } from '../common/types.js';
|
|
7
|
+
import type { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CertProvisioner manages certificate provisioning and renewal workflows,
|
|
11
|
+
* unifying static certificates and HTTP-01 challenges via Port80Handler.
|
|
12
|
+
*/
|
|
13
|
+
export class CertProvisioner extends plugins.EventEmitter {
|
|
14
|
+
private domainConfigs: IDomainConfig[];
|
|
15
|
+
private port80Handler: Port80Handler;
|
|
16
|
+
private networkProxyBridge: NetworkProxyBridge;
|
|
17
|
+
private certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
|
18
|
+
private forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }>;
|
|
19
|
+
private renewThresholdDays: number;
|
|
20
|
+
private renewCheckIntervalHours: number;
|
|
21
|
+
private autoRenew: boolean;
|
|
22
|
+
private renewManager?: plugins.taskbuffer.TaskManager;
|
|
23
|
+
// Track provisioning type per domain: 'http01' or 'static'
|
|
24
|
+
private provisionMap: Map<string, 'http01' | 'static'>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param domainConfigs Array of domain configuration objects
|
|
28
|
+
* @param port80Handler HTTP-01 challenge handler instance
|
|
29
|
+
* @param networkProxyBridge Bridge for applying external certificates
|
|
30
|
+
* @param certProvider Optional callback returning a static cert or 'http01'
|
|
31
|
+
* @param renewThresholdDays Days before expiry to trigger renewals
|
|
32
|
+
* @param renewCheckIntervalHours Interval in hours to check for renewals
|
|
33
|
+
* @param autoRenew Whether to automatically schedule renewals
|
|
34
|
+
*/
|
|
35
|
+
constructor(
|
|
36
|
+
domainConfigs: IDomainConfig[],
|
|
37
|
+
port80Handler: Port80Handler,
|
|
38
|
+
networkProxyBridge: NetworkProxyBridge,
|
|
39
|
+
certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>,
|
|
40
|
+
renewThresholdDays: number = 30,
|
|
41
|
+
renewCheckIntervalHours: number = 24,
|
|
42
|
+
autoRenew: boolean = true,
|
|
43
|
+
forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }> = []
|
|
44
|
+
) {
|
|
45
|
+
super();
|
|
46
|
+
this.domainConfigs = domainConfigs;
|
|
47
|
+
this.port80Handler = port80Handler;
|
|
48
|
+
this.networkProxyBridge = networkProxyBridge;
|
|
49
|
+
this.certProvider = certProvider;
|
|
50
|
+
this.renewThresholdDays = renewThresholdDays;
|
|
51
|
+
this.renewCheckIntervalHours = renewCheckIntervalHours;
|
|
52
|
+
this.autoRenew = autoRenew;
|
|
53
|
+
this.provisionMap = new Map();
|
|
54
|
+
this.forwardConfigs = forwardConfigs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start initial provisioning and schedule renewals.
|
|
59
|
+
*/
|
|
60
|
+
public async start(): Promise<void> {
|
|
61
|
+
// Subscribe to Port80Handler certificate events
|
|
62
|
+
subscribeToPort80Handler(this.port80Handler, {
|
|
63
|
+
onCertificateIssued: (data: ICertificateData) => {
|
|
64
|
+
this.emit('certificate', { ...data, source: 'http01', isRenewal: false });
|
|
65
|
+
},
|
|
66
|
+
onCertificateRenewed: (data: ICertificateData) => {
|
|
67
|
+
this.emit('certificate', { ...data, source: 'http01', isRenewal: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Apply external forwarding for ACME challenges (e.g. Synology)
|
|
72
|
+
for (const f of this.forwardConfigs) {
|
|
73
|
+
this.port80Handler.addDomain({
|
|
74
|
+
domainName: f.domain,
|
|
75
|
+
sslRedirect: f.sslRedirect,
|
|
76
|
+
acmeMaintenance: false,
|
|
77
|
+
forward: f.forwardConfig,
|
|
78
|
+
acmeForward: f.acmeForwardConfig
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Initial provisioning for all domains
|
|
82
|
+
const domains = this.domainConfigs.flatMap(cfg => cfg.domains);
|
|
83
|
+
for (const domain of domains) {
|
|
84
|
+
// Skip wildcard domains
|
|
85
|
+
if (domain.includes('*')) continue;
|
|
86
|
+
let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
|
|
87
|
+
if (this.certProvider) {
|
|
88
|
+
try {
|
|
89
|
+
provision = await this.certProvider(domain);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(`certProvider error for ${domain}:`, err);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (provision === 'http01') {
|
|
95
|
+
this.provisionMap.set(domain, 'http01');
|
|
96
|
+
this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true });
|
|
97
|
+
} else {
|
|
98
|
+
this.provisionMap.set(domain, 'static');
|
|
99
|
+
const certObj = provision as plugins.tsclass.network.ICert;
|
|
100
|
+
const certData: ICertificateData = {
|
|
101
|
+
domain: certObj.domainName,
|
|
102
|
+
certificate: certObj.publicKey,
|
|
103
|
+
privateKey: certObj.privateKey,
|
|
104
|
+
expiryDate: new Date(certObj.validUntil)
|
|
105
|
+
};
|
|
106
|
+
this.networkProxyBridge.applyExternalCertificate(certData);
|
|
107
|
+
this.emit('certificate', { ...certData, source: 'static', isRenewal: false });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Schedule renewals if enabled
|
|
112
|
+
if (this.autoRenew) {
|
|
113
|
+
this.renewManager = new plugins.taskbuffer.TaskManager();
|
|
114
|
+
const renewTask = new plugins.taskbuffer.Task({
|
|
115
|
+
name: 'CertificateRenewals',
|
|
116
|
+
taskFunction: async () => {
|
|
117
|
+
for (const [domain, type] of this.provisionMap.entries()) {
|
|
118
|
+
// Skip wildcard domains
|
|
119
|
+
if (domain.includes('*')) continue;
|
|
120
|
+
try {
|
|
121
|
+
if (type === 'http01') {
|
|
122
|
+
await this.port80Handler.renewCertificate(domain);
|
|
123
|
+
} else if (type === 'static' && this.certProvider) {
|
|
124
|
+
const provision2 = await this.certProvider(domain);
|
|
125
|
+
if (provision2 !== 'http01') {
|
|
126
|
+
const certObj = provision2 as plugins.tsclass.network.ICert;
|
|
127
|
+
const certData: ICertificateData = {
|
|
128
|
+
domain: certObj.domainName,
|
|
129
|
+
certificate: certObj.publicKey,
|
|
130
|
+
privateKey: certObj.privateKey,
|
|
131
|
+
expiryDate: new Date(certObj.validUntil)
|
|
132
|
+
};
|
|
133
|
+
this.networkProxyBridge.applyExternalCertificate(certData);
|
|
134
|
+
this.emit('certificate', { ...certData, source: 'static', isRenewal: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(`Renewal error for ${domain}:`, err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
const hours = this.renewCheckIntervalHours;
|
|
144
|
+
const cronExpr = `0 0 */${hours} * * *`;
|
|
145
|
+
this.renewManager.addAndScheduleTask(renewTask, cronExpr);
|
|
146
|
+
this.renewManager.start();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Stop all scheduled renewal tasks.
|
|
152
|
+
*/
|
|
153
|
+
public async stop(): Promise<void> {
|
|
154
|
+
// Stop scheduled renewals
|
|
155
|
+
if (this.renewManager) {
|
|
156
|
+
this.renewManager.stop();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Request a certificate on-demand for the given domain.
|
|
162
|
+
* @param domain Domain name to provision
|
|
163
|
+
*/
|
|
164
|
+
public async requestCertificate(domain: string): Promise<void> {
|
|
165
|
+
// Skip wildcard domains
|
|
166
|
+
if (domain.includes('*')) {
|
|
167
|
+
throw new Error(`Cannot request certificate for wildcard domain: ${domain}`);
|
|
168
|
+
}
|
|
169
|
+
// Determine provisioning method
|
|
170
|
+
let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
|
|
171
|
+
if (this.certProvider) {
|
|
172
|
+
provision = await this.certProvider(domain);
|
|
173
|
+
}
|
|
174
|
+
if (provision === 'http01') {
|
|
175
|
+
await this.port80Handler.renewCertificate(domain);
|
|
176
|
+
} else {
|
|
177
|
+
const certObj = provision as plugins.tsclass.network.ICert;
|
|
178
|
+
const certData: ICertificateData = {
|
|
179
|
+
domain: certObj.domainName,
|
|
180
|
+
certificate: certObj.publicKey,
|
|
181
|
+
privateKey: certObj.privateKey,
|
|
182
|
+
expiryDate: new Date(certObj.validUntil)
|
|
183
|
+
};
|
|
184
|
+
this.networkProxyBridge.applyExternalCertificate(certData);
|
|
185
|
+
this.emit('certificate', { ...certData, source: 'static', isRenewal: false });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Provision object for static or HTTP-01 certificate
|
|
5
|
+
*/
|
|
6
|
+
export type ISmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01';
|
|
7
|
+
|
|
3
8
|
/** Domain configuration with per-domain allowed port ranges */
|
|
4
9
|
export interface IDomainConfig {
|
|
5
10
|
domains: string[]; // Glob patterns for domain(s)
|
|
@@ -16,6 +21,7 @@ export interface IDomainConfig {
|
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
/** Port proxy settings including global allowed port ranges */
|
|
24
|
+
import type { IAcmeOptions } from '../common/types.js';
|
|
19
25
|
export interface IPortProxySettings {
|
|
20
26
|
fromPort: number;
|
|
21
27
|
toPort: number;
|
|
@@ -78,43 +84,14 @@ export interface IPortProxySettings {
|
|
|
78
84
|
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
|
|
79
85
|
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
|
80
86
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
enabled?: boolean; // Whether to enable automatic certificate management
|
|
84
|
-
port?: number; // Port to listen on for ACME challenges (default: 80)
|
|
85
|
-
contactEmail?: string; // Email for Let's Encrypt account
|
|
86
|
-
useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
|
|
87
|
-
renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
|
|
88
|
-
autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
|
|
89
|
-
certificateStore?: string; // Directory to store certificates (default: ./certs)
|
|
90
|
-
skipConfiguredCerts?: boolean; // Skip domains that already have certificates
|
|
91
|
-
httpsRedirectPort?: number; // Port to redirect HTTP requests to HTTPS (default: 443)
|
|
92
|
-
renewCheckIntervalHours?: number; // How often to check for renewals (default: 24)
|
|
93
|
-
// Domain-specific forwarding configurations
|
|
94
|
-
domainForwards?: Array<{
|
|
95
|
-
domain: string;
|
|
96
|
-
forwardConfig?: {
|
|
97
|
-
ip: string;
|
|
98
|
-
port: number;
|
|
99
|
-
};
|
|
100
|
-
acmeForwardConfig?: {
|
|
101
|
-
ip: string;
|
|
102
|
-
port: number;
|
|
103
|
-
};
|
|
104
|
-
}>;
|
|
105
|
-
};
|
|
87
|
+
// ACME configuration options for SmartProxy
|
|
88
|
+
acme?: IAcmeOptions;
|
|
106
89
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
useProduction?: boolean;
|
|
113
|
-
renewThresholdDays?: number;
|
|
114
|
-
autoRenew?: boolean;
|
|
115
|
-
certificateStore?: string;
|
|
116
|
-
skipConfiguredCerts?: boolean;
|
|
117
|
-
};
|
|
90
|
+
/**
|
|
91
|
+
* Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
|
|
92
|
+
* or a static certificate object for immediate provisioning.
|
|
93
|
+
*/
|
|
94
|
+
certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
|
118
95
|
}
|
|
119
96
|
|
|
120
97
|
/**
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import { NetworkProxy } from '../networkproxy/classes.np.networkproxy.js';
|
|
3
|
-
import { Port80Handler
|
|
3
|
+
import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
|
4
|
+
import { Port80HandlerEvents } from '../common/types.js';
|
|
5
|
+
import { subscribeToPort80Handler } from '../common/eventUtils.js';
|
|
6
|
+
import type { ICertificateData } from '../common/types.js';
|
|
4
7
|
import type { IConnectionRecord, IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js';
|
|
5
8
|
|
|
6
9
|
/**
|
|
@@ -18,9 +21,11 @@ export class NetworkProxyBridge {
|
|
|
18
21
|
public setPort80Handler(handler: Port80Handler): void {
|
|
19
22
|
this.port80Handler = handler;
|
|
20
23
|
|
|
21
|
-
//
|
|
22
|
-
handler
|
|
23
|
-
|
|
24
|
+
// Subscribe to certificate events
|
|
25
|
+
subscribeToPort80Handler(handler, {
|
|
26
|
+
onCertificateIssued: this.handleCertificateEvent.bind(this),
|
|
27
|
+
onCertificateRenewed: this.handleCertificateEvent.bind(this)
|
|
28
|
+
});
|
|
24
29
|
|
|
25
30
|
// If NetworkProxy is already initialized, connect it with Port80Handler
|
|
26
31
|
if (this.networkProxy) {
|
|
@@ -43,10 +48,6 @@ export class NetworkProxyBridge {
|
|
|
43
48
|
useExternalPort80Handler: !!this.port80Handler // Use Port80Handler if available
|
|
44
49
|
};
|
|
45
50
|
|
|
46
|
-
// Copy ACME settings for backward compatibility (if port80HandlerConfig not set)
|
|
47
|
-
if (!this.settings.port80HandlerConfig && this.settings.acme) {
|
|
48
|
-
networkProxyOptions.acme = { ...this.settings.acme };
|
|
49
|
-
}
|
|
50
51
|
|
|
51
52
|
this.networkProxy = new NetworkProxy(networkProxyOptions);
|
|
52
53
|
|
|
@@ -95,6 +96,17 @@ export class NetworkProxyBridge {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Apply an external (static) certificate into NetworkProxy
|
|
101
|
+
*/
|
|
102
|
+
public applyExternalCertificate(data: ICertificateData): void {
|
|
103
|
+
if (!this.networkProxy) {
|
|
104
|
+
console.log(`NetworkProxy not initialized: cannot apply external certificate for ${data.domain}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.handleCertificateEvent(data);
|
|
108
|
+
}
|
|
109
|
+
|
|
98
110
|
/**
|
|
99
111
|
* Get the NetworkProxy instance
|
|
100
112
|
*/
|
|
@@ -277,7 +289,7 @@ export class NetworkProxyBridge {
|
|
|
277
289
|
);
|
|
278
290
|
|
|
279
291
|
// Log ACME-eligible domains
|
|
280
|
-
const acmeEnabled = this.settings.
|
|
292
|
+
const acmeEnabled = !!this.settings.acme?.enabled;
|
|
281
293
|
if (acmeEnabled) {
|
|
282
294
|
const acmeEligibleDomains = proxyConfigs
|
|
283
295
|
.filter((config) => !config.hostName.includes('*')) // Exclude wildcards
|
|
@@ -338,7 +350,7 @@ export class NetworkProxyBridge {
|
|
|
338
350
|
return false;
|
|
339
351
|
}
|
|
340
352
|
|
|
341
|
-
if (!this.settings.
|
|
353
|
+
if (!this.settings.acme?.enabled) {
|
|
342
354
|
console.log('Cannot request certificate - ACME is not enabled');
|
|
343
355
|
return false;
|
|
344
356
|
}
|
|
@@ -117,10 +117,6 @@ export class PortRangeManager {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
// Add ACME HTTP challenge port if enabled
|
|
121
|
-
if (this.settings.acme?.enabled && this.settings.acme.port) {
|
|
122
|
-
ports.add(this.settings.acme.port);
|
|
123
|
-
}
|
|
124
120
|
|
|
125
121
|
// Add global port ranges
|
|
126
122
|
if (this.settings.globalPortRanges) {
|
|
@@ -202,12 +198,6 @@ export class PortRangeManager {
|
|
|
202
198
|
warnings.push(`NetworkProxy port ${this.settings.networkProxyPort} is also used in port ranges`);
|
|
203
199
|
}
|
|
204
200
|
|
|
205
|
-
// Check ACME port
|
|
206
|
-
if (this.settings.acme?.enabled && this.settings.acme.port) {
|
|
207
|
-
if (portMappings.has(this.settings.acme.port)) {
|
|
208
|
-
warnings.push(`ACME HTTP challenge port ${this.settings.acme.port} is also used in port ranges`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
201
|
|
|
212
202
|
return warnings;
|
|
213
203
|
}
|