@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/acme",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "An intergration with ACME and CoCreateJS.",
5
5
  "keywords": [
6
6
  "acme",
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 email = 'mailto:ssl@cocreate.app';
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
- const accountKeyPath = keyPath + 'account.pem';
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
- let accountKey;
16
- let isNewAccount = false; // Flag to check if the account is new
32
+ const accountKeyPath = keyPath + 'account.pem';
17
33
 
18
- // Check if the account key exists and load it; otherwise, create a new one
19
- if (!fs.existsSync(accountKeyPath)) {
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
- isNewAccount = true; // New account, so will need to register it
25
- } else {
26
- // Load the existing account key
27
- accountKey = fs.readFileSync(accountKeyPath, 'utf8');
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
- // Initialize the ACME client with the account key
31
- client = new Client({
32
- directoryUrl: Client.directory.letsencrypt.staging,
33
- accountKey: accountKey
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 function requestCertificate(host, wildcard = false) {
46
- /* Place your domain(s) here */
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
- console.log(`Add this TXT record to your DNS:`);
91
- console.log(`Name: ${dnsRecordName}`);
92
- console.log(`Value: ${dnsRecordValue}`);
75
+ const self = this
93
76
 
94
- // Here, implement the logic to add the TXT record to your DNS
95
- // await updateDnsTxtRecord(dnsRecordName, dnsRecordValue); // Hypothetical function to update DNS
77
+ const hostKeyPath = keyPath + host + '/';
78
+ if (!fs.existsSync(hostKeyPath)) {
79
+ fs.mkdirSync(hostKeyPath, { recursive: true });
96
80
  }
97
81
 
98
- },
99
- challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
100
- /* Clean up challenge response here if necessary */
101
- console.log(`Challenge removed for token: ${challenge.token}`);
102
- if (challenge.type === 'http-01') {
103
- /* Clean up challenge response here if necessary */
104
- } else if (challenge.type === 'dns-01') {
105
- // await removeDnsTxtRecord(challenge); // A hypothetical function to clean up DNS
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
- /* Save the certificate and key */
112
- fs.writeFileSync(keyPath + 'certificate.pem', cert);
113
- fs.chmodSync(keyPath + 'certificate.pem', '444')
188
+ }
114
189
 
115
- fs.writeFileSync(keyPath + 'private-key.pem', key);
116
- fs.chmodSync(keyPath + 'certificate.pem', '400')
190
+ async getCertificate(host, organization_id) {
191
+ const hostKeyPath = keyPath + host + '/';
117
192
 
118
- console.log('Successfully created certificate!');
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
- init().catch(err => {
123
- console.error('Error initializing ACME client:', err);
124
- // TODO: Handle initialization error (possibly retry or exit)
125
- });
240
+ let expires = certificates.get(host)
241
+ if (expires && this.isValid(expires)) {
242
+ return true
243
+ }
126
244
 
127
- module.exports = { requestCertificate }
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
- // requestCertificate(host).catch(err => {
130
- // console.error('Error creating certificate:', err);
131
- // });
274
+ module.exports = CoCreateAcme;