@small-tech/auto-encrypt 4.1.1 → 4.1.3

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/README.md CHANGED
@@ -12,6 +12,8 @@ When not provisioning certificates, Auto Encrypt also forwards HTTP calls to HTT
12
12
 
13
13
  ## Compatibility
14
14
 
15
+ Node.js 18.20.0+.
16
+
15
17
  All tests pass on Node.js LTS (version 22).
16
18
 
17
19
  ## Installation
package/index.js CHANGED
@@ -14,6 +14,7 @@
14
14
  import os from 'os'
15
15
  import util from 'util'
16
16
  import https from 'https'
17
+ import ocsp from 'ocsp'
17
18
  import monkeyPatchTls from './lib/staging/monkeyPatchTls.js'
18
19
  import LetsEncryptServer from './lib/LetsEncryptServer.js'
19
20
  import Configuration from './lib/Configuration.js'
@@ -76,6 +77,9 @@ export default class AutoEncrypt {
76
77
  */
77
78
  static get https () { return AutoEncrypt }
78
79
 
80
+
81
+ static ocspCache = null
82
+
79
83
  /**
80
84
  * Automatically manages Let’s Encrypt certificate provisioning and renewal for Node.js
81
85
  * https servers using the HTTP-01 challenge on first hit of an HTTPS route via use of
@@ -170,7 +174,7 @@ export default class AutoEncrypt {
170
174
  }
171
175
  }
172
176
 
173
- const server = https.createServer(options, listener)
177
+ const server = this.addOcspStapling(https.createServer(options, listener))
174
178
 
175
179
  //
176
180
  // Monkey-patch the server.
@@ -208,11 +212,25 @@ export default class AutoEncrypt {
208
212
  }
209
213
 
210
214
 
215
+ /**
216
+ * The OCSP module does not have a means of clearing its cache check timers
217
+ * so we do it here. (Otherwise, the test suite would hang.)
218
+ */
219
+ static clearOcspCacheTimers () {
220
+ if (this.ocspCache !== null) {
221
+ const cacheIds = Object.keys(this.ocspCache.cache)
222
+ cacheIds.forEach(cacheId => {
223
+ clearInterval(this.ocspCache.cache[cacheId].timer)
224
+ })
225
+ }
226
+ }
227
+
211
228
  /**
212
229
  * Shut Auto Encrypt down. Do this before app exit. Performs necessary clean-up and removes
213
230
  * any references that might cause the app to not exit.
214
231
  */
215
232
  static shutdown () {
233
+ this.clearOcspCacheTimers()
216
234
  this.certificate.stopCheckingForRenewal()
217
235
  }
218
236
 
@@ -220,6 +238,64 @@ export default class AutoEncrypt {
220
238
  // Private.
221
239
  //
222
240
 
241
+ /**
242
+ * Adds Online Certificate Status Protocol (OCSP) stapling (also known as TLS Certificate Status Request extension)
243
+ * support to the passed server instance.
244
+ *
245
+ * @private
246
+ * @param {https.Server} server HTTPS server instance without OCSP Stapling support.
247
+ * @returns {https.Server} HTTPS server instance with OCSP Stapling support.
248
+ */
249
+ static addOcspStapling(server) {
250
+ // OCSP stapling
251
+ //
252
+ // Many browsers will fetch OCSP from Let’s Encrypt when they load your site. This is a performance and privacy
253
+ // problem. Ideally, connections to your site should not wait for a secondary connection to Let’s Encrypt. Also,
254
+ // OCSP requests tell Let’s Encrypt which sites people are visiting. We have a good privacy policy and do not record
255
+ // individually identifying details from OCSP requests, we’d rather not even receive the data in the first place.
256
+ // Additionally, we anticipate our bandwidth costs for serving OCSP every time a browser visits a Let’s Encrypt site
257
+ // for the first time will be a big part of our infrastructure expense.
258
+ //
259
+ // By turning on OCSP Stapling, you can improve the performance of your website, provide better privacy protections
260
+ // … and help Let’s Encrypt efficiently serve as many people as possible.
261
+ //
262
+ // (Source: https://letsencrypt.org/docs/integration-guide/implement-ocsp-stapling)
263
+
264
+ this.ocspCache = new ocsp.Cache()
265
+ const cache = this.ocspCache
266
+
267
+ server.on('OCSPRequest', (certificate, issuer, callback) => {
268
+
269
+ if (certificate == null) {
270
+ return callback(new Error('Cannot OCSP staple: certificate not yet provisioned.'))
271
+ }
272
+
273
+ ocsp.getOCSPURI(certificate, function(error, uri) {
274
+ if (error) return callback(error)
275
+ if (uri === null) return callback()
276
+
277
+ const request = ocsp.request.generate(certificate, issuer)
278
+
279
+ cache.probe(request.id, (error, cached) => {
280
+ if (error) return callback(error)
281
+
282
+ if (cached !== false) {
283
+ return callback(null, cached.response)
284
+ }
285
+
286
+ const options = {
287
+ url: uri,
288
+ ocsp: request.data
289
+ }
290
+
291
+ cache.request(request.id, options, callback);
292
+ })
293
+ })
294
+ })
295
+
296
+ return server
297
+ }
298
+
223
299
  // Custom object description for console output (for debugging).
224
300
  static [util.inspect.custom] () {
225
301
  return `
@@ -6,9 +6,7 @@
6
6
  * @license AGPLv3 or later.
7
7
  */
8
8
 
9
- import fs from 'fs'
10
- import path from 'path'
11
- import { fileURLToPath } from 'url'
9
+ import packageJson from '../package.json' with { type: 'json' }
12
10
  import jose from 'jose'
13
11
  import prepareRequest from 'bent'
14
12
  import types from '../typedefs/lib/AcmeRequest.js'
@@ -26,7 +24,6 @@ const throws = new Throws({
26
24
  [Symbol.for('AcmeRequest.requestError')]: error => `(${error.status} ${error.type} ${error.detail})`
27
25
  })
28
26
 
29
- const __dirname = fileURLToPath(new URL('.', import.meta.url))
30
27
  /**
31
28
  * Abstract base request class for carrying out signed ACME requests over HTTPS.
32
29
  *
@@ -39,7 +36,7 @@ export default class AcmeRequest {
39
36
  static nonce = null
40
37
  static __account = null
41
38
  /** @type {string} */
42
- static autoEncryptVersion = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'))).version
39
+ static autoEncryptVersion = packageJson.version
43
40
 
44
41
  static initialise (directory = throws.ifMissing(), accountIdentity = throws.ifMissing()) {
45
42
  this.directory = directory
@@ -172,9 +169,9 @@ export default class AcmeRequest {
172
169
  * Concatenates the output of a stream and returns a buffer. Taken from the bent module.
173
170
  *
174
171
  * @param {stream} stream A Node stream.
175
- * @returns {Buffer} The concatenated output of the Node stream.
172
+ * @returns {Promise<Buffer>} The concatenated output of the Node stream.
176
173
  */
177
- async getBuffer (stream) {
174
+ getBuffer (stream) {
178
175
  return new Promise((resolve, reject) => {
179
176
  const parts = []
180
177
  stream.on('error', reject)
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@small-tech/auto-encrypt",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "description": "Automatically provisions and renews Let’s Encrypt TLS certificates on Node.js https servers (including Kitten, Polka, Express.js, etc.)",
5
5
  "engines": {
6
- "node": ">=18.2.0"
6
+ "node": ">=18.20.0"
7
7
  },
8
8
  "keywords": [
9
9
  "let's encrypt",
@@ -68,7 +68,8 @@
68
68
  "encodeurl": "^1.0.2",
69
69
  "jose": "^1.28.2",
70
70
  "moment": "^2.29.4",
71
- "node-forge": "^1.3.1"
71
+ "node-forge": "^1.3.1",
72
+ "ocsp": "^1.2.0"
72
73
  },
73
74
  "devDependencies": {
74
75
  "@small-tech/esm-tape-runner": "^1.0.3",