@push.rocks/smartproxy 4.2.4 → 4.2.6
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.networkproxy.d.ts +6 -6
- package/dist_ts/classes.networkproxy.js +56 -39
- package/dist_ts/classes.port80handler.d.ts +89 -13
- package/dist_ts/classes.port80handler.js +312 -116
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.networkproxy.ts +59 -38
- package/ts/classes.port80handler.ts +402 -123
|
@@ -1,9 +1,58 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Custom error classes for better error handling
|
|
6
|
+
*/
|
|
7
|
+
export class Port80HandlerError extends Error {
|
|
8
|
+
constructor(message: string) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'Port80HandlerError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class CertificateError extends Port80HandlerError {
|
|
15
|
+
constructor(
|
|
16
|
+
message: string,
|
|
17
|
+
public readonly domain: string,
|
|
18
|
+
public readonly isRenewal: boolean = false
|
|
19
|
+
) {
|
|
20
|
+
super(`${message} for domain ${domain}${isRenewal ? ' (renewal)' : ''}`);
|
|
21
|
+
this.name = 'CertificateError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ServerError extends Port80HandlerError {
|
|
26
|
+
constructor(message: string, public readonly code?: string) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'ServerError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Domain forwarding configuration
|
|
34
|
+
*/
|
|
35
|
+
export interface IForwardConfig {
|
|
36
|
+
ip: string;
|
|
37
|
+
port: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Domain configuration options
|
|
42
|
+
*/
|
|
43
|
+
export interface IDomainOptions {
|
|
44
|
+
domainName: string;
|
|
45
|
+
sslRedirect: boolean; // if true redirects the request to port 443
|
|
46
|
+
acmeMaintenance: boolean; // tries to always have a valid cert for this domain
|
|
47
|
+
forward?: IForwardConfig; // forwards all http requests to that target
|
|
48
|
+
acmeForward?: IForwardConfig; // forwards letsencrypt requests to this config
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Represents a domain configuration with certificate status information
|
|
5
53
|
*/
|
|
6
54
|
interface IDomainCertificate {
|
|
55
|
+
options: IDomainOptions;
|
|
7
56
|
certObtained: boolean;
|
|
8
57
|
obtainingInProgress: boolean;
|
|
9
58
|
certificate?: string;
|
|
@@ -15,9 +64,9 @@ interface IDomainCertificate {
|
|
|
15
64
|
}
|
|
16
65
|
|
|
17
66
|
/**
|
|
18
|
-
* Configuration options for the
|
|
67
|
+
* Configuration options for the Port80Handler
|
|
19
68
|
*/
|
|
20
|
-
interface
|
|
69
|
+
interface IPort80HandlerOptions {
|
|
21
70
|
port?: number;
|
|
22
71
|
contactEmail?: string;
|
|
23
72
|
useProduction?: boolean;
|
|
@@ -29,7 +78,7 @@ interface IAcmeCertManagerOptions {
|
|
|
29
78
|
/**
|
|
30
79
|
* Certificate data that can be emitted via events or set from outside
|
|
31
80
|
*/
|
|
32
|
-
interface ICertificateData {
|
|
81
|
+
export interface ICertificateData {
|
|
33
82
|
domain: string;
|
|
34
83
|
certificate: string;
|
|
35
84
|
privateKey: string;
|
|
@@ -37,34 +86,53 @@ interface ICertificateData {
|
|
|
37
86
|
}
|
|
38
87
|
|
|
39
88
|
/**
|
|
40
|
-
* Events emitted by the
|
|
89
|
+
* Events emitted by the Port80Handler
|
|
41
90
|
*/
|
|
42
|
-
export enum
|
|
91
|
+
export enum Port80HandlerEvents {
|
|
43
92
|
CERTIFICATE_ISSUED = 'certificate-issued',
|
|
44
93
|
CERTIFICATE_RENEWED = 'certificate-renewed',
|
|
45
94
|
CERTIFICATE_FAILED = 'certificate-failed',
|
|
46
95
|
CERTIFICATE_EXPIRING = 'certificate-expiring',
|
|
47
96
|
MANAGER_STARTED = 'manager-started',
|
|
48
97
|
MANAGER_STOPPED = 'manager-stopped',
|
|
98
|
+
REQUEST_FORWARDED = 'request-forwarded',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Certificate failure payload type
|
|
103
|
+
*/
|
|
104
|
+
export interface ICertificateFailure {
|
|
105
|
+
domain: string;
|
|
106
|
+
error: string;
|
|
107
|
+
isRenewal: boolean;
|
|
49
108
|
}
|
|
50
109
|
|
|
51
110
|
/**
|
|
52
|
-
*
|
|
111
|
+
* Certificate expiry payload type
|
|
53
112
|
*/
|
|
54
|
-
export
|
|
113
|
+
export interface ICertificateExpiring {
|
|
114
|
+
domain: string;
|
|
115
|
+
expiryDate: Date;
|
|
116
|
+
daysRemaining: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Port80Handler with ACME certificate management and request forwarding capabilities
|
|
121
|
+
*/
|
|
122
|
+
export class Port80Handler extends plugins.EventEmitter {
|
|
55
123
|
private domainCertificates: Map<string, IDomainCertificate>;
|
|
56
124
|
private server: plugins.http.Server | null = null;
|
|
57
125
|
private acmeClient: plugins.acme.Client | null = null;
|
|
58
126
|
private accountKey: string | null = null;
|
|
59
127
|
private renewalTimer: NodeJS.Timeout | null = null;
|
|
60
128
|
private isShuttingDown: boolean = false;
|
|
61
|
-
private options: Required<
|
|
129
|
+
private options: Required<IPort80HandlerOptions>;
|
|
62
130
|
|
|
63
131
|
/**
|
|
64
|
-
* Creates a new
|
|
132
|
+
* Creates a new Port80Handler
|
|
65
133
|
* @param options Configuration options
|
|
66
134
|
*/
|
|
67
|
-
constructor(options:
|
|
135
|
+
constructor(options: IPort80HandlerOptions = {}) {
|
|
68
136
|
super();
|
|
69
137
|
this.domainCertificates = new Map<string, IDomainCertificate>();
|
|
70
138
|
|
|
@@ -73,7 +141,7 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
73
141
|
port: options.port ?? 80,
|
|
74
142
|
contactEmail: options.contactEmail ?? 'admin@example.com',
|
|
75
143
|
useProduction: options.useProduction ?? false, // Safer default: staging
|
|
76
|
-
renewThresholdDays: options.renewThresholdDays ??
|
|
144
|
+
renewThresholdDays: options.renewThresholdDays ?? 10, // Changed to 10 days as per requirements
|
|
77
145
|
httpsRedirectPort: options.httpsRedirectPort ?? 443,
|
|
78
146
|
renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
|
|
79
147
|
};
|
|
@@ -84,11 +152,11 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
84
152
|
*/
|
|
85
153
|
public async start(): Promise<void> {
|
|
86
154
|
if (this.server) {
|
|
87
|
-
throw new
|
|
155
|
+
throw new ServerError('Server is already running');
|
|
88
156
|
}
|
|
89
157
|
|
|
90
158
|
if (this.isShuttingDown) {
|
|
91
|
-
throw new
|
|
159
|
+
throw new ServerError('Server is shutting down');
|
|
92
160
|
}
|
|
93
161
|
|
|
94
162
|
return new Promise((resolve, reject) => {
|
|
@@ -97,22 +165,33 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
97
165
|
|
|
98
166
|
this.server.on('error', (error: NodeJS.ErrnoException) => {
|
|
99
167
|
if (error.code === 'EACCES') {
|
|
100
|
-
reject(new
|
|
168
|
+
reject(new ServerError(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`, error.code));
|
|
101
169
|
} else if (error.code === 'EADDRINUSE') {
|
|
102
|
-
reject(new
|
|
170
|
+
reject(new ServerError(`Port ${this.options.port} is already in use.`, error.code));
|
|
103
171
|
} else {
|
|
104
|
-
reject(error);
|
|
172
|
+
reject(new ServerError(error.message, error.code));
|
|
105
173
|
}
|
|
106
174
|
});
|
|
107
175
|
|
|
108
176
|
this.server.listen(this.options.port, () => {
|
|
109
|
-
console.log(`
|
|
177
|
+
console.log(`Port80Handler is listening on port ${this.options.port}`);
|
|
110
178
|
this.startRenewalTimer();
|
|
111
|
-
this.emit(
|
|
179
|
+
this.emit(Port80HandlerEvents.MANAGER_STARTED, this.options.port);
|
|
180
|
+
|
|
181
|
+
// Start certificate process for domains with acmeMaintenance enabled
|
|
182
|
+
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
|
|
183
|
+
if (domainInfo.options.acmeMaintenance && !domainInfo.certObtained && !domainInfo.obtainingInProgress) {
|
|
184
|
+
this.obtainCertificate(domain).catch(err => {
|
|
185
|
+
console.error(`Error obtaining initial certificate for ${domain}:`, err);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
112
190
|
resolve();
|
|
113
191
|
});
|
|
114
192
|
} catch (error) {
|
|
115
|
-
|
|
193
|
+
const message = error instanceof Error ? error.message : 'Unknown error starting server';
|
|
194
|
+
reject(new ServerError(message));
|
|
116
195
|
}
|
|
117
196
|
});
|
|
118
197
|
}
|
|
@@ -138,7 +217,7 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
138
217
|
this.server.close(() => {
|
|
139
218
|
this.server = null;
|
|
140
219
|
this.isShuttingDown = false;
|
|
141
|
-
this.emit(
|
|
220
|
+
this.emit(Port80HandlerEvents.MANAGER_STOPPED);
|
|
142
221
|
resolve();
|
|
143
222
|
});
|
|
144
223
|
} else {
|
|
@@ -149,13 +228,41 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
149
228
|
}
|
|
150
229
|
|
|
151
230
|
/**
|
|
152
|
-
* Adds a domain
|
|
153
|
-
* @param
|
|
231
|
+
* Adds a domain with configuration options
|
|
232
|
+
* @param options Domain configuration options
|
|
154
233
|
*/
|
|
155
|
-
public addDomain(
|
|
156
|
-
if (!
|
|
157
|
-
|
|
158
|
-
|
|
234
|
+
public addDomain(options: IDomainOptions): void {
|
|
235
|
+
if (!options.domainName || typeof options.domainName !== 'string') {
|
|
236
|
+
throw new Port80HandlerError('Invalid domain name');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const domainName = options.domainName;
|
|
240
|
+
|
|
241
|
+
if (!this.domainCertificates.has(domainName)) {
|
|
242
|
+
this.domainCertificates.set(domainName, {
|
|
243
|
+
options,
|
|
244
|
+
certObtained: false,
|
|
245
|
+
obtainingInProgress: false
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
console.log(`Domain added: ${domainName} with configuration:`, {
|
|
249
|
+
sslRedirect: options.sslRedirect,
|
|
250
|
+
acmeMaintenance: options.acmeMaintenance,
|
|
251
|
+
hasForward: !!options.forward,
|
|
252
|
+
hasAcmeForward: !!options.acmeForward
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// If acmeMaintenance is enabled, start certificate process immediately
|
|
256
|
+
if (options.acmeMaintenance && this.server) {
|
|
257
|
+
this.obtainCertificate(domainName).catch(err => {
|
|
258
|
+
console.error(`Error obtaining initial certificate for ${domainName}:`, err);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
// Update existing domain with new options
|
|
263
|
+
const existing = this.domainCertificates.get(domainName)!;
|
|
264
|
+
existing.options = options;
|
|
265
|
+
console.log(`Domain ${domainName} configuration updated`);
|
|
159
266
|
}
|
|
160
267
|
}
|
|
161
268
|
|
|
@@ -177,10 +284,25 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
177
284
|
* @param expiryDate Optional expiry date
|
|
178
285
|
*/
|
|
179
286
|
public setCertificate(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
|
|
287
|
+
if (!domain || !certificate || !privateKey) {
|
|
288
|
+
throw new Port80HandlerError('Domain, certificate and privateKey are required');
|
|
289
|
+
}
|
|
290
|
+
|
|
180
291
|
let domainInfo = this.domainCertificates.get(domain);
|
|
181
292
|
|
|
182
293
|
if (!domainInfo) {
|
|
183
|
-
|
|
294
|
+
// Create default domain options if not already configured
|
|
295
|
+
const defaultOptions: IDomainOptions = {
|
|
296
|
+
domainName: domain,
|
|
297
|
+
sslRedirect: true,
|
|
298
|
+
acmeMaintenance: true
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
domainInfo = {
|
|
302
|
+
options: defaultOptions,
|
|
303
|
+
certObtained: false,
|
|
304
|
+
obtainingInProgress: false
|
|
305
|
+
};
|
|
184
306
|
this.domainCertificates.set(domain, domainInfo);
|
|
185
307
|
}
|
|
186
308
|
|
|
@@ -192,27 +314,18 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
192
314
|
if (expiryDate) {
|
|
193
315
|
domainInfo.expiryDate = expiryDate;
|
|
194
316
|
} else {
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
// This is a simplistic approach - in a real implementation, use a proper
|
|
198
|
-
// certificate parsing library like node-forge or x509
|
|
199
|
-
const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
|
|
200
|
-
if (matches && matches[1]) {
|
|
201
|
-
domainInfo.expiryDate = new Date(matches[1]);
|
|
202
|
-
}
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.warn(`Failed to extract expiry date from certificate for ${domain}`);
|
|
205
|
-
}
|
|
317
|
+
// Extract expiry date from certificate
|
|
318
|
+
domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain);
|
|
206
319
|
}
|
|
207
320
|
|
|
208
321
|
console.log(`Certificate set for ${domain}`);
|
|
209
322
|
|
|
210
323
|
// Emit certificate event
|
|
211
|
-
this.emitCertificateEvent(
|
|
324
|
+
this.emitCertificateEvent(Port80HandlerEvents.CERTIFICATE_ISSUED, {
|
|
212
325
|
domain,
|
|
213
326
|
certificate,
|
|
214
327
|
privateKey,
|
|
215
|
-
expiryDate: domainInfo.expiryDate ||
|
|
328
|
+
expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
|
|
216
329
|
});
|
|
217
330
|
}
|
|
218
331
|
|
|
@@ -231,7 +344,7 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
231
344
|
domain,
|
|
232
345
|
certificate: domainInfo.certificate,
|
|
233
346
|
privateKey: domainInfo.privateKey,
|
|
234
|
-
expiryDate: domainInfo.expiryDate ||
|
|
347
|
+
expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
|
|
235
348
|
};
|
|
236
349
|
}
|
|
237
350
|
|
|
@@ -244,23 +357,28 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
244
357
|
return this.acmeClient;
|
|
245
358
|
}
|
|
246
359
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
360
|
+
try {
|
|
361
|
+
// Generate a new account key
|
|
362
|
+
this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
|
|
363
|
+
|
|
364
|
+
this.acmeClient = new plugins.acme.Client({
|
|
365
|
+
directoryUrl: this.options.useProduction
|
|
366
|
+
? plugins.acme.directory.letsencrypt.production
|
|
367
|
+
: plugins.acme.directory.letsencrypt.staging,
|
|
368
|
+
accountKey: this.accountKey,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Create a new account
|
|
372
|
+
await this.acmeClient.createAccount({
|
|
373
|
+
termsOfServiceAgreed: true,
|
|
374
|
+
contact: [`mailto:${this.options.contactEmail}`],
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return this.acmeClient;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
const message = error instanceof Error ? error.message : 'Unknown error initializing ACME client';
|
|
380
|
+
throw new Port80HandlerError(`Failed to initialize ACME client: ${message}`);
|
|
381
|
+
}
|
|
264
382
|
}
|
|
265
383
|
|
|
266
384
|
/**
|
|
@@ -279,12 +397,7 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
279
397
|
// Extract domain (ignoring any port in the Host header)
|
|
280
398
|
const domain = hostHeader.split(':')[0];
|
|
281
399
|
|
|
282
|
-
//
|
|
283
|
-
if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
|
|
284
|
-
this.handleAcmeChallenge(req, res, domain);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
400
|
+
// Check if domain is configured
|
|
288
401
|
if (!this.domainCertificates.has(domain)) {
|
|
289
402
|
res.statusCode = 404;
|
|
290
403
|
res.end('Domain not configured');
|
|
@@ -292,9 +405,28 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
292
405
|
}
|
|
293
406
|
|
|
294
407
|
const domainInfo = this.domainCertificates.get(domain)!;
|
|
408
|
+
const options = domainInfo.options;
|
|
295
409
|
|
|
296
|
-
// If
|
|
297
|
-
if (
|
|
410
|
+
// If the request is for an ACME HTTP-01 challenge, handle it
|
|
411
|
+
if (req.url && req.url.startsWith('/.well-known/acme-challenge/') && (options.acmeMaintenance || options.acmeForward)) {
|
|
412
|
+
// Check if we should forward ACME requests
|
|
413
|
+
if (options.acmeForward) {
|
|
414
|
+
this.forwardRequest(req, res, options.acmeForward, 'ACME challenge');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
this.handleAcmeChallenge(req, res, domain);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Check if we should forward non-ACME requests
|
|
423
|
+
if (options.forward) {
|
|
424
|
+
this.forwardRequest(req, res, options.forward, 'HTTP');
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// If certificate exists and sslRedirect is enabled, redirect to HTTPS
|
|
429
|
+
if (domainInfo.certObtained && options.sslRedirect) {
|
|
298
430
|
const httpsPort = this.options.httpsRedirectPort;
|
|
299
431
|
const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`;
|
|
300
432
|
const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`;
|
|
@@ -302,17 +434,93 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
302
434
|
res.statusCode = 301;
|
|
303
435
|
res.setHeader('Location', redirectUrl);
|
|
304
436
|
res.end(`Redirecting to ${redirectUrl}`);
|
|
305
|
-
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Handle case where certificate maintenance is enabled but not yet obtained
|
|
441
|
+
if (options.acmeMaintenance && !domainInfo.certObtained) {
|
|
306
442
|
// Trigger certificate issuance if not already running
|
|
307
443
|
if (!domainInfo.obtainingInProgress) {
|
|
308
444
|
this.obtainCertificate(domain).catch(err => {
|
|
309
|
-
|
|
445
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
446
|
+
this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
|
|
447
|
+
domain,
|
|
448
|
+
error: errorMessage,
|
|
449
|
+
isRenewal: false
|
|
450
|
+
});
|
|
310
451
|
console.error(`Error obtaining certificate for ${domain}:`, err);
|
|
311
452
|
});
|
|
312
453
|
}
|
|
313
454
|
|
|
314
455
|
res.statusCode = 503;
|
|
315
456
|
res.end('Certificate issuance in progress, please try again later.');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Default response for unhandled request
|
|
461
|
+
res.statusCode = 404;
|
|
462
|
+
res.end('No handlers configured for this request');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Forwards an HTTP request to the specified target
|
|
467
|
+
* @param req The original request
|
|
468
|
+
* @param res The response object
|
|
469
|
+
* @param target The forwarding target (IP and port)
|
|
470
|
+
* @param requestType Type of request for logging
|
|
471
|
+
*/
|
|
472
|
+
private forwardRequest(
|
|
473
|
+
req: plugins.http.IncomingMessage,
|
|
474
|
+
res: plugins.http.ServerResponse,
|
|
475
|
+
target: IForwardConfig,
|
|
476
|
+
requestType: string
|
|
477
|
+
): void {
|
|
478
|
+
const options = {
|
|
479
|
+
hostname: target.ip,
|
|
480
|
+
port: target.port,
|
|
481
|
+
path: req.url,
|
|
482
|
+
method: req.method,
|
|
483
|
+
headers: { ...req.headers }
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const domain = req.headers.host?.split(':')[0] || 'unknown';
|
|
487
|
+
console.log(`Forwarding ${requestType} request for ${domain} to ${target.ip}:${target.port}`);
|
|
488
|
+
|
|
489
|
+
const proxyReq = plugins.http.request(options, (proxyRes) => {
|
|
490
|
+
// Copy status code
|
|
491
|
+
res.statusCode = proxyRes.statusCode || 500;
|
|
492
|
+
|
|
493
|
+
// Copy headers
|
|
494
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
495
|
+
if (value) res.setHeader(key, value);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Pipe response data
|
|
499
|
+
proxyRes.pipe(res);
|
|
500
|
+
|
|
501
|
+
this.emit(Port80HandlerEvents.REQUEST_FORWARDED, {
|
|
502
|
+
domain,
|
|
503
|
+
requestType,
|
|
504
|
+
target: `${target.ip}:${target.port}`,
|
|
505
|
+
statusCode: proxyRes.statusCode
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
proxyReq.on('error', (error) => {
|
|
510
|
+
console.error(`Error forwarding request to ${target.ip}:${target.port}:`, error);
|
|
511
|
+
if (!res.headersSent) {
|
|
512
|
+
res.statusCode = 502;
|
|
513
|
+
res.end(`Proxy error: ${error.message}`);
|
|
514
|
+
} else {
|
|
515
|
+
res.end();
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Pipe original request to proxy request
|
|
520
|
+
if (req.readable) {
|
|
521
|
+
req.pipe(proxyReq);
|
|
522
|
+
} else {
|
|
523
|
+
proxyReq.end();
|
|
316
524
|
}
|
|
317
525
|
}
|
|
318
526
|
|
|
@@ -354,7 +562,13 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
354
562
|
// Get the domain info
|
|
355
563
|
const domainInfo = this.domainCertificates.get(domain);
|
|
356
564
|
if (!domainInfo) {
|
|
357
|
-
throw new
|
|
565
|
+
throw new CertificateError('Domain not found', domain, isRenewal);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Verify that acmeMaintenance is enabled
|
|
569
|
+
if (!domainInfo.options.acmeMaintenance) {
|
|
570
|
+
console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`);
|
|
571
|
+
return;
|
|
358
572
|
}
|
|
359
573
|
|
|
360
574
|
// Prevent concurrent certificate issuance
|
|
@@ -377,40 +591,8 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
377
591
|
// Get the authorizations for the order
|
|
378
592
|
const authorizations = await client.getAuthorizations(order);
|
|
379
593
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (!challenge) {
|
|
383
|
-
throw new Error('HTTP-01 challenge not found');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Get the key authorization for the challenge
|
|
387
|
-
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
|
388
|
-
|
|
389
|
-
// Store the challenge data
|
|
390
|
-
domainInfo.challengeToken = challenge.token;
|
|
391
|
-
domainInfo.challengeKeyAuthorization = keyAuthorization;
|
|
392
|
-
|
|
393
|
-
// ACME client type definition workaround - use compatible approach
|
|
394
|
-
// First check if challenge verification is needed
|
|
395
|
-
const authzUrl = authz.url;
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
// Check if authzUrl exists and perform verification
|
|
399
|
-
if (authzUrl) {
|
|
400
|
-
await client.verifyChallenge(authz, challenge);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Complete the challenge
|
|
404
|
-
await client.completeChallenge(challenge);
|
|
405
|
-
|
|
406
|
-
// Wait for validation
|
|
407
|
-
await client.waitForValidStatus(challenge);
|
|
408
|
-
console.log(`HTTP-01 challenge completed for ${domain}`);
|
|
409
|
-
} catch (error) {
|
|
410
|
-
console.error(`Challenge error for ${domain}:`, error);
|
|
411
|
-
throw error;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
594
|
+
// Process each authorization
|
|
595
|
+
await this.processAuthorizations(client, domain, authorizations);
|
|
414
596
|
|
|
415
597
|
// Generate a CSR and private key
|
|
416
598
|
const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({
|
|
@@ -436,28 +618,20 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
436
618
|
delete domainInfo.challengeKeyAuthorization;
|
|
437
619
|
|
|
438
620
|
// Extract expiry date from certificate
|
|
439
|
-
|
|
440
|
-
const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
|
|
441
|
-
if (matches && matches[1]) {
|
|
442
|
-
domainInfo.expiryDate = new Date(matches[1]);
|
|
443
|
-
console.log(`Certificate for ${domain} will expire on ${domainInfo.expiryDate.toISOString()}`);
|
|
444
|
-
}
|
|
445
|
-
} catch (error) {
|
|
446
|
-
console.warn(`Failed to extract expiry date from certificate for ${domain}`);
|
|
447
|
-
}
|
|
621
|
+
domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain);
|
|
448
622
|
|
|
449
623
|
console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
|
|
450
624
|
|
|
451
625
|
// Emit the appropriate event
|
|
452
626
|
const eventType = isRenewal
|
|
453
|
-
?
|
|
454
|
-
:
|
|
627
|
+
? Port80HandlerEvents.CERTIFICATE_RENEWED
|
|
628
|
+
: Port80HandlerEvents.CERTIFICATE_ISSUED;
|
|
455
629
|
|
|
456
630
|
this.emitCertificateEvent(eventType, {
|
|
457
631
|
domain,
|
|
458
632
|
certificate,
|
|
459
633
|
privateKey,
|
|
460
|
-
expiryDate: domainInfo.expiryDate ||
|
|
634
|
+
expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
|
|
461
635
|
});
|
|
462
636
|
|
|
463
637
|
} catch (error: any) {
|
|
@@ -473,17 +647,76 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
473
647
|
}
|
|
474
648
|
|
|
475
649
|
// Emit failure event
|
|
476
|
-
this.emit(
|
|
650
|
+
this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
|
|
477
651
|
domain,
|
|
478
652
|
error: error.message || 'Unknown error',
|
|
479
653
|
isRenewal
|
|
480
|
-
});
|
|
654
|
+
} as ICertificateFailure);
|
|
655
|
+
|
|
656
|
+
throw new CertificateError(
|
|
657
|
+
error.message || 'Certificate issuance failed',
|
|
658
|
+
domain,
|
|
659
|
+
isRenewal
|
|
660
|
+
);
|
|
481
661
|
} finally {
|
|
482
662
|
// Reset flag whether successful or not
|
|
483
663
|
domainInfo.obtainingInProgress = false;
|
|
484
664
|
}
|
|
485
665
|
}
|
|
486
666
|
|
|
667
|
+
/**
|
|
668
|
+
* Process ACME authorizations by verifying and completing challenges
|
|
669
|
+
* @param client ACME client
|
|
670
|
+
* @param domain Domain name
|
|
671
|
+
* @param authorizations Authorizations to process
|
|
672
|
+
*/
|
|
673
|
+
private async processAuthorizations(
|
|
674
|
+
client: plugins.acme.Client,
|
|
675
|
+
domain: string,
|
|
676
|
+
authorizations: plugins.acme.Authorization[]
|
|
677
|
+
): Promise<void> {
|
|
678
|
+
const domainInfo = this.domainCertificates.get(domain);
|
|
679
|
+
if (!domainInfo) {
|
|
680
|
+
throw new CertificateError('Domain not found during authorization', domain);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
for (const authz of authorizations) {
|
|
684
|
+
const challenge = authz.challenges.find(ch => ch.type === 'http-01');
|
|
685
|
+
if (!challenge) {
|
|
686
|
+
throw new CertificateError('HTTP-01 challenge not found', domain);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Get the key authorization for the challenge
|
|
690
|
+
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
|
691
|
+
|
|
692
|
+
// Store the challenge data
|
|
693
|
+
domainInfo.challengeToken = challenge.token;
|
|
694
|
+
domainInfo.challengeKeyAuthorization = keyAuthorization;
|
|
695
|
+
|
|
696
|
+
// ACME client type definition workaround - use compatible approach
|
|
697
|
+
// First check if challenge verification is needed
|
|
698
|
+
const authzUrl = authz.url;
|
|
699
|
+
|
|
700
|
+
try {
|
|
701
|
+
// Check if authzUrl exists and perform verification
|
|
702
|
+
if (authzUrl) {
|
|
703
|
+
await client.verifyChallenge(authz, challenge);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Complete the challenge
|
|
707
|
+
await client.completeChallenge(challenge);
|
|
708
|
+
|
|
709
|
+
// Wait for validation
|
|
710
|
+
await client.waitForValidStatus(challenge);
|
|
711
|
+
console.log(`HTTP-01 challenge completed for ${domain}`);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown challenge error';
|
|
714
|
+
console.error(`Challenge error for ${domain}:`, error);
|
|
715
|
+
throw new CertificateError(`Challenge verification failed: ${errorMessage}`, domain);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
487
720
|
/**
|
|
488
721
|
* Starts the certificate renewal timer
|
|
489
722
|
*/
|
|
@@ -519,6 +752,11 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
519
752
|
const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000;
|
|
520
753
|
|
|
521
754
|
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
|
|
755
|
+
// Skip domains with acmeMaintenance disabled
|
|
756
|
+
if (!domainInfo.options.acmeMaintenance) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
522
760
|
// Skip domains without certificates or already in renewal
|
|
523
761
|
if (!domainInfo.certObtained || domainInfo.obtainingInProgress) {
|
|
524
762
|
continue;
|
|
@@ -534,26 +772,67 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
|
|
534
772
|
// Check if certificate is near expiry
|
|
535
773
|
if (timeUntilExpiry <= renewThresholdMs) {
|
|
536
774
|
console.log(`Certificate for ${domain} expires soon, renewing...`);
|
|
537
|
-
|
|
775
|
+
|
|
776
|
+
const daysRemaining = Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000));
|
|
777
|
+
|
|
778
|
+
this.emit(Port80HandlerEvents.CERTIFICATE_EXPIRING, {
|
|
538
779
|
domain,
|
|
539
780
|
expiryDate: domainInfo.expiryDate,
|
|
540
|
-
daysRemaining
|
|
541
|
-
});
|
|
781
|
+
daysRemaining
|
|
782
|
+
} as ICertificateExpiring);
|
|
542
783
|
|
|
543
784
|
// Start renewal process
|
|
544
785
|
this.obtainCertificate(domain, true).catch(err => {
|
|
545
|
-
|
|
786
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
787
|
+
console.error(`Error renewing certificate for ${domain}:`, errorMessage);
|
|
546
788
|
});
|
|
547
789
|
}
|
|
548
790
|
}
|
|
549
791
|
}
|
|
550
792
|
|
|
793
|
+
/**
|
|
794
|
+
* Extract expiry date from certificate using a more robust approach
|
|
795
|
+
* @param certificate Certificate PEM string
|
|
796
|
+
* @param domain Domain for logging
|
|
797
|
+
* @returns Extracted expiry date or default
|
|
798
|
+
*/
|
|
799
|
+
private extractExpiryDateFromCertificate(certificate: string, domain: string): Date {
|
|
800
|
+
try {
|
|
801
|
+
// This is still using regex, but in a real implementation you would use
|
|
802
|
+
// a library like node-forge or x509 to properly parse the certificate
|
|
803
|
+
const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
|
|
804
|
+
if (matches && matches[1]) {
|
|
805
|
+
const expiryDate = new Date(matches[1]);
|
|
806
|
+
|
|
807
|
+
// Validate that we got a valid date
|
|
808
|
+
if (!isNaN(expiryDate.getTime())) {
|
|
809
|
+
console.log(`Certificate for ${domain} will expire on ${expiryDate.toISOString()}`);
|
|
810
|
+
return expiryDate;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
console.warn(`Could not extract valid expiry date from certificate for ${domain}, using default`);
|
|
815
|
+
return this.getDefaultExpiryDate();
|
|
816
|
+
} catch (error) {
|
|
817
|
+
console.warn(`Failed to extract expiry date from certificate for ${domain}, using default`);
|
|
818
|
+
return this.getDefaultExpiryDate();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Get a default expiry date (90 days from now)
|
|
824
|
+
* @returns Default expiry date
|
|
825
|
+
*/
|
|
826
|
+
private getDefaultExpiryDate(): Date {
|
|
827
|
+
return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); // 90 days default
|
|
828
|
+
}
|
|
829
|
+
|
|
551
830
|
/**
|
|
552
831
|
* Emits a certificate event with the certificate data
|
|
553
832
|
* @param eventType The event type to emit
|
|
554
833
|
* @param data The certificate data
|
|
555
834
|
*/
|
|
556
|
-
private emitCertificateEvent(eventType:
|
|
835
|
+
private emitCertificateEvent(eventType: Port80HandlerEvents, data: ICertificateData): void {
|
|
557
836
|
this.emit(eventType, data);
|
|
558
837
|
}
|
|
559
838
|
}
|