@push.rocks/smartproxy 18.2.0 → 19.2.2
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/common/eventUtils.d.ts +1 -2
- package/dist_ts/common/eventUtils.js +2 -1
- package/dist_ts/core/models/common-types.d.ts +1 -1
- package/dist_ts/core/models/common-types.js +1 -1
- package/dist_ts/core/utils/event-utils.d.ts +9 -9
- package/dist_ts/core/utils/event-utils.js +6 -14
- package/dist_ts/http/models/http-types.d.ts +13 -1
- package/dist_ts/http/models/http-types.js +1 -1
- package/dist_ts/index.d.ts +4 -6
- package/dist_ts/index.js +4 -10
- package/dist_ts/proxies/index.d.ts +3 -2
- package/dist_ts/proxies/index.js +4 -5
- package/dist_ts/proxies/network-proxy/certificate-manager.d.ts +31 -49
- package/dist_ts/proxies/network-proxy/certificate-manager.js +77 -374
- package/dist_ts/proxies/network-proxy/models/types.d.ts +12 -1
- package/dist_ts/proxies/network-proxy/models/types.js +1 -1
- package/dist_ts/proxies/network-proxy/network-proxy.d.ts +2 -7
- package/dist_ts/proxies/network-proxy/network-proxy.js +10 -19
- package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +6 -0
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +24 -5
- package/dist_ts/proxies/smart-proxy/models/index.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/models/index.js +1 -5
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +30 -1
- package/dist_ts/proxies/smart-proxy/route-manager.d.ts +4 -0
- package/dist_ts/proxies/smart-proxy/route-manager.js +7 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +4 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +112 -26
- package/package.json +1 -2
- package/readme.hints.md +31 -1
- package/readme.md +82 -6
- package/readme.plan.md +109 -1417
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/common/eventUtils.ts +2 -2
- package/ts/core/models/common-types.ts +1 -1
- package/ts/core/utils/event-utils.ts +12 -21
- package/ts/http/models/http-types.ts +8 -4
- package/ts/index.ts +11 -14
- package/ts/proxies/index.ts +7 -4
- package/ts/proxies/network-proxy/certificate-manager.ts +92 -417
- package/ts/proxies/network-proxy/models/types.ts +14 -2
- package/ts/proxies/network-proxy/network-proxy.ts +10 -19
- package/ts/proxies/smart-proxy/certificate-manager.ts +31 -4
- package/ts/proxies/smart-proxy/models/index.ts +2 -1
- package/ts/proxies/smart-proxy/models/interfaces.ts +31 -2
- package/ts/proxies/smart-proxy/models/route-types.ts +1 -1
- package/ts/proxies/smart-proxy/route-manager.ts +7 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +142 -25
- package/ts/certificate/acme/acme-factory.ts +0 -48
- package/ts/certificate/acme/challenge-handler.ts +0 -110
- package/ts/certificate/acme/index.ts +0 -3
- package/ts/certificate/events/certificate-events.ts +0 -36
- package/ts/certificate/index.ts +0 -75
- package/ts/certificate/models/certificate-types.ts +0 -109
- package/ts/certificate/providers/cert-provisioner.ts +0 -519
- package/ts/certificate/providers/index.ts +0 -3
- package/ts/certificate/storage/file-storage.ts +0 -234
- package/ts/certificate/storage/index.ts +0 -3
- package/ts/certificate/utils/certificate-helpers.ts +0 -50
- package/ts/http/port80/acme-interfaces.ts +0 -169
- package/ts/http/port80/challenge-responder.ts +0 -246
- package/ts/http/port80/index.ts +0 -13
- package/ts/http/port80/port80-handler.ts +0 -728
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as plugins from '../../plugins.js';
|
|
4
|
-
import type { ICertificateData, ICertificates } from '../models/certificate-types.js';
|
|
5
|
-
import { ensureCertificateDirectory } from '../utils/certificate-helpers.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* FileStorage provides file system storage for certificates
|
|
9
|
-
*/
|
|
10
|
-
export class FileStorage {
|
|
11
|
-
private storageDir: string;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Creates a new file storage provider
|
|
15
|
-
* @param storageDir Directory to store certificates
|
|
16
|
-
*/
|
|
17
|
-
constructor(storageDir: string) {
|
|
18
|
-
this.storageDir = path.resolve(storageDir);
|
|
19
|
-
ensureCertificateDirectory(this.storageDir);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Save a certificate to the file system
|
|
24
|
-
* @param domain Domain name
|
|
25
|
-
* @param certData Certificate data to save
|
|
26
|
-
*/
|
|
27
|
-
public async saveCertificate(domain: string, certData: ICertificateData): Promise<void> {
|
|
28
|
-
const sanitizedDomain = this.sanitizeDomain(domain);
|
|
29
|
-
const certDir = path.join(this.storageDir, sanitizedDomain);
|
|
30
|
-
ensureCertificateDirectory(certDir);
|
|
31
|
-
|
|
32
|
-
const certPath = path.join(certDir, 'fullchain.pem');
|
|
33
|
-
const keyPath = path.join(certDir, 'privkey.pem');
|
|
34
|
-
const metaPath = path.join(certDir, 'metadata.json');
|
|
35
|
-
|
|
36
|
-
// Write certificate and private key
|
|
37
|
-
await fs.promises.writeFile(certPath, certData.certificate, 'utf8');
|
|
38
|
-
await fs.promises.writeFile(keyPath, certData.privateKey, 'utf8');
|
|
39
|
-
|
|
40
|
-
// Write metadata
|
|
41
|
-
const metadata = {
|
|
42
|
-
domain: certData.domain,
|
|
43
|
-
expiryDate: certData.expiryDate.toISOString(),
|
|
44
|
-
source: certData.source || 'unknown',
|
|
45
|
-
issuedAt: new Date().toISOString()
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
await fs.promises.writeFile(
|
|
49
|
-
metaPath,
|
|
50
|
-
JSON.stringify(metadata, null, 2),
|
|
51
|
-
'utf8'
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Load a certificate from the file system
|
|
57
|
-
* @param domain Domain name
|
|
58
|
-
* @returns Certificate data if found, null otherwise
|
|
59
|
-
*/
|
|
60
|
-
public async loadCertificate(domain: string): Promise<ICertificateData | null> {
|
|
61
|
-
const sanitizedDomain = this.sanitizeDomain(domain);
|
|
62
|
-
const certDir = path.join(this.storageDir, sanitizedDomain);
|
|
63
|
-
|
|
64
|
-
if (!fs.existsSync(certDir)) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const certPath = path.join(certDir, 'fullchain.pem');
|
|
69
|
-
const keyPath = path.join(certDir, 'privkey.pem');
|
|
70
|
-
const metaPath = path.join(certDir, 'metadata.json');
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
// Check if all required files exist
|
|
74
|
-
if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Read certificate and private key
|
|
79
|
-
const certificate = await fs.promises.readFile(certPath, 'utf8');
|
|
80
|
-
const privateKey = await fs.promises.readFile(keyPath, 'utf8');
|
|
81
|
-
|
|
82
|
-
// Try to read metadata if available
|
|
83
|
-
let expiryDate = new Date();
|
|
84
|
-
let source: 'static' | 'http01' | 'dns01' | undefined;
|
|
85
|
-
|
|
86
|
-
if (fs.existsSync(metaPath)) {
|
|
87
|
-
const metaContent = await fs.promises.readFile(metaPath, 'utf8');
|
|
88
|
-
const metadata = JSON.parse(metaContent);
|
|
89
|
-
|
|
90
|
-
if (metadata.expiryDate) {
|
|
91
|
-
expiryDate = new Date(metadata.expiryDate);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (metadata.source) {
|
|
95
|
-
source = metadata.source as 'static' | 'http01' | 'dns01';
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
domain,
|
|
101
|
-
certificate,
|
|
102
|
-
privateKey,
|
|
103
|
-
expiryDate,
|
|
104
|
-
source
|
|
105
|
-
};
|
|
106
|
-
} catch (error) {
|
|
107
|
-
console.error(`Error loading certificate for ${domain}:`, error);
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Delete a certificate from the file system
|
|
114
|
-
* @param domain Domain name
|
|
115
|
-
*/
|
|
116
|
-
public async deleteCertificate(domain: string): Promise<boolean> {
|
|
117
|
-
const sanitizedDomain = this.sanitizeDomain(domain);
|
|
118
|
-
const certDir = path.join(this.storageDir, sanitizedDomain);
|
|
119
|
-
|
|
120
|
-
if (!fs.existsSync(certDir)) {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
// Recursively delete the certificate directory
|
|
126
|
-
await this.deleteDirectory(certDir);
|
|
127
|
-
return true;
|
|
128
|
-
} catch (error) {
|
|
129
|
-
console.error(`Error deleting certificate for ${domain}:`, error);
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* List all domains with stored certificates
|
|
136
|
-
* @returns Array of domain names
|
|
137
|
-
*/
|
|
138
|
-
public async listCertificates(): Promise<string[]> {
|
|
139
|
-
try {
|
|
140
|
-
const entries = await fs.promises.readdir(this.storageDir, { withFileTypes: true });
|
|
141
|
-
return entries
|
|
142
|
-
.filter(entry => entry.isDirectory())
|
|
143
|
-
.map(entry => entry.name);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
console.error('Error listing certificates:', error);
|
|
146
|
-
return [];
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Check if a certificate is expiring soon
|
|
152
|
-
* @param domain Domain name
|
|
153
|
-
* @param thresholdDays Days threshold to consider expiring
|
|
154
|
-
* @returns Information about expiring certificate or null
|
|
155
|
-
*/
|
|
156
|
-
public async isExpiringSoon(
|
|
157
|
-
domain: string,
|
|
158
|
-
thresholdDays: number = 30
|
|
159
|
-
): Promise<{ domain: string; expiryDate: Date; daysRemaining: number } | null> {
|
|
160
|
-
const certData = await this.loadCertificate(domain);
|
|
161
|
-
|
|
162
|
-
if (!certData) {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const now = new Date();
|
|
167
|
-
const expiryDate = certData.expiryDate;
|
|
168
|
-
const timeRemaining = expiryDate.getTime() - now.getTime();
|
|
169
|
-
const daysRemaining = Math.floor(timeRemaining / (1000 * 60 * 60 * 24));
|
|
170
|
-
|
|
171
|
-
if (daysRemaining <= thresholdDays) {
|
|
172
|
-
return {
|
|
173
|
-
domain,
|
|
174
|
-
expiryDate,
|
|
175
|
-
daysRemaining
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Check all certificates for expiration
|
|
184
|
-
* @param thresholdDays Days threshold to consider expiring
|
|
185
|
-
* @returns List of expiring certificates
|
|
186
|
-
*/
|
|
187
|
-
public async getExpiringCertificates(
|
|
188
|
-
thresholdDays: number = 30
|
|
189
|
-
): Promise<Array<{ domain: string; expiryDate: Date; daysRemaining: number }>> {
|
|
190
|
-
const domains = await this.listCertificates();
|
|
191
|
-
const expiringCerts = [];
|
|
192
|
-
|
|
193
|
-
for (const domain of domains) {
|
|
194
|
-
const expiring = await this.isExpiringSoon(domain, thresholdDays);
|
|
195
|
-
if (expiring) {
|
|
196
|
-
expiringCerts.push(expiring);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return expiringCerts;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Delete a directory recursively
|
|
205
|
-
* @param directoryPath Directory to delete
|
|
206
|
-
*/
|
|
207
|
-
private async deleteDirectory(directoryPath: string): Promise<void> {
|
|
208
|
-
if (fs.existsSync(directoryPath)) {
|
|
209
|
-
const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true });
|
|
210
|
-
|
|
211
|
-
for (const entry of entries) {
|
|
212
|
-
const fullPath = path.join(directoryPath, entry.name);
|
|
213
|
-
|
|
214
|
-
if (entry.isDirectory()) {
|
|
215
|
-
await this.deleteDirectory(fullPath);
|
|
216
|
-
} else {
|
|
217
|
-
await fs.promises.unlink(fullPath);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
await fs.promises.rmdir(directoryPath);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Sanitize a domain name for use as a directory name
|
|
227
|
-
* @param domain Domain name
|
|
228
|
-
* @returns Sanitized domain name
|
|
229
|
-
*/
|
|
230
|
-
private sanitizeDomain(domain: string): string {
|
|
231
|
-
// Replace wildcard and any invalid filesystem characters
|
|
232
|
-
return domain.replace(/\*/g, '_wildcard_').replace(/[/\\:*?"<>|]/g, '_');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import type { ICertificates } from '../models/certificate-types.js';
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Loads the default SSL certificates from the assets directory
|
|
10
|
-
* @returns The certificate key pair
|
|
11
|
-
*/
|
|
12
|
-
export function loadDefaultCertificates(): ICertificates {
|
|
13
|
-
try {
|
|
14
|
-
// Need to adjust path from /ts/certificate/utils to /assets/certs
|
|
15
|
-
const certPath = path.join(__dirname, '..', '..', '..', 'assets', 'certs');
|
|
16
|
-
const privateKey = fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8');
|
|
17
|
-
const publicKey = fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8');
|
|
18
|
-
|
|
19
|
-
if (!privateKey || !publicKey) {
|
|
20
|
-
throw new Error('Failed to load default certificates');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
privateKey,
|
|
25
|
-
publicKey
|
|
26
|
-
};
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error('Error loading default certificates:', error);
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Checks if a certificate file exists at the specified path
|
|
35
|
-
* @param certPath Path to check for certificate
|
|
36
|
-
* @returns True if the certificate exists, false otherwise
|
|
37
|
-
*/
|
|
38
|
-
export function certificateExists(certPath: string): boolean {
|
|
39
|
-
return fs.existsSync(certPath);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Ensures the certificate directory exists
|
|
44
|
-
* @param dirPath Path to the certificate directory
|
|
45
|
-
*/
|
|
46
|
-
export function ensureCertificateDirectory(dirPath: string): void {
|
|
47
|
-
if (!fs.existsSync(dirPath)) {
|
|
48
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type definitions for SmartAcme interfaces used by ChallengeResponder
|
|
3
|
-
* These reflect the actual SmartAcme API based on the documentation
|
|
4
|
-
*
|
|
5
|
-
* Also includes route-based interfaces for Port80Handler to extract domains
|
|
6
|
-
* that need certificate management from route configurations.
|
|
7
|
-
*/
|
|
8
|
-
import * as plugins from '../../plugins.js';
|
|
9
|
-
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Structure for SmartAcme certificate result
|
|
13
|
-
*/
|
|
14
|
-
export interface ISmartAcmeCert {
|
|
15
|
-
id?: string;
|
|
16
|
-
domainName: string;
|
|
17
|
-
created?: number | Date | string;
|
|
18
|
-
privateKey: string;
|
|
19
|
-
publicKey: string;
|
|
20
|
-
csr?: string;
|
|
21
|
-
validUntil: number | Date | string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Structure for SmartAcme options
|
|
26
|
-
*/
|
|
27
|
-
export interface ISmartAcmeOptions {
|
|
28
|
-
accountEmail: string;
|
|
29
|
-
certManager: ICertManager;
|
|
30
|
-
environment: 'production' | 'integration';
|
|
31
|
-
challengeHandlers: IChallengeHandler<any>[];
|
|
32
|
-
challengePriority?: string[];
|
|
33
|
-
retryOptions?: {
|
|
34
|
-
retries?: number;
|
|
35
|
-
factor?: number;
|
|
36
|
-
minTimeoutMs?: number;
|
|
37
|
-
maxTimeoutMs?: number;
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Interface for certificate manager
|
|
43
|
-
*/
|
|
44
|
-
export interface ICertManager {
|
|
45
|
-
init(): Promise<void>;
|
|
46
|
-
get(domainName: string): Promise<ISmartAcmeCert | null>;
|
|
47
|
-
put(cert: ISmartAcmeCert): Promise<ISmartAcmeCert>;
|
|
48
|
-
delete(domainName: string): Promise<void>;
|
|
49
|
-
close?(): Promise<void>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Interface for challenge handler
|
|
54
|
-
*/
|
|
55
|
-
export interface IChallengeHandler<T> {
|
|
56
|
-
getSupportedTypes(): string[];
|
|
57
|
-
prepare(ch: T): Promise<void>;
|
|
58
|
-
verify?(ch: T): Promise<void>;
|
|
59
|
-
cleanup(ch: T): Promise<void>;
|
|
60
|
-
checkWetherDomainIsSupported(domain: string): Promise<boolean>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* HTTP-01 challenge type
|
|
65
|
-
*/
|
|
66
|
-
export interface IHttp01Challenge {
|
|
67
|
-
type: string; // 'http-01'
|
|
68
|
-
token: string;
|
|
69
|
-
keyAuthorization: string;
|
|
70
|
-
webPath: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* HTTP-01 Memory Handler Interface
|
|
75
|
-
*/
|
|
76
|
-
export interface IHttp01MemoryHandler extends IChallengeHandler<IHttp01Challenge> {
|
|
77
|
-
handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, next?: () => void): void;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* SmartAcme main class interface
|
|
82
|
-
*/
|
|
83
|
-
export interface ISmartAcme {
|
|
84
|
-
start(): Promise<void>;
|
|
85
|
-
stop(): Promise<void>;
|
|
86
|
-
getCertificateForDomain(domain: string): Promise<ISmartAcmeCert>;
|
|
87
|
-
on?(event: string, listener: (data: any) => void): void;
|
|
88
|
-
eventEmitter?: plugins.EventEmitter;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Port80Handler route options
|
|
93
|
-
*/
|
|
94
|
-
export interface IPort80RouteOptions {
|
|
95
|
-
// The domain for the certificate
|
|
96
|
-
domain: string;
|
|
97
|
-
|
|
98
|
-
// Whether to redirect HTTP to HTTPS
|
|
99
|
-
sslRedirect: boolean;
|
|
100
|
-
|
|
101
|
-
// Whether to enable ACME certificate management
|
|
102
|
-
acmeMaintenance: boolean;
|
|
103
|
-
|
|
104
|
-
// Optional target for forwarding HTTP requests
|
|
105
|
-
forward?: {
|
|
106
|
-
ip: string;
|
|
107
|
-
port: number;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// Optional target for forwarding ACME challenge requests
|
|
111
|
-
acmeForward?: {
|
|
112
|
-
ip: string;
|
|
113
|
-
port: number;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// Reference to the route that requested this certificate
|
|
117
|
-
routeReference?: {
|
|
118
|
-
routeId?: string;
|
|
119
|
-
routeName?: string;
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Extract domains that need certificate management from routes
|
|
125
|
-
* @param routes Route configurations to extract domains from
|
|
126
|
-
* @returns Array of Port80RouteOptions for each domain
|
|
127
|
-
*/
|
|
128
|
-
export function extractPort80RoutesFromRoutes(routes: IRouteConfig[]): IPort80RouteOptions[] {
|
|
129
|
-
const result: IPort80RouteOptions[] = [];
|
|
130
|
-
|
|
131
|
-
for (const route of routes) {
|
|
132
|
-
// Skip routes that don't have domains or TLS configuration
|
|
133
|
-
if (!route.match.domains || !route.action.tls) continue;
|
|
134
|
-
|
|
135
|
-
// Skip routes that don't terminate TLS
|
|
136
|
-
if (route.action.tls.mode !== 'terminate' && route.action.tls.mode !== 'terminate-and-reencrypt') continue;
|
|
137
|
-
|
|
138
|
-
// Only routes with automatic certificates need ACME
|
|
139
|
-
if (route.action.tls.certificate !== 'auto') continue;
|
|
140
|
-
|
|
141
|
-
// Get domains from route
|
|
142
|
-
const domains = Array.isArray(route.match.domains)
|
|
143
|
-
? route.match.domains
|
|
144
|
-
: [route.match.domains];
|
|
145
|
-
|
|
146
|
-
// Create Port80RouteOptions for each domain
|
|
147
|
-
for (const domain of domains) {
|
|
148
|
-
// Skip wildcards (we can't get certificates for them)
|
|
149
|
-
if (domain.includes('*')) continue;
|
|
150
|
-
|
|
151
|
-
// Create Port80RouteOptions
|
|
152
|
-
const options: IPort80RouteOptions = {
|
|
153
|
-
domain,
|
|
154
|
-
sslRedirect: true, // Default to true for HTTPS routes
|
|
155
|
-
acmeMaintenance: true, // Default to true for auto certificates
|
|
156
|
-
|
|
157
|
-
// Add route reference
|
|
158
|
-
routeReference: {
|
|
159
|
-
routeName: route.name
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
// Add domain to result
|
|
164
|
-
result.push(options);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return result;
|
|
169
|
-
}
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import * as plugins from '../../plugins.js';
|
|
2
|
-
import { IncomingMessage, ServerResponse } from 'http';
|
|
3
|
-
import {
|
|
4
|
-
CertificateEvents
|
|
5
|
-
} from '../../certificate/events/certificate-events.js';
|
|
6
|
-
import type {
|
|
7
|
-
ICertificateData,
|
|
8
|
-
ICertificateFailure,
|
|
9
|
-
ICertificateExpiring
|
|
10
|
-
} from '../../certificate/models/certificate-types.js';
|
|
11
|
-
import type {
|
|
12
|
-
ISmartAcme,
|
|
13
|
-
ISmartAcmeCert,
|
|
14
|
-
ISmartAcmeOptions,
|
|
15
|
-
IHttp01MemoryHandler
|
|
16
|
-
} from './acme-interfaces.js';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* ChallengeResponder handles ACME HTTP-01 challenges by leveraging SmartAcme
|
|
20
|
-
* It acts as a bridge between the HTTP server and the ACME challenge verification process
|
|
21
|
-
*/
|
|
22
|
-
export class ChallengeResponder extends plugins.EventEmitter {
|
|
23
|
-
private smartAcme: ISmartAcme | null = null;
|
|
24
|
-
private http01Handler: IHttp01MemoryHandler | null = null;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Creates a new challenge responder
|
|
28
|
-
* @param useProduction Whether to use production ACME servers
|
|
29
|
-
* @param email Account email for ACME
|
|
30
|
-
* @param certificateStore Directory to store certificates
|
|
31
|
-
*/
|
|
32
|
-
constructor(
|
|
33
|
-
private readonly useProduction: boolean = false,
|
|
34
|
-
private readonly email: string = 'admin@example.com',
|
|
35
|
-
private readonly certificateStore: string = './certs'
|
|
36
|
-
) {
|
|
37
|
-
super();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Initialize the ACME client
|
|
42
|
-
*/
|
|
43
|
-
public async initialize(): Promise<void> {
|
|
44
|
-
try {
|
|
45
|
-
// Create the HTTP-01 memory handler from SmartACME
|
|
46
|
-
this.http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
|
|
47
|
-
|
|
48
|
-
// Ensure certificate store directory exists
|
|
49
|
-
await this.ensureCertificateStore();
|
|
50
|
-
|
|
51
|
-
// Create a MemoryCertManager for certificate storage
|
|
52
|
-
const certManager = new plugins.smartacme.certmanagers.MemoryCertManager();
|
|
53
|
-
|
|
54
|
-
// Initialize the SmartACME client with appropriate options
|
|
55
|
-
this.smartAcme = new plugins.smartacme.SmartAcme({
|
|
56
|
-
accountEmail: this.email,
|
|
57
|
-
certManager: certManager,
|
|
58
|
-
environment: this.useProduction ? 'production' : 'integration',
|
|
59
|
-
challengeHandlers: [this.http01Handler],
|
|
60
|
-
challengePriority: ['http-01']
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Set up event forwarding from SmartAcme
|
|
64
|
-
this.setupEventListeners();
|
|
65
|
-
|
|
66
|
-
// Start the SmartACME client
|
|
67
|
-
await this.smartAcme.start();
|
|
68
|
-
console.log('ACME client initialized successfully');
|
|
69
|
-
} catch (error) {
|
|
70
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
71
|
-
throw new Error(`Failed to initialize ACME client: ${errorMessage}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Ensure the certificate store directory exists
|
|
77
|
-
*/
|
|
78
|
-
private async ensureCertificateStore(): Promise<void> {
|
|
79
|
-
try {
|
|
80
|
-
await plugins.fs.promises.mkdir(this.certificateStore, { recursive: true });
|
|
81
|
-
} catch (error) {
|
|
82
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
83
|
-
throw new Error(`Failed to create certificate store: ${errorMessage}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Setup event listeners to forward SmartACME events to our own event emitter
|
|
89
|
-
*/
|
|
90
|
-
private setupEventListeners(): void {
|
|
91
|
-
if (!this.smartAcme) return;
|
|
92
|
-
|
|
93
|
-
const setupEvents = (emitter: { on: (event: string, listener: (data: any) => void) => void }) => {
|
|
94
|
-
// Forward certificate events
|
|
95
|
-
emitter.on('certificate', (data: any) => {
|
|
96
|
-
const isRenewal = !!data.isRenewal;
|
|
97
|
-
|
|
98
|
-
const certData: ICertificateData = {
|
|
99
|
-
domain: data.domainName || data.domain,
|
|
100
|
-
certificate: data.publicKey || data.cert,
|
|
101
|
-
privateKey: data.privateKey || data.key,
|
|
102
|
-
expiryDate: new Date(data.validUntil || data.expiryDate || Date.now()),
|
|
103
|
-
source: 'http01',
|
|
104
|
-
isRenewal
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const eventType = isRenewal
|
|
108
|
-
? CertificateEvents.CERTIFICATE_RENEWED
|
|
109
|
-
: CertificateEvents.CERTIFICATE_ISSUED;
|
|
110
|
-
|
|
111
|
-
this.emit(eventType, certData);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Forward error events
|
|
115
|
-
emitter.on('error', (error: any) => {
|
|
116
|
-
const domain = error.domainName || error.domain || 'unknown';
|
|
117
|
-
const failureData: ICertificateFailure = {
|
|
118
|
-
domain,
|
|
119
|
-
error: error.message || String(error),
|
|
120
|
-
isRenewal: !!error.isRenewal
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
this.emit(CertificateEvents.CERTIFICATE_FAILED, failureData);
|
|
124
|
-
});
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// Check for direct event methods on SmartAcme
|
|
128
|
-
if (typeof this.smartAcme.on === 'function') {
|
|
129
|
-
setupEvents(this.smartAcme as any);
|
|
130
|
-
}
|
|
131
|
-
// Check for eventEmitter property
|
|
132
|
-
else if (this.smartAcme.eventEmitter) {
|
|
133
|
-
setupEvents(this.smartAcme.eventEmitter);
|
|
134
|
-
}
|
|
135
|
-
// If no proper event handling, log a warning
|
|
136
|
-
else {
|
|
137
|
-
console.warn('SmartAcme instance does not support expected event interface - events may not be forwarded');
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Handle HTTP request by checking if it's an ACME challenge
|
|
143
|
-
* @param req HTTP request object
|
|
144
|
-
* @param res HTTP response object
|
|
145
|
-
* @returns true if the request was handled, false otherwise
|
|
146
|
-
*/
|
|
147
|
-
public handleRequest(req: IncomingMessage, res: ServerResponse): boolean {
|
|
148
|
-
if (!this.http01Handler) return false;
|
|
149
|
-
|
|
150
|
-
// Check if this is an ACME challenge request (/.well-known/acme-challenge/*)
|
|
151
|
-
const url = req.url || '';
|
|
152
|
-
if (url.startsWith('/.well-known/acme-challenge/')) {
|
|
153
|
-
try {
|
|
154
|
-
// Delegate to the HTTP-01 memory handler, which knows how to serve challenges
|
|
155
|
-
this.http01Handler.handleRequest(req, res);
|
|
156
|
-
return true;
|
|
157
|
-
} catch (error) {
|
|
158
|
-
console.error('Error handling ACME challenge:', error);
|
|
159
|
-
// If there was an error, send a 404 response
|
|
160
|
-
res.writeHead(404);
|
|
161
|
-
res.end('Not found');
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Request a certificate for a domain
|
|
171
|
-
* @param domain Domain name to request a certificate for
|
|
172
|
-
* @param isRenewal Whether this is a renewal request
|
|
173
|
-
*/
|
|
174
|
-
public async requestCertificate(domain: string, isRenewal: boolean = false): Promise<ICertificateData> {
|
|
175
|
-
if (!this.smartAcme) {
|
|
176
|
-
throw new Error('ACME client not initialized');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
// Request certificate using SmartACME
|
|
181
|
-
const certObj = await this.smartAcme.getCertificateForDomain(domain);
|
|
182
|
-
|
|
183
|
-
// Convert the certificate object to our CertificateData format
|
|
184
|
-
const certData: ICertificateData = {
|
|
185
|
-
domain,
|
|
186
|
-
certificate: certObj.publicKey,
|
|
187
|
-
privateKey: certObj.privateKey,
|
|
188
|
-
expiryDate: new Date(certObj.validUntil),
|
|
189
|
-
source: 'http01',
|
|
190
|
-
isRenewal
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
return certData;
|
|
194
|
-
} catch (error) {
|
|
195
|
-
// Create failure object
|
|
196
|
-
const failure: ICertificateFailure = {
|
|
197
|
-
domain,
|
|
198
|
-
error: error instanceof Error ? error.message : String(error),
|
|
199
|
-
isRenewal
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// Emit failure event
|
|
203
|
-
this.emit(CertificateEvents.CERTIFICATE_FAILED, failure);
|
|
204
|
-
|
|
205
|
-
// Rethrow with more context
|
|
206
|
-
throw new Error(`Failed to ${isRenewal ? 'renew' : 'obtain'} certificate for ${domain}: ${
|
|
207
|
-
error instanceof Error ? error.message : String(error)
|
|
208
|
-
}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Check if a certificate is expiring soon and trigger renewal if needed
|
|
214
|
-
* @param domain Domain name
|
|
215
|
-
* @param certificate Certificate data
|
|
216
|
-
* @param thresholdDays Days before expiry to trigger renewal
|
|
217
|
-
*/
|
|
218
|
-
public checkCertificateExpiry(
|
|
219
|
-
domain: string,
|
|
220
|
-
certificate: ICertificateData,
|
|
221
|
-
thresholdDays: number = 30
|
|
222
|
-
): void {
|
|
223
|
-
if (!certificate.expiryDate) return;
|
|
224
|
-
|
|
225
|
-
const now = new Date();
|
|
226
|
-
const expiryDate = certificate.expiryDate;
|
|
227
|
-
const daysDifference = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
228
|
-
|
|
229
|
-
if (daysDifference <= thresholdDays) {
|
|
230
|
-
const expiryInfo: ICertificateExpiring = {
|
|
231
|
-
domain,
|
|
232
|
-
expiryDate,
|
|
233
|
-
daysRemaining: daysDifference
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiryInfo);
|
|
237
|
-
|
|
238
|
-
// Automatically attempt renewal if expiring
|
|
239
|
-
if (this.smartAcme) {
|
|
240
|
-
this.requestCertificate(domain, true).catch(error => {
|
|
241
|
-
console.error(`Failed to auto-renew certificate for ${domain}:`, error);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|