@push.rocks/smartproxy 3.26.0 → 3.28.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.port80handler.d.ts +96 -14
- package/dist_ts/classes.port80handler.js +330 -61
- package/dist_ts/classes.router.d.ts +85 -7
- package/dist_ts/classes.router.js +241 -15
- package/dist_ts/plugins.d.ts +4 -2
- package/dist_ts/plugins.js +5 -3
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.port80handler.ts +413 -68
- package/ts/classes.router.ts +335 -17
- package/ts/plugins.ts +5 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import * as acme from 'acme-client';
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
3
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Represents a domain certificate with various status information
|
|
5
|
+
*/
|
|
4
6
|
interface IDomainCertificate {
|
|
5
7
|
certObtained: boolean;
|
|
6
8
|
obtainingInProgress: boolean;
|
|
@@ -8,27 +10,147 @@ interface IDomainCertificate {
|
|
|
8
10
|
privateKey?: string;
|
|
9
11
|
challengeToken?: string;
|
|
10
12
|
challengeKeyAuthorization?: string;
|
|
13
|
+
expiryDate?: Date;
|
|
14
|
+
lastRenewalAttempt?: Date;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Configuration options for the ACME Certificate Manager
|
|
19
|
+
*/
|
|
20
|
+
interface IAcmeCertManagerOptions {
|
|
21
|
+
port?: number;
|
|
22
|
+
contactEmail?: string;
|
|
23
|
+
useProduction?: boolean;
|
|
24
|
+
renewThresholdDays?: number;
|
|
25
|
+
httpsRedirectPort?: number;
|
|
26
|
+
renewCheckIntervalHours?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Certificate data that can be emitted via events or set from outside
|
|
31
|
+
*/
|
|
32
|
+
interface ICertificateData {
|
|
33
|
+
domain: string;
|
|
34
|
+
certificate: string;
|
|
35
|
+
privateKey: string;
|
|
36
|
+
expiryDate: Date;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Events emitted by the ACME Certificate Manager
|
|
41
|
+
*/
|
|
42
|
+
export enum CertManagerEvents {
|
|
43
|
+
CERTIFICATE_ISSUED = 'certificate-issued',
|
|
44
|
+
CERTIFICATE_RENEWED = 'certificate-renewed',
|
|
45
|
+
CERTIFICATE_FAILED = 'certificate-failed',
|
|
46
|
+
CERTIFICATE_EXPIRING = 'certificate-expiring',
|
|
47
|
+
MANAGER_STARTED = 'manager-started',
|
|
48
|
+
MANAGER_STOPPED = 'manager-stopped',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Improved ACME Certificate Manager with event emission and external certificate management
|
|
53
|
+
*/
|
|
54
|
+
export class AcmeCertManager extends plugins.EventEmitter {
|
|
14
55
|
private domainCertificates: Map<string, IDomainCertificate>;
|
|
15
|
-
private server: http.Server;
|
|
16
|
-
private acmeClient: acme.Client | null = null;
|
|
56
|
+
private server: plugins.http.Server | null = null;
|
|
57
|
+
private acmeClient: plugins.acme.Client | null = null;
|
|
17
58
|
private accountKey: string | null = null;
|
|
59
|
+
private renewalTimer: NodeJS.Timeout | null = null;
|
|
60
|
+
private isShuttingDown: boolean = false;
|
|
61
|
+
private options: Required<IAcmeCertManagerOptions>;
|
|
18
62
|
|
|
19
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Creates a new ACME Certificate Manager
|
|
65
|
+
* @param options Configuration options
|
|
66
|
+
*/
|
|
67
|
+
constructor(options: IAcmeCertManagerOptions = {}) {
|
|
68
|
+
super();
|
|
20
69
|
this.domainCertificates = new Map<string, IDomainCertificate>();
|
|
70
|
+
|
|
71
|
+
// Default options
|
|
72
|
+
this.options = {
|
|
73
|
+
port: options.port ?? 80,
|
|
74
|
+
contactEmail: options.contactEmail ?? 'admin@example.com',
|
|
75
|
+
useProduction: options.useProduction ?? false, // Safer default: staging
|
|
76
|
+
renewThresholdDays: options.renewThresholdDays ?? 30,
|
|
77
|
+
httpsRedirectPort: options.httpsRedirectPort ?? 443,
|
|
78
|
+
renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Starts the HTTP server for ACME challenges
|
|
84
|
+
*/
|
|
85
|
+
public async start(): Promise<void> {
|
|
86
|
+
if (this.server) {
|
|
87
|
+
throw new Error('Server is already running');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (this.isShuttingDown) {
|
|
91
|
+
throw new Error('Server is shutting down');
|
|
92
|
+
}
|
|
21
93
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
try {
|
|
96
|
+
this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res));
|
|
97
|
+
|
|
98
|
+
this.server.on('error', (error: NodeJS.ErrnoException) => {
|
|
99
|
+
if (error.code === 'EACCES') {
|
|
100
|
+
reject(new Error(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`));
|
|
101
|
+
} else if (error.code === 'EADDRINUSE') {
|
|
102
|
+
reject(new Error(`Port ${this.options.port} is already in use.`));
|
|
103
|
+
} else {
|
|
104
|
+
reject(error);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.server.listen(this.options.port, () => {
|
|
109
|
+
console.log(`AcmeCertManager is listening on port ${this.options.port}`);
|
|
110
|
+
this.startRenewalTimer();
|
|
111
|
+
this.emit(CertManagerEvents.MANAGER_STARTED, this.options.port);
|
|
112
|
+
resolve();
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
reject(error);
|
|
116
|
+
}
|
|
26
117
|
});
|
|
27
118
|
}
|
|
28
119
|
|
|
29
120
|
/**
|
|
30
|
-
*
|
|
31
|
-
|
|
121
|
+
* Stops the HTTP server and renewal timer
|
|
122
|
+
*/
|
|
123
|
+
public async stop(): Promise<void> {
|
|
124
|
+
if (!this.server) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.isShuttingDown = true;
|
|
129
|
+
|
|
130
|
+
// Stop the renewal timer
|
|
131
|
+
if (this.renewalTimer) {
|
|
132
|
+
clearInterval(this.renewalTimer);
|
|
133
|
+
this.renewalTimer = null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return new Promise<void>((resolve) => {
|
|
137
|
+
if (this.server) {
|
|
138
|
+
this.server.close(() => {
|
|
139
|
+
this.server = null;
|
|
140
|
+
this.isShuttingDown = false;
|
|
141
|
+
this.emit(CertManagerEvents.MANAGER_STOPPED);
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
this.isShuttingDown = false;
|
|
146
|
+
resolve();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Adds a domain to be managed for certificates
|
|
153
|
+
* @param domain The domain to add
|
|
32
154
|
*/
|
|
33
155
|
public addDomain(domain: string): void {
|
|
34
156
|
if (!this.domainCertificates.has(domain)) {
|
|
@@ -38,55 +160,126 @@ export class Port80Handler {
|
|
|
38
160
|
}
|
|
39
161
|
|
|
40
162
|
/**
|
|
41
|
-
* Removes a domain from management
|
|
42
|
-
* @param domain The domain to remove
|
|
163
|
+
* Removes a domain from management
|
|
164
|
+
* @param domain The domain to remove
|
|
43
165
|
*/
|
|
44
166
|
public removeDomain(domain: string): void {
|
|
45
167
|
if (this.domainCertificates.delete(domain)) {
|
|
46
168
|
console.log(`Domain removed: ${domain}`);
|
|
47
169
|
}
|
|
48
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Sets a certificate for a domain directly (for externally obtained certificates)
|
|
174
|
+
* @param domain The domain for the certificate
|
|
175
|
+
* @param certificate The certificate (PEM format)
|
|
176
|
+
* @param privateKey The private key (PEM format)
|
|
177
|
+
* @param expiryDate Optional expiry date
|
|
178
|
+
*/
|
|
179
|
+
public setCertificate(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
|
|
180
|
+
let domainInfo = this.domainCertificates.get(domain);
|
|
181
|
+
|
|
182
|
+
if (!domainInfo) {
|
|
183
|
+
domainInfo = { certObtained: false, obtainingInProgress: false };
|
|
184
|
+
this.domainCertificates.set(domain, domainInfo);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
domainInfo.certificate = certificate;
|
|
188
|
+
domainInfo.privateKey = privateKey;
|
|
189
|
+
domainInfo.certObtained = true;
|
|
190
|
+
domainInfo.obtainingInProgress = false;
|
|
191
|
+
|
|
192
|
+
if (expiryDate) {
|
|
193
|
+
domainInfo.expiryDate = expiryDate;
|
|
194
|
+
} else {
|
|
195
|
+
// Try to extract expiry date from certificate
|
|
196
|
+
try {
|
|
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
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(`Certificate set for ${domain}`);
|
|
209
|
+
|
|
210
|
+
// Emit certificate event
|
|
211
|
+
this.emitCertificateEvent(CertManagerEvents.CERTIFICATE_ISSUED, {
|
|
212
|
+
domain,
|
|
213
|
+
certificate,
|
|
214
|
+
privateKey,
|
|
215
|
+
expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Gets the certificate for a domain if it exists
|
|
221
|
+
* @param domain The domain to get the certificate for
|
|
222
|
+
*/
|
|
223
|
+
public getCertificate(domain: string): ICertificateData | null {
|
|
224
|
+
const domainInfo = this.domainCertificates.get(domain);
|
|
225
|
+
|
|
226
|
+
if (!domainInfo || !domainInfo.certObtained || !domainInfo.certificate || !domainInfo.privateKey) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
domain,
|
|
232
|
+
certificate: domainInfo.certificate,
|
|
233
|
+
privateKey: domainInfo.privateKey,
|
|
234
|
+
expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
|
|
235
|
+
};
|
|
236
|
+
}
|
|
49
237
|
|
|
50
238
|
/**
|
|
51
|
-
* Lazy initialization of the ACME client
|
|
52
|
-
*
|
|
239
|
+
* Lazy initialization of the ACME client
|
|
240
|
+
* @returns An ACME client instance
|
|
53
241
|
*/
|
|
54
|
-
private async getAcmeClient(): Promise<acme.Client> {
|
|
242
|
+
private async getAcmeClient(): Promise<plugins.acme.Client> {
|
|
55
243
|
if (this.acmeClient) {
|
|
56
244
|
return this.acmeClient;
|
|
57
245
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
246
|
+
|
|
247
|
+
// Generate a new account key
|
|
248
|
+
this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
|
|
249
|
+
|
|
250
|
+
this.acmeClient = new plugins.acme.Client({
|
|
251
|
+
directoryUrl: this.options.useProduction
|
|
252
|
+
? plugins.acme.directory.letsencrypt.production
|
|
253
|
+
: plugins.acme.directory.letsencrypt.staging,
|
|
64
254
|
accountKey: this.accountKey,
|
|
65
255
|
});
|
|
66
|
-
|
|
256
|
+
|
|
257
|
+
// Create a new account
|
|
67
258
|
await this.acmeClient.createAccount({
|
|
68
259
|
termsOfServiceAgreed: true,
|
|
69
|
-
contact: [
|
|
260
|
+
contact: [`mailto:${this.options.contactEmail}`],
|
|
70
261
|
});
|
|
262
|
+
|
|
71
263
|
return this.acmeClient;
|
|
72
264
|
}
|
|
73
265
|
|
|
74
266
|
/**
|
|
75
|
-
* Handles incoming HTTP requests
|
|
76
|
-
*
|
|
77
|
-
*
|
|
267
|
+
* Handles incoming HTTP requests
|
|
268
|
+
* @param req The HTTP request
|
|
269
|
+
* @param res The HTTP response
|
|
78
270
|
*/
|
|
79
|
-
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
271
|
+
private handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
|
80
272
|
const hostHeader = req.headers.host;
|
|
81
273
|
if (!hostHeader) {
|
|
82
274
|
res.statusCode = 400;
|
|
83
275
|
res.end('Bad Request: Host header is missing');
|
|
84
276
|
return;
|
|
85
277
|
}
|
|
278
|
+
|
|
86
279
|
// Extract domain (ignoring any port in the Host header)
|
|
87
280
|
const domain = hostHeader.split(':')[0];
|
|
88
281
|
|
|
89
|
-
// If the request is for an ACME HTTP-01 challenge, handle it
|
|
282
|
+
// If the request is for an ACME HTTP-01 challenge, handle it
|
|
90
283
|
if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
|
|
91
284
|
this.handleAcmeChallenge(req, res, domain);
|
|
92
285
|
return;
|
|
@@ -100,38 +293,47 @@ export class Port80Handler {
|
|
|
100
293
|
|
|
101
294
|
const domainInfo = this.domainCertificates.get(domain)!;
|
|
102
295
|
|
|
103
|
-
// If certificate exists, redirect to HTTPS
|
|
296
|
+
// If certificate exists, redirect to HTTPS
|
|
104
297
|
if (domainInfo.certObtained) {
|
|
105
|
-
const
|
|
298
|
+
const httpsPort = this.options.httpsRedirectPort;
|
|
299
|
+
const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`;
|
|
300
|
+
const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`;
|
|
301
|
+
|
|
106
302
|
res.statusCode = 301;
|
|
107
303
|
res.setHeader('Location', redirectUrl);
|
|
108
304
|
res.end(`Redirecting to ${redirectUrl}`);
|
|
109
305
|
} else {
|
|
110
|
-
// Trigger certificate issuance if not already running
|
|
306
|
+
// Trigger certificate issuance if not already running
|
|
111
307
|
if (!domainInfo.obtainingInProgress) {
|
|
112
|
-
domainInfo.obtainingInProgress = true;
|
|
113
308
|
this.obtainCertificate(domain).catch(err => {
|
|
309
|
+
this.emit(CertManagerEvents.CERTIFICATE_FAILED, { domain, error: err.message });
|
|
114
310
|
console.error(`Error obtaining certificate for ${domain}:`, err);
|
|
115
311
|
});
|
|
116
312
|
}
|
|
313
|
+
|
|
117
314
|
res.statusCode = 503;
|
|
118
315
|
res.end('Certificate issuance in progress, please try again later.');
|
|
119
316
|
}
|
|
120
317
|
}
|
|
121
318
|
|
|
122
319
|
/**
|
|
123
|
-
* Serves the ACME HTTP-01 challenge response
|
|
320
|
+
* Serves the ACME HTTP-01 challenge response
|
|
321
|
+
* @param req The HTTP request
|
|
322
|
+
* @param res The HTTP response
|
|
323
|
+
* @param domain The domain for the challenge
|
|
124
324
|
*/
|
|
125
|
-
private handleAcmeChallenge(req: http.IncomingMessage, res: http.ServerResponse, domain: string): void {
|
|
325
|
+
private handleAcmeChallenge(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, domain: string): void {
|
|
126
326
|
const domainInfo = this.domainCertificates.get(domain);
|
|
127
327
|
if (!domainInfo) {
|
|
128
328
|
res.statusCode = 404;
|
|
129
329
|
res.end('Domain not configured');
|
|
130
330
|
return;
|
|
131
331
|
}
|
|
132
|
-
|
|
332
|
+
|
|
333
|
+
// The token is the last part of the URL
|
|
133
334
|
const urlParts = req.url?.split('/');
|
|
134
335
|
const token = urlParts ? urlParts[urlParts.length - 1] : '';
|
|
336
|
+
|
|
135
337
|
if (domainInfo.challengeToken === token && domainInfo.challengeKeyAuthorization) {
|
|
136
338
|
res.statusCode = 200;
|
|
137
339
|
res.setHeader('Content-Type', 'text/plain');
|
|
@@ -144,71 +346,214 @@ export class Port80Handler {
|
|
|
144
346
|
}
|
|
145
347
|
|
|
146
348
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
349
|
+
* Obtains a certificate for a domain using ACME HTTP-01 challenge
|
|
350
|
+
* @param domain The domain to obtain a certificate for
|
|
351
|
+
* @param isRenewal Whether this is a renewal attempt
|
|
149
352
|
*/
|
|
150
|
-
private async obtainCertificate(domain: string): Promise<void> {
|
|
353
|
+
private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
|
|
354
|
+
// Get the domain info
|
|
355
|
+
const domainInfo = this.domainCertificates.get(domain);
|
|
356
|
+
if (!domainInfo) {
|
|
357
|
+
throw new Error(`Domain not found: ${domain}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Prevent concurrent certificate issuance
|
|
361
|
+
if (domainInfo.obtainingInProgress) {
|
|
362
|
+
console.log(`Certificate issuance already in progress for ${domain}`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
domainInfo.obtainingInProgress = true;
|
|
367
|
+
domainInfo.lastRenewalAttempt = new Date();
|
|
368
|
+
|
|
151
369
|
try {
|
|
152
370
|
const client = await this.getAcmeClient();
|
|
153
371
|
|
|
154
|
-
// Create a new order for the domain
|
|
372
|
+
// Create a new order for the domain
|
|
155
373
|
const order = await client.createOrder({
|
|
156
374
|
identifiers: [{ type: 'dns', value: domain }],
|
|
157
375
|
});
|
|
158
376
|
|
|
159
|
-
// Get the authorizations for the order
|
|
377
|
+
// Get the authorizations for the order
|
|
160
378
|
const authorizations = await client.getAuthorizations(order);
|
|
379
|
+
|
|
161
380
|
for (const authz of authorizations) {
|
|
162
381
|
const challenge = authz.challenges.find(ch => ch.type === 'http-01');
|
|
163
382
|
if (!challenge) {
|
|
164
383
|
throw new Error('HTTP-01 challenge not found');
|
|
165
384
|
}
|
|
166
|
-
|
|
385
|
+
|
|
386
|
+
// Get the key authorization for the challenge
|
|
167
387
|
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
|
168
|
-
|
|
388
|
+
|
|
389
|
+
// Store the challenge data
|
|
169
390
|
domainInfo.challengeToken = challenge.token;
|
|
170
391
|
domainInfo.challengeKeyAuthorization = keyAuthorization;
|
|
171
392
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
}
|
|
182
413
|
}
|
|
183
414
|
|
|
184
|
-
// Generate a CSR and
|
|
185
|
-
|
|
186
|
-
const [csrBuffer, privateKeyBuffer] = await acme.forge.createCsr({
|
|
415
|
+
// Generate a CSR and private key
|
|
416
|
+
const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({
|
|
187
417
|
commonName: domain,
|
|
188
418
|
});
|
|
419
|
+
|
|
189
420
|
const csr = csrBuffer.toString();
|
|
190
421
|
const privateKey = privateKeyBuffer.toString();
|
|
191
422
|
|
|
192
|
-
// Finalize the order
|
|
423
|
+
// Finalize the order with our CSR
|
|
193
424
|
await client.finalizeOrder(order, csr);
|
|
425
|
+
|
|
426
|
+
// Get the certificate with the full chain
|
|
194
427
|
const certificate = await client.getCertificate(order);
|
|
195
428
|
|
|
196
|
-
|
|
429
|
+
// Store the certificate and key
|
|
197
430
|
domainInfo.certificate = certificate;
|
|
198
431
|
domainInfo.privateKey = privateKey;
|
|
199
432
|
domainInfo.certObtained = true;
|
|
200
|
-
|
|
433
|
+
|
|
434
|
+
// Clear challenge data
|
|
201
435
|
delete domainInfo.challengeToken;
|
|
202
436
|
delete domainInfo.challengeKeyAuthorization;
|
|
437
|
+
|
|
438
|
+
// Extract expiry date from certificate
|
|
439
|
+
try {
|
|
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
|
+
}
|
|
203
448
|
|
|
204
|
-
console.log(`Certificate obtained for ${domain}`);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
449
|
+
console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
|
|
450
|
+
|
|
451
|
+
// Emit the appropriate event
|
|
452
|
+
const eventType = isRenewal
|
|
453
|
+
? CertManagerEvents.CERTIFICATE_RENEWED
|
|
454
|
+
: CertManagerEvents.CERTIFICATE_ISSUED;
|
|
455
|
+
|
|
456
|
+
this.emitCertificateEvent(eventType, {
|
|
457
|
+
domain,
|
|
458
|
+
certificate,
|
|
459
|
+
privateKey,
|
|
460
|
+
expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
} catch (error: any) {
|
|
464
|
+
// Check for rate limit errors
|
|
465
|
+
if (error.message && (
|
|
466
|
+
error.message.includes('rateLimited') ||
|
|
467
|
+
error.message.includes('too many certificates') ||
|
|
468
|
+
error.message.includes('rate limit')
|
|
469
|
+
)) {
|
|
470
|
+
console.error(`Rate limit reached for ${domain}. Waiting before retry.`);
|
|
471
|
+
} else {
|
|
472
|
+
console.error(`Error during certificate issuance for ${domain}:`, error);
|
|
211
473
|
}
|
|
474
|
+
|
|
475
|
+
// Emit failure event
|
|
476
|
+
this.emit(CertManagerEvents.CERTIFICATE_FAILED, {
|
|
477
|
+
domain,
|
|
478
|
+
error: error.message || 'Unknown error',
|
|
479
|
+
isRenewal
|
|
480
|
+
});
|
|
481
|
+
} finally {
|
|
482
|
+
// Reset flag whether successful or not
|
|
483
|
+
domainInfo.obtainingInProgress = false;
|
|
212
484
|
}
|
|
213
485
|
}
|
|
214
|
-
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Starts the certificate renewal timer
|
|
489
|
+
*/
|
|
490
|
+
private startRenewalTimer(): void {
|
|
491
|
+
if (this.renewalTimer) {
|
|
492
|
+
clearInterval(this.renewalTimer);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Convert hours to milliseconds
|
|
496
|
+
const checkInterval = this.options.renewCheckIntervalHours * 60 * 60 * 1000;
|
|
497
|
+
|
|
498
|
+
this.renewalTimer = setInterval(() => this.checkForRenewals(), checkInterval);
|
|
499
|
+
|
|
500
|
+
// Prevent the timer from keeping the process alive
|
|
501
|
+
if (this.renewalTimer.unref) {
|
|
502
|
+
this.renewalTimer.unref();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
console.log(`Certificate renewal check scheduled every ${this.options.renewCheckIntervalHours} hours`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Checks for certificates that need renewal
|
|
510
|
+
*/
|
|
511
|
+
private checkForRenewals(): void {
|
|
512
|
+
if (this.isShuttingDown) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
console.log('Checking for certificates that need renewal...');
|
|
517
|
+
|
|
518
|
+
const now = new Date();
|
|
519
|
+
const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000;
|
|
520
|
+
|
|
521
|
+
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
|
|
522
|
+
// Skip domains without certificates or already in renewal
|
|
523
|
+
if (!domainInfo.certObtained || domainInfo.obtainingInProgress) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Skip domains without expiry dates
|
|
528
|
+
if (!domainInfo.expiryDate) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const timeUntilExpiry = domainInfo.expiryDate.getTime() - now.getTime();
|
|
533
|
+
|
|
534
|
+
// Check if certificate is near expiry
|
|
535
|
+
if (timeUntilExpiry <= renewThresholdMs) {
|
|
536
|
+
console.log(`Certificate for ${domain} expires soon, renewing...`);
|
|
537
|
+
this.emit(CertManagerEvents.CERTIFICATE_EXPIRING, {
|
|
538
|
+
domain,
|
|
539
|
+
expiryDate: domainInfo.expiryDate,
|
|
540
|
+
daysRemaining: Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000))
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Start renewal process
|
|
544
|
+
this.obtainCertificate(domain, true).catch(err => {
|
|
545
|
+
console.error(`Error renewing certificate for ${domain}:`, err);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Emits a certificate event with the certificate data
|
|
553
|
+
* @param eventType The event type to emit
|
|
554
|
+
* @param data The certificate data
|
|
555
|
+
*/
|
|
556
|
+
private emitCertificateEvent(eventType: CertManagerEvents, data: ICertificateData): void {
|
|
557
|
+
this.emit(eventType, data);
|
|
558
|
+
}
|
|
559
|
+
}
|