cipher-shield 1.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/LICENSE +21 -0
- package/README.md +666 -0
- package/index.js +75 -0
- package/package.json +57 -0
- package/src/core/aesEngine.js +369 -0
- package/src/core/blacklistMem.js +510 -0
- package/src/core/defconSystem.js +301 -0
- package/src/magicAuth.js +272 -0
- package/src/modules/aiScanner.js +482 -0
- package/src/modules/ghostHandler.js +279 -0
- package/src/modules/shadowHandler.js +266 -0
- package/src/shield.js +694 -0
- package/src/smartLogger.js +268 -0
- package/src/sslManager.js +345 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartLogger - Advanced Logging with Automatic Data Masking
|
|
3
|
+
*
|
|
4
|
+
* A secure logging utility that automatically masks sensitive data in logs.
|
|
5
|
+
* Recursively scans objects, arrays, and strings to protect sensitive information
|
|
6
|
+
* like passwords, tokens, and API keys.
|
|
7
|
+
*
|
|
8
|
+
* @module SmartLogger
|
|
9
|
+
* @version 1.0.0
|
|
10
|
+
* @author Omindu Dissanayaka
|
|
11
|
+
* @license MIT
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
class SmartLogger {
|
|
15
|
+
/**
|
|
16
|
+
* Creates a SmartLogger instance
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} [options] - Configuration options
|
|
19
|
+
* @param {string[]} [options.maskKeys] - Additional keys to mask (case-insensitive)
|
|
20
|
+
* @param {string} [options.maskValue='********'] - Value to replace sensitive data with
|
|
21
|
+
* @param {boolean} [options.enableTimestamp=true] - Include timestamp in logs
|
|
22
|
+
* @param {boolean} [options.enableColors=true] - Enable colored output
|
|
23
|
+
*/
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.options = {
|
|
26
|
+
maskKeys: [
|
|
27
|
+
'password', 'token', 'secret', 'apikey', 'api_key', 'apiKey',
|
|
28
|
+
'creditcard', 'credit_card', 'cvv', 'auth', 'authorization',
|
|
29
|
+
'bearer', 'session', 'cookie', 'private_key', 'privateKey'
|
|
30
|
+
].concat(options.maskKeys || []),
|
|
31
|
+
maskValue: options.maskValue || '********',
|
|
32
|
+
enableTimestamp: options.enableTimestamp !== false,
|
|
33
|
+
enableColors: options.enableColors !== false,
|
|
34
|
+
...options
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
this.colors = {
|
|
38
|
+
reset: '\x1b[0m',
|
|
39
|
+
bright: '\x1b[1m',
|
|
40
|
+
dim: '\x1b[2m',
|
|
41
|
+
red: '\x1b[31m',
|
|
42
|
+
green: '\x1b[32m',
|
|
43
|
+
yellow: '\x1b[33m',
|
|
44
|
+
blue: '\x1b[34m',
|
|
45
|
+
magenta: '\x1b[35m',
|
|
46
|
+
cyan: '\x1b[36m',
|
|
47
|
+
white: '\x1b[37m'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Add additional keys to mask in logs
|
|
53
|
+
*
|
|
54
|
+
* @param {string|string[]} keys - Key(s) to add to the masking list
|
|
55
|
+
*/
|
|
56
|
+
addMaskKey(keys) {
|
|
57
|
+
if (Array.isArray(keys)) {
|
|
58
|
+
this.options.maskKeys = this.options.maskKeys.concat(keys);
|
|
59
|
+
} else if (typeof keys === 'string') {
|
|
60
|
+
this.options.maskKeys.push(keys);
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error('Keys must be a string or array of strings');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Log information with data masking
|
|
68
|
+
*
|
|
69
|
+
* @param {*} message - Message or data to log
|
|
70
|
+
* @param {...*} args - Additional arguments
|
|
71
|
+
*/
|
|
72
|
+
info(message, ...args) {
|
|
73
|
+
this._log('INFO', message, args, this.colors.green);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Log warnings with data masking
|
|
78
|
+
*
|
|
79
|
+
* @param {*} message - Message or data to log
|
|
80
|
+
* @param {...*} args - Additional arguments
|
|
81
|
+
*/
|
|
82
|
+
warn(message, ...args) {
|
|
83
|
+
this._log('WARN', message, args, this.colors.yellow);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Log errors with data masking
|
|
88
|
+
*
|
|
89
|
+
* @param {*} message - Message or data to log
|
|
90
|
+
* @param {...*} args - Additional arguments
|
|
91
|
+
*/
|
|
92
|
+
error(message, ...args) {
|
|
93
|
+
this._log('ERROR', message, args, this.colors.red);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Log debug information with data masking
|
|
98
|
+
*
|
|
99
|
+
* @param {*} message - Message or data to log
|
|
100
|
+
* @param {...*} args - Additional arguments
|
|
101
|
+
*/
|
|
102
|
+
debug(message, ...args) {
|
|
103
|
+
this._log('DEBUG', message, args, this.colors.blue);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Log with custom level
|
|
108
|
+
*
|
|
109
|
+
* @param {string} level - Log level
|
|
110
|
+
* @param {*} message - Message or data to log
|
|
111
|
+
* @param {...*} args - Additional arguments
|
|
112
|
+
*/
|
|
113
|
+
log(level, message, ...args) {
|
|
114
|
+
this._log(level.toUpperCase(), message, args, this.colors.white);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Internal logging method
|
|
119
|
+
*
|
|
120
|
+
* @private
|
|
121
|
+
* @param {string} level - Log level
|
|
122
|
+
* @param {*} message - Message or data to log
|
|
123
|
+
* @param {Array} args - Additional arguments
|
|
124
|
+
* @param {string} color - Color code for output
|
|
125
|
+
*/
|
|
126
|
+
_log(level, message, args, color) {
|
|
127
|
+
const timestamp = this.options.enableTimestamp ?
|
|
128
|
+
`[${new Date().toISOString()}] ` : '';
|
|
129
|
+
|
|
130
|
+
const levelStr = this.options.enableColors ?
|
|
131
|
+
`${color}[${level}]${this.colors.reset}` : `[${level}]`;
|
|
132
|
+
|
|
133
|
+
const sanitizedMessage = this._sanitize(message);
|
|
134
|
+
const sanitizedArgs = args.map(arg => this._sanitize(arg));
|
|
135
|
+
|
|
136
|
+
let output = `${timestamp}${levelStr} ${sanitizedMessage}`;
|
|
137
|
+
|
|
138
|
+
if (sanitizedArgs.length > 0) {
|
|
139
|
+
output += ' ' + sanitizedArgs.map(arg =>
|
|
140
|
+
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
|
|
141
|
+
).join(' ');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(output);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Sanitize data by masking sensitive information
|
|
149
|
+
*
|
|
150
|
+
* @private
|
|
151
|
+
* @param {*} data - Data to sanitize
|
|
152
|
+
* @returns {*} Sanitized data
|
|
153
|
+
*/
|
|
154
|
+
_sanitize(data) {
|
|
155
|
+
if (data === null || data === undefined) {
|
|
156
|
+
return data;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof data === 'string') {
|
|
160
|
+
return this._sanitizeString(data);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (typeof data === 'object') {
|
|
164
|
+
if (Array.isArray(data)) {
|
|
165
|
+
return data.map(item => this._sanitize(item));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return this._sanitizeObject(data);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return data;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Sanitize string data
|
|
176
|
+
*
|
|
177
|
+
* @private
|
|
178
|
+
* @param {string} str - String to sanitize
|
|
179
|
+
* @returns {string} Sanitized string
|
|
180
|
+
*/
|
|
181
|
+
_sanitizeString(str) {
|
|
182
|
+
if (str.trim().startsWith('{') || str.trim().startsWith('[')) {
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(str);
|
|
185
|
+
const sanitized = this._sanitize(parsed);
|
|
186
|
+
return JSON.stringify(sanitized);
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const sensitivePatterns = [
|
|
192
|
+
/password[\s]*[:=][\s]*[^\s]+/gi,
|
|
193
|
+
/token[\s]*[:=][\s]*[^\s]+/gi,
|
|
194
|
+
/secret[\s]*[:=][\s]*[^\s]+/gi,
|
|
195
|
+
/apikey[\s]*[:=][\s]*[^\s]+/gi,
|
|
196
|
+
/api_key[\s]*[:=][\s]*[^\s]+/gi,
|
|
197
|
+
/bearer[\s]+[^\s]+/gi,
|
|
198
|
+
/authorization[\s]*[:=][\s]*[^\s]+/gi
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
let sanitized = str;
|
|
202
|
+
for (const pattern of sensitivePatterns) {
|
|
203
|
+
sanitized = sanitized.replace(pattern, (match) => {
|
|
204
|
+
const parts = match.split(/[:=\s]+/);
|
|
205
|
+
return parts[0] + ': ' + this.options.maskValue;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return sanitized;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Sanitize object data recursively
|
|
214
|
+
*
|
|
215
|
+
* @private
|
|
216
|
+
* @param {Object} obj - Object to sanitize
|
|
217
|
+
* @returns {Object} Sanitized object
|
|
218
|
+
*/
|
|
219
|
+
_sanitizeObject(obj) {
|
|
220
|
+
if (obj === null || typeof obj !== 'object') {
|
|
221
|
+
return obj;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const sanitized = Array.isArray(obj) ? [] : {};
|
|
225
|
+
|
|
226
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
227
|
+
const lowerKey = key.toLowerCase();
|
|
228
|
+
|
|
229
|
+
const shouldMask = this.options.maskKeys.some(maskKey =>
|
|
230
|
+
lowerKey.includes(maskKey.toLowerCase())
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (shouldMask) {
|
|
234
|
+
sanitized[key] = this.options.maskValue;
|
|
235
|
+
} else {
|
|
236
|
+
sanitized[key] = this._sanitize(value);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return sanitized;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Add custom keys to mask
|
|
245
|
+
*
|
|
246
|
+
* @param {string|string[]} keys - Keys to add to masking list
|
|
247
|
+
*/
|
|
248
|
+
addMaskKeys(keys) {
|
|
249
|
+
const newKeys = Array.isArray(keys) ? keys : [keys];
|
|
250
|
+
this.options.maskKeys.push(...newKeys);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Remove keys from masking list
|
|
255
|
+
*
|
|
256
|
+
* @param {string|string[]} keys - Keys to remove from masking list
|
|
257
|
+
*/
|
|
258
|
+
removeMaskKeys(keys) {
|
|
259
|
+
const keysToRemove = Array.isArray(keys) ? keys : [keys];
|
|
260
|
+
this.options.maskKeys = this.options.maskKeys.filter(maskKey =>
|
|
261
|
+
!keysToRemove.some(removeKey =>
|
|
262
|
+
maskKey.toLowerCase() === removeKey.toLowerCase()
|
|
263
|
+
)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = SmartLogger;
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSLManager - Automated SSL Certificate Management for Cipher Shield
|
|
3
|
+
*
|
|
4
|
+
* Handles automated SSL certificate generation and renewal using Let's Encrypt ACME protocol.
|
|
5
|
+
* Supports HTTP-01 challenge automation, secure certificate storage, and auto-renewal.
|
|
6
|
+
*
|
|
7
|
+
* @module SSLManager
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
* @author Omindu Dissanayaka
|
|
10
|
+
* @license MIT
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const acme = require('acme-client');
|
|
14
|
+
const forge = require('node-forge');
|
|
15
|
+
const fs = require('fs').promises;
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
|
|
19
|
+
class SSLManager {
|
|
20
|
+
/**
|
|
21
|
+
* Creates an SSL Manager instance
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} config - SSL configuration
|
|
24
|
+
* @param {string} config.email - Email for Let's Encrypt account registration
|
|
25
|
+
* @param {string[]} config.domains - Array of domains to obtain certificates for
|
|
26
|
+
* @param {boolean} [config.staging=true] - Use Let's Encrypt staging environment
|
|
27
|
+
* @param {string} [config.certDir='./ssl-certs'] - Directory to store certificates
|
|
28
|
+
* @param {Object} [config.expressApp] - Express app instance for HTTP-01 challenge
|
|
29
|
+
*/
|
|
30
|
+
constructor(config) {
|
|
31
|
+
this.config = {
|
|
32
|
+
email: config.email,
|
|
33
|
+
domains: Array.isArray(config.domains) ? config.domains : [config.domains],
|
|
34
|
+
staging: config.staging !== false,
|
|
35
|
+
certDir: config.certDir || path.join(process.cwd(), 'ssl-certs'),
|
|
36
|
+
expressApp: config.expressApp,
|
|
37
|
+
...config
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
this.client = null;
|
|
41
|
+
this.accountKey = null;
|
|
42
|
+
this.certificates = new Map();
|
|
43
|
+
this.renewalInterval = null;
|
|
44
|
+
|
|
45
|
+
this.directoryUrl = this.config.staging
|
|
46
|
+
? acme.directory.letsencrypt.staging
|
|
47
|
+
: acme.directory.letsencrypt.production;
|
|
48
|
+
|
|
49
|
+
this.logger = {
|
|
50
|
+
info: (msg) => console.log(`[SSLManager] ${msg}`),
|
|
51
|
+
error: (msg) => console.error(`[SSLManager] ${msg}`),
|
|
52
|
+
warn: (msg) => console.warn(`[SSLManager] ${msg}`)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize SSL Manager and start certificate management
|
|
58
|
+
*/
|
|
59
|
+
async initialize() {
|
|
60
|
+
try {
|
|
61
|
+
this.logger.info('Initializing SSL Manager...');
|
|
62
|
+
|
|
63
|
+
await this.ensureCertDirectory();
|
|
64
|
+
await this.initializeACMEClient();
|
|
65
|
+
await this.loadExistingCertificates();
|
|
66
|
+
this.startAutoRenewal();
|
|
67
|
+
await this.obtainCertificates();
|
|
68
|
+
|
|
69
|
+
this.logger.info('SSL Manager initialized successfully');
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.logger.error(`SSL Manager initialization failed: ${error.message}`);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Ensure certificate directory exists
|
|
78
|
+
*/
|
|
79
|
+
async ensureCertDirectory() {
|
|
80
|
+
try {
|
|
81
|
+
await fs.access(this.config.certDir);
|
|
82
|
+
} catch {
|
|
83
|
+
await fs.mkdir(this.config.certDir, { recursive: true });
|
|
84
|
+
this.logger.info(`Created certificate directory: ${this.config.certDir}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Initialize ACME client with account registration
|
|
90
|
+
*/
|
|
91
|
+
async initializeACMEClient() {
|
|
92
|
+
try {
|
|
93
|
+
this.accountKey = await this.getOrCreateAccountKey();
|
|
94
|
+
|
|
95
|
+
this.client = new acme.Client({
|
|
96
|
+
directoryUrl: this.directoryUrl,
|
|
97
|
+
accountKey: this.accountKey
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.logger.info('ACME client initialized');
|
|
101
|
+
} catch (error) {
|
|
102
|
+
this.logger.error(`ACME client initialization failed: ${error.message}`);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get or create account private key
|
|
109
|
+
*/
|
|
110
|
+
async getOrCreateAccountKey() {
|
|
111
|
+
const accountKeyPath = path.join(this.config.certDir, 'account-key.pem');
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const keyData = await fs.readFile(accountKeyPath, 'utf8');
|
|
115
|
+
this.logger.info('Loaded existing account key');
|
|
116
|
+
return keyData;
|
|
117
|
+
} catch {
|
|
118
|
+
this.logger.info('Generating new account key...');
|
|
119
|
+
const { privateKey } = crypto.generateKeyPairSync('ec', {
|
|
120
|
+
namedCurve: 'P-256',
|
|
121
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
122
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await fs.writeFile(accountKeyPath, privateKey, { mode: 0o600 });
|
|
126
|
+
this.logger.info('Account key saved');
|
|
127
|
+
return privateKey;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Load existing certificates from disk
|
|
133
|
+
*/
|
|
134
|
+
async loadExistingCertificates() {
|
|
135
|
+
for (const domain of this.config.domains) {
|
|
136
|
+
try {
|
|
137
|
+
const certData = await this.loadCertificate(domain);
|
|
138
|
+
if (certData) {
|
|
139
|
+
this.certificates.set(domain, certData);
|
|
140
|
+
this.logger.info(`Loaded existing certificate for ${domain}`);
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
this.logger.warn(`Could not load certificate for ${domain}: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Load certificate data for a domain
|
|
150
|
+
*/
|
|
151
|
+
async loadCertificate(domain) {
|
|
152
|
+
const certPath = path.join(this.config.certDir, `${domain}.pem`);
|
|
153
|
+
const keyPath = path.join(this.config.certDir, `${domain}-key.pem`);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const [cert, key] = await Promise.all([
|
|
157
|
+
fs.readFile(certPath, 'utf8'),
|
|
158
|
+
fs.readFile(keyPath, 'utf8')
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
return { cert, key, domain };
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Obtain certificates for all configured domains
|
|
169
|
+
*/
|
|
170
|
+
async obtainCertificates() {
|
|
171
|
+
for (const domain of this.config.domains) {
|
|
172
|
+
try {
|
|
173
|
+
const existingCert = this.certificates.get(domain);
|
|
174
|
+
if (existingCert && !this.isCertificateExpiringSoon(existingCert.cert)) {
|
|
175
|
+
this.logger.info(`Certificate for ${domain} is still valid`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.logger.info(`Obtaining certificate for ${domain}...`);
|
|
180
|
+
const certData = await this.obtainCertificate(domain);
|
|
181
|
+
this.certificates.set(domain, certData);
|
|
182
|
+
await this.saveCertificate(certData);
|
|
183
|
+
|
|
184
|
+
this.logger.info(`Certificate obtained and saved for ${domain}`);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
this.logger.error(`Failed to obtain certificate for ${domain}: ${error.message}`);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Obtain certificate for a specific domain
|
|
194
|
+
*/
|
|
195
|
+
async obtainCertificate(domain) {
|
|
196
|
+
try {
|
|
197
|
+
const domainKey = crypto.generateKeyPairSync('rsa', {
|
|
198
|
+
modulusLength: 2048,
|
|
199
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
200
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const [key, csr] = await acme.crypto.createCsr({
|
|
204
|
+
commonName: domain,
|
|
205
|
+
altNames: [domain]
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const challengeHandler = this.createChallengeHandler(domain);
|
|
209
|
+
|
|
210
|
+
const certificate = await this.client.auto({
|
|
211
|
+
csr,
|
|
212
|
+
email: this.config.email,
|
|
213
|
+
termsOfServiceAgreed: true,
|
|
214
|
+
challengeCreateFn: challengeHandler.create,
|
|
215
|
+
challengeRemoveFn: challengeHandler.remove
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
cert: certificate,
|
|
220
|
+
key: key,
|
|
221
|
+
domain
|
|
222
|
+
};
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this.logger.error(`Certificate obtain failed for ${domain}: ${error.message}`);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create HTTP-01 challenge handler
|
|
231
|
+
*/
|
|
232
|
+
createChallengeHandler(domain) {
|
|
233
|
+
const challenges = new Map();
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
create: async (authz, challenge, keyAuthorization) => {
|
|
237
|
+
if (challenge.type === 'http-01') {
|
|
238
|
+
const challengePath = `/.well-known/acme-challenge/${challenge.token}`;
|
|
239
|
+
|
|
240
|
+
challenges.set(challenge.token, keyAuthorization);
|
|
241
|
+
|
|
242
|
+
if (this.config.expressApp) {
|
|
243
|
+
this.config.expressApp.get(challengePath, (req, res) => {
|
|
244
|
+
const token = req.path.split('/').pop();
|
|
245
|
+
const auth = challenges.get(token);
|
|
246
|
+
if (auth) {
|
|
247
|
+
res.set('Content-Type', 'text/plain');
|
|
248
|
+
res.send(auth);
|
|
249
|
+
} else {
|
|
250
|
+
res.status(404).send('Challenge not found');
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
this.logger.info(`HTTP-01 challenge route setup for ${domain}: ${challengePath}`);
|
|
255
|
+
} else {
|
|
256
|
+
this.logger.warn('No Express app provided - HTTP-01 challenge will not be served automatically');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
remove: async (authz, challenge) => {
|
|
262
|
+
if (challenge.type === 'http-01') {
|
|
263
|
+
challenges.delete(challenge.token);
|
|
264
|
+
this.logger.info(`HTTP-01 challenge removed for ${domain}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
isCertificateExpiringSoon(certPem) {
|
|
271
|
+
try {
|
|
272
|
+
const cert = forge.pki.certificateFromPem(certPem);
|
|
273
|
+
const now = new Date();
|
|
274
|
+
const expiry = cert.validity.notAfter;
|
|
275
|
+
const daysUntilExpiry = (expiry - now) / (1000 * 60 * 60 * 24);
|
|
276
|
+
|
|
277
|
+
return daysUntilExpiry < 30;
|
|
278
|
+
} catch {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Save certificate to disk
|
|
285
|
+
*/
|
|
286
|
+
async saveCertificate(certData) {
|
|
287
|
+
const certPath = path.join(this.config.certDir, `${certData.domain}.pem`);
|
|
288
|
+
const keyPath = path.join(this.config.certDir, `${certData.domain}-key.pem`);
|
|
289
|
+
|
|
290
|
+
await Promise.all([
|
|
291
|
+
fs.writeFile(certPath, certData.cert, { mode: 0o644 }),
|
|
292
|
+
fs.writeFile(keyPath, certData.key, { mode: 0o600 })
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
this.logger.info(`Certificate saved for ${certData.domain}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Start auto-renewal process
|
|
300
|
+
*/
|
|
301
|
+
startAutoRenewal() {
|
|
302
|
+
this.renewalInterval = setInterval(async () => {
|
|
303
|
+
try {
|
|
304
|
+
this.logger.info('Checking certificates for renewal...');
|
|
305
|
+
await this.obtainCertificates();
|
|
306
|
+
} catch (error) {
|
|
307
|
+
this.logger.error(`Auto-renewal check failed: ${error.message}`);
|
|
308
|
+
}
|
|
309
|
+
}, 24 * 60 * 60 * 1000);
|
|
310
|
+
|
|
311
|
+
this.logger.info('Auto-renewal process started (checks every 24 hours)');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
stopAutoRenewal() {
|
|
315
|
+
if (this.renewalInterval) {
|
|
316
|
+
clearInterval(this.renewalInterval);
|
|
317
|
+
this.renewalInterval = null;
|
|
318
|
+
this.logger.info('Auto-renewal process stopped');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get certificate for domain
|
|
324
|
+
*/
|
|
325
|
+
getCertificate(domain) {
|
|
326
|
+
return this.certificates.get(domain) || null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get all certificates
|
|
331
|
+
*/
|
|
332
|
+
getAllCertificates() {
|
|
333
|
+
return Array.from(this.certificates.values());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Shutdown SSL Manager
|
|
338
|
+
*/
|
|
339
|
+
async shutdown() {
|
|
340
|
+
this.stopAutoRenewal();
|
|
341
|
+
this.logger.info('SSL Manager shutdown complete');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = SSLManager;
|