@cocreate/acme 1.0.0 → 1.1.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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/index.js +247 -104
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.1.0](https://github.com/CoCreate-app/CoCreate-acme/compare/v1.0.0...v1.1.0) (2023-12-31)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* filenames ([23735c4](https://github.com/CoCreate-app/CoCreate-acme/commit/23735c44f30cdd848959a3046ac40665302f38b6))
|
|
7
|
+
* Wraped in class to use with modules ([4ff5bea](https://github.com/CoCreate-app/CoCreate-acme/commit/4ff5beabf9aee2b53d41839d3c6f276344726cb3))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* emit certificateCreated ([eb5cdc6](https://github.com/CoCreate-app/CoCreate-acme/commit/eb5cdc6142c7bfd15f548bbdaf908c33e3f6c19f))
|
|
13
|
+
* store certificates using crud so other nodes can reused ([aea5f15](https://github.com/CoCreate-app/CoCreate-acme/commit/aea5f1557f358934f6b08c9f94a734af80a56f6e))
|
|
14
|
+
|
|
1
15
|
# 1.0.0 (2023-12-30)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,131 +1,274 @@
|
|
|
1
|
-
const { Client } = require('acme-client');
|
|
1
|
+
const { Client, forge } = require('acme-client');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const certificates = new Map()
|
|
5
|
+
const email = 'ssl@cocreate.app';
|
|
5
6
|
const keyPath = 'certificates/';
|
|
6
7
|
let client
|
|
8
|
+
const hosts = {}
|
|
9
|
+
|
|
10
|
+
// Run this once per server to generate random constants
|
|
11
|
+
const DAYS = Math.floor(Math.random() * 7); // Random days between 0-6
|
|
12
|
+
const HOURS = Math.floor(Math.random() * 24); // Random hours between 0-23
|
|
13
|
+
const MINUTES = Math.floor(Math.random() * 60); // Random minutes between 0-59
|
|
14
|
+
|
|
15
|
+
// Store these constants in a configuration file, environment variable, or database
|
|
16
|
+
|
|
17
|
+
class CoCreateAcme {
|
|
18
|
+
constructor(crud) {
|
|
19
|
+
this.crud = crud
|
|
20
|
+
this.init().catch(err => {
|
|
21
|
+
console.error('Error initializing ACME client:', err);
|
|
22
|
+
// TODO: Handle initialization error (possibly retry or exit)
|
|
23
|
+
});
|
|
7
24
|
|
|
8
|
-
async function init() {
|
|
9
|
-
if (!fs.existsSync(keyPath)) {
|
|
10
|
-
fs.mkdirSync(keyPath, { recursive: true }); // Create the directory if it doesn't exist
|
|
11
25
|
}
|
|
12
26
|
|
|
13
|
-
|
|
27
|
+
async init() {
|
|
28
|
+
if (!fs.existsSync(keyPath)) {
|
|
29
|
+
fs.mkdirSync(keyPath, { recursive: true }); // Create the directory if it doesn't exist
|
|
30
|
+
}
|
|
14
31
|
|
|
15
|
-
|
|
16
|
-
let isNewAccount = false; // Flag to check if the account is new
|
|
32
|
+
const accountKeyPath = keyPath + 'account.pem';
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
accountKey = await Client.forge.createPrivateKey();
|
|
21
|
-
fs.writeFileSync(accountKeyPath, accountKey); // Store the account key
|
|
22
|
-
fs.chmodSync(accountKeyPath, '400')
|
|
34
|
+
let accountKey = '';
|
|
35
|
+
let isNewAccount = false; // Flag to check if the account is new
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
// Check if the account key exists and load it; otherwise, create a new one
|
|
38
|
+
if (!fs.existsSync(accountKeyPath)) {
|
|
39
|
+
fs.writeFileSync(accountKeyPath, accountKey); // Store the account key
|
|
40
|
+
accountKey = await forge.createPrivateKey();
|
|
41
|
+
isNewAccount = true; // New account, so will need to register it
|
|
42
|
+
fs.writeFileSync(accountKeyPath, accountKey); // Store the account key
|
|
43
|
+
// fs.chmodSync(accountKeyPath, '400')
|
|
44
|
+
} else {
|
|
45
|
+
accountKey = fs.readFileSync(accountKeyPath, 'utf8');
|
|
46
|
+
}
|
|
29
47
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Register the new account if it was just created
|
|
37
|
-
if (isNewAccount) {
|
|
38
|
-
await client.createAccount({
|
|
39
|
-
termsOfServiceAgreed: true,
|
|
40
|
-
contact: [email]
|
|
48
|
+
// Initialize the ACME client with the account key
|
|
49
|
+
client = new Client({
|
|
50
|
+
directoryUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
|
51
|
+
accountKey: accountKey
|
|
41
52
|
});
|
|
53
|
+
|
|
54
|
+
// Register the new account if it was just created
|
|
55
|
+
if (isNewAccount) {
|
|
56
|
+
try {
|
|
57
|
+
// Attempt to create an account
|
|
58
|
+
await client.createAccount({
|
|
59
|
+
termsOfServiceAgreed: true,
|
|
60
|
+
contact: ['mailto:' + email]
|
|
61
|
+
});
|
|
62
|
+
console.log("ACME account created successfully!");
|
|
63
|
+
} catch (error) {
|
|
64
|
+
fs.unlinkSync(accountKeyPath)
|
|
65
|
+
// Handle errors that occur during account creation
|
|
66
|
+
console.error("Error creating ACME account:", error.message);
|
|
67
|
+
// Depending on the type of error, you might want to retry, log the error, alert someone, etc.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
42
70
|
}
|
|
43
|
-
}
|
|
44
71
|
|
|
45
|
-
async
|
|
46
|
-
|
|
47
|
-
const domains = wildcard ? [host, `*.${host}`] : [host, `www.${host}`];
|
|
48
|
-
|
|
49
|
-
/* Create certificate request */
|
|
50
|
-
const [key, csr] = await Client.forge.createCsr({
|
|
51
|
-
commonName: domains[0],
|
|
52
|
-
altNames: domains
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
/* Request certificate */
|
|
56
|
-
const cert = await client.auto({
|
|
57
|
-
csr,
|
|
58
|
-
email, // Replace with your email
|
|
59
|
-
termsOfServiceAgreed: true,
|
|
60
|
-
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
|
|
61
|
-
/* Log the URL and content for the HTTP-01 challenge */
|
|
62
|
-
if (challenge.type === 'http-01') {
|
|
63
|
-
const challengeUrl = `http://${authz.identifier.value}/.well-known/acme-challenge/${challenge.token}`;
|
|
64
|
-
const keyAuth = keyAuthorization;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log('Please create a file accessible on:');
|
|
68
|
-
console.log(challengeUrl);
|
|
69
|
-
console.log('With the content:');
|
|
70
|
-
console.log(keyAuth);
|
|
71
|
-
|
|
72
|
-
let file = {
|
|
73
|
-
"content-type": "text/plain",
|
|
74
|
-
"directory": "acme-challenge",
|
|
75
|
-
"host": [
|
|
76
|
-
authz.identifier.value
|
|
77
|
-
],
|
|
78
|
-
"name": challenge.token,
|
|
79
|
-
"organization_id": "652c8d62679eca03e0b116a7",
|
|
80
|
-
"path": "/.well-known/acme-challenge/",
|
|
81
|
-
"pathname": `/.well-known/acme-challenge/${challenge.token}`,
|
|
82
|
-
"public": "true",
|
|
83
|
-
"src": keyAuth
|
|
84
|
-
}
|
|
85
|
-
} else if (challenge.type === 'dns-01') {
|
|
86
|
-
// Calculate the DNS TXT record value
|
|
87
|
-
const dnsRecordName = `_acme-challenge.${authz.identifier.value}`;
|
|
88
|
-
const dnsRecordValue = await client.getChallengeKeyAuthorization(challenge);
|
|
72
|
+
async requestCertificate(host, organization_id, wildcard = false) {
|
|
73
|
+
try {
|
|
89
74
|
|
|
90
|
-
|
|
91
|
-
console.log(`Name: ${dnsRecordName}`);
|
|
92
|
-
console.log(`Value: ${dnsRecordValue}`);
|
|
75
|
+
const self = this
|
|
93
76
|
|
|
94
|
-
|
|
95
|
-
|
|
77
|
+
const hostKeyPath = keyPath + host + '/';
|
|
78
|
+
if (!fs.existsSync(hostKeyPath)) {
|
|
79
|
+
fs.mkdirSync(hostKeyPath, { recursive: true });
|
|
96
80
|
}
|
|
97
81
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
82
|
+
const domains = wildcard ? [host, `*.${host}`] : [host];
|
|
83
|
+
|
|
84
|
+
/* Create certificate request */
|
|
85
|
+
const [key, csr] = await forge.createCsr({
|
|
86
|
+
commonName: domains[0],
|
|
87
|
+
altNames: domains
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
let challenge_id = ''
|
|
91
|
+
|
|
92
|
+
/* Request certificate */
|
|
93
|
+
const cert = await client.auto({
|
|
94
|
+
csr,
|
|
95
|
+
email: [email], // Replace with your email
|
|
96
|
+
termsOfServiceAgreed: true,
|
|
97
|
+
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
|
|
98
|
+
if (challenge.type === 'http-01') {
|
|
99
|
+
const httpChallenge = await self.crud.send({
|
|
100
|
+
method: 'object.create',
|
|
101
|
+
array: 'files',
|
|
102
|
+
object: {
|
|
103
|
+
"content-type": "text/plain",
|
|
104
|
+
"directory": "acme-challenge",
|
|
105
|
+
"host": [
|
|
106
|
+
authz.identifier.value
|
|
107
|
+
],
|
|
108
|
+
"name": challenge.token,
|
|
109
|
+
"organization_id": "652c8d62679eca03e0b116a7",
|
|
110
|
+
"path": "/.well-known/acme-challenge/",
|
|
111
|
+
"pathname": `/.well-known/acme-challenge/${challenge.token}`,
|
|
112
|
+
"public": "true",
|
|
113
|
+
"src": keyAuthorization
|
|
114
|
+
},
|
|
115
|
+
organization_id,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (httpChallenge && httpChallenge.object && httpChallenge.object[0])
|
|
119
|
+
challenge_id = httpChallenge.object[0]._id
|
|
120
|
+
else
|
|
121
|
+
console.error('error creating challenge url')
|
|
122
|
+
|
|
123
|
+
} else if (challenge.type === 'dns-01') {
|
|
124
|
+
// Calculate the DNS TXT record value
|
|
125
|
+
const dnsRecordName = `_acme-challenge.${authz.identifier.value}`;
|
|
126
|
+
const dnsRecordValue = await client.getChallengeKeyAuthorization(challenge);
|
|
127
|
+
|
|
128
|
+
console.log(`Add this TXT record to your DNS:`);
|
|
129
|
+
console.log(`Name: ${dnsRecordName}`);
|
|
130
|
+
console.log(`Value: ${dnsRecordValue}`);
|
|
131
|
+
|
|
132
|
+
// Here, implement the logic to add the TXT record to your DNS
|
|
133
|
+
// await updateDnsTxtRecord(dnsRecordName, dnsRecordValue); // Hypothetical function to update DNS
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
|
139
|
+
/* Clean up challenge response here if necessary */
|
|
140
|
+
console.log(`Challenge removed for token: ${challenge.token}`);
|
|
141
|
+
if (challenge.type === 'http-01') {
|
|
142
|
+
self.crud.send({
|
|
143
|
+
method: 'object.delete',
|
|
144
|
+
array: 'files',
|
|
145
|
+
object: {
|
|
146
|
+
_id: challenge_id
|
|
147
|
+
},
|
|
148
|
+
organization_id,
|
|
149
|
+
});
|
|
150
|
+
} else if (challenge.type === 'dns-01') {
|
|
151
|
+
// await removeDnsTxtRecord(challenge); // A hypothetical function to clean up DNS
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
let expires = await forge.readCertificateInfo(cert);
|
|
158
|
+
expires = expires.notAfter;
|
|
159
|
+
certificates.set(host, expires)
|
|
160
|
+
|
|
161
|
+
/* Save the certificate and key */
|
|
162
|
+
fs.writeFileSync(hostKeyPath + 'fullchain.pem', cert);
|
|
163
|
+
// fs.chmodSync(keyPath + 'fullchain.pem', '444')
|
|
164
|
+
|
|
165
|
+
fs.writeFileSync(hostKeyPath + 'private-key.pem', key);
|
|
166
|
+
// fs.chmodSync(keyPath + 'private-key.pem', '400')
|
|
107
167
|
|
|
168
|
+
process.emit('certificateCreated', host)
|
|
169
|
+
|
|
170
|
+
let safeKey = host.replace(/\./g, '_');
|
|
171
|
+
let organization = await this.crud.send({
|
|
172
|
+
method: 'object.update',
|
|
173
|
+
array: 'organizations',
|
|
174
|
+
object: {
|
|
175
|
+
_id: organization_id,
|
|
176
|
+
['ssl.' + safeKey]: { cert, key: key.toString('utf-8') }
|
|
177
|
+
},
|
|
178
|
+
organization_id,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
console.log('Successfully created certificate!');
|
|
182
|
+
return true
|
|
183
|
+
} catch (error) {
|
|
184
|
+
delete hosts[host]
|
|
185
|
+
return false
|
|
108
186
|
}
|
|
109
|
-
});
|
|
110
187
|
|
|
111
|
-
|
|
112
|
-
fs.writeFileSync(keyPath + 'certificate.pem', cert);
|
|
113
|
-
fs.chmodSync(keyPath + 'certificate.pem', '444')
|
|
188
|
+
}
|
|
114
189
|
|
|
115
|
-
|
|
116
|
-
|
|
190
|
+
async getCertificate(host, organization_id) {
|
|
191
|
+
const hostKeyPath = keyPath + host + '/';
|
|
117
192
|
|
|
118
|
-
|
|
119
|
-
|
|
193
|
+
let organization = await this.crud.send({
|
|
194
|
+
method: 'object.read',
|
|
195
|
+
array: 'organizations',
|
|
196
|
+
object: {
|
|
197
|
+
_id: organization_id
|
|
198
|
+
},
|
|
199
|
+
organization_id,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (organization && organization.object && organization.object[0]) {
|
|
203
|
+
|
|
204
|
+
if (!organization.object[0].host || !organization.object[0].host.includes(host))
|
|
205
|
+
return false
|
|
206
|
+
|
|
207
|
+
let safeKey = host.replace(/\./g, '_');
|
|
208
|
+
if (organization.object[0].ssl && organization.object[0].ssl[safeKey]) {
|
|
209
|
+
let cert = organization.object[0].ssl[safeKey].cert
|
|
210
|
+
let key = organization.object[0].ssl[safeKey].key
|
|
211
|
+
if (cert && key) {
|
|
212
|
+
let expires = await forge.readCertificateInfo(cert);
|
|
213
|
+
expires = expires.notAfter;
|
|
214
|
+
if (this.isValid(expires)) {
|
|
215
|
+
certificates.set(host, expires)
|
|
216
|
+
if (!fs.existsSync(hostKeyPath)) {
|
|
217
|
+
fs.mkdirSync(hostKeyPath, { recursive: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fs.writeFileSync(hostKeyPath + 'fullchain.pem', cert);
|
|
221
|
+
// fs.chmodSync(keyPath + 'fullchain.pem', '444')
|
|
222
|
+
fs.writeFileSync(hostKeyPath + 'private-key.pem', key);
|
|
223
|
+
// fs.chmodSync(keyPath + 'private-key.pem', '400')
|
|
224
|
+
|
|
225
|
+
// TODO: emit change so that nginx can reload
|
|
226
|
+
return true
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
120
231
|
|
|
232
|
+
return await this.requestCertificate(host, organization_id, false)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async checkCertificate(host, organization_id) {
|
|
236
|
+
let hostname = host.split(':')[0]
|
|
237
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1')
|
|
238
|
+
return true
|
|
121
239
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
240
|
+
let expires = certificates.get(host)
|
|
241
|
+
if (expires && this.isValid(expires)) {
|
|
242
|
+
return true
|
|
243
|
+
}
|
|
126
244
|
|
|
127
|
-
|
|
245
|
+
const hostKeyPath = keyPath + host + '/';
|
|
246
|
+
if (fs.existsSync(hostKeyPath + 'fullchain.pem')) {
|
|
247
|
+
expires = fs.readFileSync(hostKeyPath + 'fullchain.pem', 'utf8');
|
|
248
|
+
expires = await forge.readCertificateInfo(expires);
|
|
249
|
+
expires = expires.notAfter;
|
|
250
|
+
if (this.isValid(expires)) {
|
|
251
|
+
certificates.set(host, expires)
|
|
252
|
+
return true
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!hosts[host])
|
|
257
|
+
hosts[host] = this.getCertificate(host, organization_id)
|
|
258
|
+
return hosts[host]
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
isValid(expires) {
|
|
262
|
+
let currentDate = new Date();
|
|
263
|
+
currentDate.setDate(currentDate.getDate() + DAYS);
|
|
264
|
+
currentDate.setHours(currentDate.getHours() + HOURS);
|
|
265
|
+
currentDate.setMinutes(currentDate.getMinutes() + MINUTES);
|
|
266
|
+
|
|
267
|
+
if (expires && currentDate < expires) {
|
|
268
|
+
return true; // SSL is still valid, no need to renew
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
}
|
|
128
273
|
|
|
129
|
-
|
|
130
|
-
// console.error('Error creating certificate:', err);
|
|
131
|
-
// });
|
|
274
|
+
module.exports = CoCreateAcme;
|