@small-tech/auto-encrypt 4.3.0 → 5.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/README.md +64 -64
- package/index.d.ts +152 -0
- package/index.js +220 -205
- package/lib/Account.js +28 -20
- package/lib/AcmeRequest.js +187 -117
- package/lib/Authorisation.js +80 -36
- package/lib/Certificate.js +292 -149
- package/lib/Configuration.js +93 -77
- package/lib/Directory.js +28 -17
- package/lib/HttpServer.js +55 -28
- package/lib/IPAddresses.js +114 -0
- package/lib/Identity.js +65 -37
- package/lib/LetsEncryptServer.js +15 -15
- package/lib/Nonce.js +36 -27
- package/lib/Order.js +65 -39
- package/lib/acme-requests/AuthorisationRequest.js +15 -14
- package/lib/acme-requests/CertificateRequest.js +16 -15
- package/lib/acme-requests/CheckOrderStatusRequest.js +19 -17
- package/lib/acme-requests/FinaliseOrderRequest.js +21 -19
- package/lib/acme-requests/NewAccountRequest.js +13 -14
- package/lib/acme-requests/NewOrderRequest.js +30 -17
- package/lib/acme-requests/ReadyForChallengeValidationRequest.js +17 -16
- package/lib/acmeCsr.js +116 -59
- package/lib/identities/AccountIdentity.js +15 -14
- package/lib/identities/CertificateIdentity.js +16 -16
- package/lib/staging/monkeyPatchTls.js +12 -12
- package/lib/test-helpers/index.js +8 -23
- package/lib/util/Pluralise.js +2 -2
- package/lib/util/Throws.js +61 -21
- package/lib/util/async-foreach.js +21 -21
- package/lib/util/fromNow.js +27 -0
- package/lib/util/log.js +5 -1
- package/lib/util/waitFor.js +5 -0
- package/lib/x.509/rfc5280.js +47 -12
- package/package.json +12 -11
- package/typedefs/lib/AcmeRequest.js +36 -25
package/index.js
CHANGED
|
@@ -1,139 +1,203 @@
|
|
|
1
1
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
Automatically provisions and renews Let’s Encrypt™ TLS certificates for Node.js® https servers (as used in @small-tech/https and Kitten.)
|
|
3
|
+
|
|
4
|
+
Implements the subset of RFC 8555 – Automatic Certificate Management Environment (ACME) – necessary for a Node.js https server to provision TLS certificates from Let’s Encrypt using the HTTP-01 challenge.
|
|
5
|
+
|
|
6
|
+
The certificates are provisioned, if necessary, at time of server creation, prior to server start with hourly renewal checks and automatic renewals.
|
|
7
|
+
|
|
8
|
+
Auto Encrypt uses the new Let’s Encrypt `shortlived` profile *exclusively*, which means certificates are valid for (and renewed before) 160 hours. This should occur at around the 80 hour mark of the certificate’s lifespan, in accordance with data returned by ACME Renewal Information (ARI).
|
|
9
|
+
|
|
10
|
+
Auto Encrypt also implements automatic support for obtaining certificates on IPv4 and IPv6 addresses to enable the use of Web Numbers on the Small Web (see https://ar.al/2025/06/25/web-numbers/).
|
|
11
|
+
|
|
12
|
+
Remember that Auto Encrypt is for the Small Web (peer-to-peer Web). You might find it useful for everyday web purposes also but it is specifically focused for Small Web use cases and explicitly does not aim to be a generic or exhaustive ACME client.
|
|
13
|
+
|
|
14
|
+
@module @small-tech/auto-encrypt
|
|
15
|
+
@copyright © 2020-present Aral Balkan, Small Technology Foundation.
|
|
16
|
+
@license AGPLv3 or later.
|
|
13
17
|
*/
|
|
18
|
+
|
|
14
19
|
import os from 'os'
|
|
15
20
|
import util from 'util'
|
|
16
21
|
import https from 'https'
|
|
17
|
-
import ocsp from 'ocsp'
|
|
18
22
|
import monkeyPatchTls from './lib/staging/monkeyPatchTls.js'
|
|
19
23
|
import LetsEncryptServer from './lib/LetsEncryptServer.js'
|
|
20
24
|
import Configuration from './lib/Configuration.js'
|
|
21
25
|
import Certificate from './lib/Certificate.js'
|
|
26
|
+
import AcmeRequest from './lib/AcmeRequest.js'
|
|
22
27
|
import Pluralise from './lib/util/Pluralise.js'
|
|
23
28
|
import Throws from './lib/util/Throws.js'
|
|
24
29
|
import HttpServer from './lib/HttpServer.js'
|
|
30
|
+
import IPAddresses from './lib/IPAddresses.js'
|
|
25
31
|
import log from './lib/util/log.js'
|
|
26
32
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
import dns from 'node:dns'
|
|
30
|
-
dns.setDefaultResultOrder('ipv4first')
|
|
33
|
+
// Use module-level await to ensure we have the IP address information we need.
|
|
34
|
+
const ipAddresses = await IPAddresses.getInstanceAsync()
|
|
31
35
|
|
|
32
36
|
// Custom errors thrown by the autoEncrypt function.
|
|
33
37
|
const throws = new Throws({
|
|
34
38
|
[Symbol.for('BusyProvisioningCertificateError')]:
|
|
35
39
|
() => 'We’re busy provisioning TLS certificates and rejecting all other calls at the moment.',
|
|
36
40
|
|
|
41
|
+
[Symbol.for('IPv4AddressNotFound')]:
|
|
42
|
+
() => 'IPv4 certificate requested but could not automatically find machine’s IPv4 address',
|
|
43
|
+
|
|
44
|
+
[Symbol.for('IPv6AddressNotFound')]:
|
|
45
|
+
() => 'IPv6 certificates requested but could not automatically find any stable IPv6 addresses',
|
|
46
|
+
|
|
37
47
|
[Symbol.for('SNIIgnoreUnsupportedDomainError')]:
|
|
38
|
-
(serverName, domains) => {
|
|
48
|
+
(/** @type {string} */ serverName, /** @type {Array<string>} */ domains) => {
|
|
39
49
|
return `SNI: Not responding to request for unsupported domain ${serverName} (valid ${Pluralise.word('domain', domains)} ${Pluralise.isAre(domains)} ${domains}).`
|
|
40
50
|
}
|
|
41
51
|
})
|
|
42
52
|
|
|
43
53
|
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
@typedef {import('./index.d.ts').AutoEncryptedServer} AutoEncryptedServer
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
Auto Encrypt is a static class. Please do not instantiate.
|
|
59
|
+
|
|
60
|
+
Use: AutoEncrypt.https.createServer(…)
|
|
61
|
+
|
|
62
|
+
@alias module:@small-tech/auto-encrypt
|
|
63
|
+
@hideconstructor
|
|
64
|
+
*/
|
|
51
65
|
export default class AutoEncrypt {
|
|
66
|
+
/** @type { AutoEncryptedServer } */
|
|
67
|
+
static server = null
|
|
68
|
+
|
|
69
|
+
/** @type { import('./lib/LetsEncryptServer.js').default | null } */
|
|
52
70
|
static letsEncryptServer = null
|
|
71
|
+
|
|
72
|
+
/** @type { string[] | null } */
|
|
53
73
|
static defaultDomains = null
|
|
74
|
+
|
|
75
|
+
/** @type { string[] | null } */
|
|
54
76
|
static domains = null
|
|
77
|
+
|
|
78
|
+
/** @type { string | null } */
|
|
55
79
|
static settingsPath = null
|
|
80
|
+
|
|
81
|
+
/** @type { ((req: import('http').IncomingMessage, res: import('http').ServerResponse) => void) | null } */
|
|
56
82
|
static listener = null
|
|
83
|
+
|
|
84
|
+
/** @type { Certificate | null } */
|
|
57
85
|
static certificate = null
|
|
58
86
|
|
|
59
87
|
/**
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
Enumeration.
|
|
89
|
+
|
|
90
|
+
@type {LetsEncryptServer.type}
|
|
91
|
+
@readonly
|
|
92
|
+
@static
|
|
93
|
+
*/
|
|
66
94
|
static serverType = LetsEncryptServer.type
|
|
67
95
|
|
|
68
96
|
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
*
|
|
73
|
-
* @example import AutoEncrypt from '@small-tech/auto-encrypt'
|
|
74
|
-
* const server = AutoEncrypt.https.createServer()
|
|
75
|
-
*
|
|
76
|
-
* @static
|
|
77
|
-
*/
|
|
78
|
-
static get https () { return AutoEncrypt }
|
|
97
|
+
By aliasing the https property to the AutoEncrypt static class itself, we enable
|
|
98
|
+
people to add AutoEncrypt to their existing apps by requiring the module
|
|
99
|
+
and prefixing their https.createServer(…) line with await AutoEncrypt:
|
|
79
100
|
|
|
101
|
+
@example import AutoEncrypt from '@small-tech/auto-encrypt'
|
|
102
|
+
const server = await AutoEncrypt.https.createServer()
|
|
80
103
|
|
|
81
|
-
|
|
104
|
+
@static
|
|
105
|
+
*/
|
|
106
|
+
static get https () { return AutoEncrypt }
|
|
82
107
|
|
|
83
108
|
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
Automatically manages Let’s Encrypt certificate provisioning and renewal for Node.js
|
|
110
|
+
https servers using the HTTP-01 challenge on first hit of an HTTPS route via use of
|
|
111
|
+
the Server Name Indication (SNI) callback.
|
|
112
|
+
|
|
113
|
+
@static
|
|
114
|
+
|
|
115
|
+
@param {import('tls').SecureContextOptions & {
|
|
116
|
+
domains?: Array<string>,
|
|
117
|
+
serverType?: number,
|
|
118
|
+
settingsPath?: string,
|
|
119
|
+
ipv4?: boolean,
|
|
120
|
+
ipv6?: boolean,
|
|
121
|
+
ipOnly?: boolean,
|
|
122
|
+
SNICallback?: (serverName: string, callback: (...args: any[]) => void) => Promise<void> | void
|
|
123
|
+
} | ((...any: any[]) => void)} _options
|
|
124
|
+
|
|
125
|
+
@param {(...any: any[]) => void} [_listener]
|
|
126
|
+
|
|
127
|
+
@returns {Promise<import('./index.d.ts').AutoEncryptedServer>} A promise for the server instance (a monkey-patched https.Server similar to the one returned by Node’s https.createServer() method).
|
|
128
|
+
*/
|
|
129
|
+
static async createServer(_options, _listener) {
|
|
130
|
+
// Auto Encrypt only supports one server per Node process. As such, if the server already exists, return the existing instance.
|
|
131
|
+
if (this.server !== null) {
|
|
132
|
+
log(' ☝️ ❨auto-encrypt❩ Returning reference to existing server (there can be only one… per process).')
|
|
133
|
+
return this.server
|
|
134
|
+
}
|
|
135
|
+
|
|
101
136
|
// The first parameter is optional. If omitted, the first argument, if any, is treated as the request listener.
|
|
102
137
|
if (typeof _options === 'function') {
|
|
103
138
|
_listener = _options
|
|
104
139
|
_options = {}
|
|
105
140
|
}
|
|
106
141
|
|
|
107
|
-
const
|
|
108
|
-
const defaultPebbleDomains = ['localhost', 'pebble']
|
|
142
|
+
const defaultPebbleDomains = []
|
|
109
143
|
const options = _options || {}
|
|
110
144
|
const letsEncryptServer = new LetsEncryptServer(options.serverType || LetsEncryptServer.type.PRODUCTION)
|
|
111
145
|
const listener = _listener || null
|
|
112
146
|
const settingsPath = options.settingsPath || null
|
|
113
147
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
148
|
+
let defaultStagingAndProductionDomains = []
|
|
149
|
+
if (!options.ipOnly) {
|
|
150
|
+
defaultStagingAndProductionDomains.push(os.hostname())
|
|
151
|
+
defaultPebbleDomains.push('localhost')
|
|
152
|
+
defaultPebbleDomains.push('pebble')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (options.ipv4 === true) {
|
|
156
|
+
if (letsEncryptServer.type === LetsEncryptServer.type.PEBBLE) {
|
|
157
|
+
const localIPv4Address = '127.0.0.1'
|
|
158
|
+
defaultPebbleDomains.push(localIPv4Address)
|
|
159
|
+
log(` 📍 ❨auto-encrypt❩ Will provision TLS certificate from Pebble server for local IPv4 address (${localIPv4Address})`)
|
|
160
|
+
} else {
|
|
161
|
+
if (ipAddresses.hasIPv4Address) {
|
|
162
|
+
const ipv4Address = ipAddresses.ipv4Address
|
|
163
|
+
defaultStagingAndProductionDomains.push(ipv4Address)
|
|
164
|
+
log(` 📍 ❨auto-encrypt❩ Will provision TLS certificate for detected IPv4 address: ${ipv4Address}`)
|
|
165
|
+
} else {
|
|
166
|
+
throws.error(Symbol.for('IPv4AddressNotFound'))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (options.ipv6 === true) {
|
|
172
|
+
if (letsEncryptServer.type === LetsEncryptServer.type.PEBBLE) {
|
|
173
|
+
// IPv6: Pebble.
|
|
174
|
+
const localIPv6Address = '::1'
|
|
175
|
+
defaultPebbleDomains.push(localIPv6Address)
|
|
176
|
+
log(` 📍 ❨auto-encrypt❩ Will provision TLS certificate from Pebble server for local IPv6 address (${localIPv6Address})`)
|
|
177
|
+
} else {
|
|
178
|
+
// IPv6: Staging and production.
|
|
179
|
+
const ipv6Addresses = ipAddresses.ipv6Addresses
|
|
180
|
+
if (ipv6Addresses.length > 0) {
|
|
181
|
+
defaultStagingAndProductionDomains = defaultStagingAndProductionDomains.concat(ipv6Addresses)
|
|
182
|
+
ipv6Addresses.forEach(ipv6Address => defaultPebbleDomains.push(ipv6Address))
|
|
183
|
+
log(` 📍 ❨auto-encrypt❩ Will provision TLS certificate for detected stable IPv6 address${ipv6Addresses.length > 1 ? 'es' : ''}: ${ipv6Addresses}`)
|
|
184
|
+
} else {
|
|
185
|
+
throws.error(Symbol.for('IPv6AddressNotFound'))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
117
190
|
let defaultDomains = defaultStagingAndProductionDomains
|
|
118
191
|
|
|
192
|
+
// Behaviour specific to Let’s Encrypt server type (Pebble/staging).
|
|
119
193
|
switch (letsEncryptServer.type) {
|
|
120
194
|
case LetsEncryptServer.type.PEBBLE:
|
|
121
195
|
options.domains = null
|
|
122
196
|
defaultDomains = defaultPebbleDomains
|
|
123
197
|
break
|
|
124
198
|
|
|
125
|
-
// If this is a staging server, we add the intermediary certificate to Node.js’s trust store (only valid during
|
|
126
|
-
// the current Node.js process) so that Node will accept the certificate. Useful when running tests against the
|
|
127
|
-
// staging server.
|
|
128
|
-
//
|
|
129
|
-
// If you’re using Pebble for your tests, please install and use node-pebble manually in your tests.
|
|
130
|
-
// (We cannot automatically provide support for Pebble as it dynamically generates its root and
|
|
131
|
-
// intermediary CA certificates, which is an asynchronous process whereas the createServer method is
|
|
132
|
-
// synchronous.)*
|
|
133
|
-
//
|
|
134
|
-
// * Yes, we could check for and start the Pebble server in the asynchronous SNICallback, below, but given how
|
|
135
|
-
// often that function is called, I will not add anything to it beyond the essentials for performance reasons.
|
|
136
199
|
case LetsEncryptServer.type.STAGING:
|
|
200
|
+
// Add the intermediary certificate to Node.js’s trust store (only valid during the current Node.js process) so that Node will accept the certificate. Useful when running tests against the staging server.
|
|
137
201
|
monkeyPatchTls()
|
|
138
202
|
break
|
|
139
203
|
}
|
|
@@ -144,6 +208,9 @@ export default class AutoEncrypt {
|
|
|
144
208
|
delete options.domains
|
|
145
209
|
delete options.serverType
|
|
146
210
|
delete options.settingsPath
|
|
211
|
+
delete options.ipv4
|
|
212
|
+
delete options.ipv6
|
|
213
|
+
delete options.ipOnly
|
|
147
214
|
|
|
148
215
|
const configuration = new Configuration({ settingsPath, domains, server: letsEncryptServer})
|
|
149
216
|
const certificate = new Certificate(configuration)
|
|
@@ -155,159 +222,107 @@ export default class AutoEncrypt {
|
|
|
155
222
|
this.listener = listener
|
|
156
223
|
this.certificate = certificate
|
|
157
224
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
callback(throws.createError(error, ...args))
|
|
162
|
-
}
|
|
225
|
+
// Start HTTP server.
|
|
226
|
+
log(' 🌍 ❨auto-encrypt❩ Starting HTTP challenge responder server…')
|
|
227
|
+
await HttpServer.getSharedInstance()
|
|
163
228
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
sniError('BusyProvisioningCertificateError', callback, '⏳')
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
callback(null, secureContext)
|
|
172
|
-
} else {
|
|
173
|
-
sniError('SNIIgnoreUnsupportedDomainError', callback, '🤨', serverName, domains)
|
|
174
|
-
}
|
|
175
|
-
}
|
|
229
|
+
// Provision certificate (if required).
|
|
230
|
+
log(` 📃 ❨auto-encrypt❩ Provisioning certificate (if required) for domains: ${domains.join(', ')}…`)
|
|
231
|
+
await certificate.getSecureContext()
|
|
232
|
+
log(' ✅ ❨auto-encrypt❩ Certificate provisioned (or already exists).')
|
|
176
233
|
|
|
177
|
-
|
|
234
|
+
// Pass the certificate and key to the https.createServer options as well.
|
|
235
|
+
// This is required for IP hits (which don't use SNI) and also acts as the
|
|
236
|
+
// default certificate.
|
|
237
|
+
options.key = certificate.key
|
|
238
|
+
options.cert = certificate.pem
|
|
178
239
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// over, we create the server accordingly based on whether the certificate
|
|
183
|
-
// has OCSP stapling or not so it works for both cases.
|
|
184
|
-
// TODO: OCSP support can be fully removed in August 2025. All existing
|
|
185
|
-
// servers with valid certificates will be non-OCSP at that point.
|
|
186
|
-
const serverWithoutOcspMustStaple = https.createServer(options, listener)
|
|
187
|
-
const server = shouldAddOcspMustStaple
|
|
188
|
-
? this.addOcspStapling(serverWithoutOcspMustStaple)
|
|
189
|
-
: serverWithoutOcspMustStaple
|
|
240
|
+
// Create node:https server.
|
|
241
|
+
const server = /** @type {import('./index.d.ts').AutoEncryptedServer} */ (/** @type {unknown} */ (https.createServer(options, listener)))
|
|
242
|
+
this.server = server
|
|
190
243
|
|
|
191
244
|
//
|
|
192
|
-
// Monkey-patch
|
|
245
|
+
// Monkey-patch server.
|
|
193
246
|
//
|
|
194
247
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
248
|
+
/**
|
|
249
|
+
Auto Encrypt’s async version of Node’s built-in `https.listen()` method.
|
|
250
|
+
|
|
251
|
+
You can use this method with exactly the same signature as Node’s and
|
|
252
|
+
with a callback but, ideally, you should be awaiting it and not using the callback.
|
|
253
|
+
|
|
254
|
+
@param {...*} args - Arguments to pass to https.Server.listen()
|
|
255
|
+
@returns {Promise<AutoEncryptedServer>}
|
|
256
|
+
*/
|
|
257
|
+
const listenAsync = async function (...args) {
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
const server = this['__original_listen__'](...args)
|
|
260
|
+
.once('listening', () => resolve(server))
|
|
261
|
+
.once('error', reject)
|
|
205
262
|
})
|
|
206
263
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
264
|
+
server['__original_listen__'] = server.listen.bind(server)
|
|
265
|
+
server.listen = listenAsync.bind(server)
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
Auto Encrypt Localhost’s async version of Node’s built-in `https.close()` method.
|
|
269
|
+
|
|
270
|
+
Unlike Node’s `close()` method, this version also calls `closeAllConnections()` so it
|
|
271
|
+
will sever any existing idle and active connections. When `await`ed, this call will
|
|
272
|
+
only return once `node:https`’s close callback is called. This is also where your
|
|
273
|
+
callback will fire, if you are using Node’s original method signature will callbacks.
|
|
274
|
+
|
|
275
|
+
You can still use this method with exactly the same signature as Node’s and
|
|
276
|
+
with a callback but, ideally, you should be awaiting it and not using the callback.
|
|
277
|
+
|
|
278
|
+
@returns {Promise<void>}
|
|
279
|
+
*/
|
|
280
|
+
const closeAsync = async function () {
|
|
213
281
|
// Clean-up our own house.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
282
|
+
await AutoEncrypt.shutdown()
|
|
283
|
+
|
|
284
|
+
// Start shutting down the HTTPS server.
|
|
285
|
+
const closePromise = new Promise((resolve, reject) => {
|
|
286
|
+
this['__original_close__']((/** @type { Error } */ error) => {
|
|
287
|
+
if (error) {
|
|
288
|
+
reject(error)
|
|
289
|
+
} else {
|
|
290
|
+
resolve()
|
|
291
|
+
}
|
|
292
|
+
})
|
|
220
293
|
})
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return server
|
|
224
|
-
}
|
|
225
294
|
|
|
295
|
+
// Sever all idle and active connections.
|
|
296
|
+
this.closeAllConnections()
|
|
226
297
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
*/
|
|
231
|
-
static clearOcspCacheTimers () {
|
|
232
|
-
if (this.ocspCache !== null) {
|
|
233
|
-
const cacheIds = Object.keys(this.ocspCache.cache)
|
|
234
|
-
cacheIds.forEach(cacheId => {
|
|
235
|
-
clearInterval(this.ocspCache.cache[cacheId].timer)
|
|
236
|
-
})
|
|
298
|
+
// Only now await the promise.
|
|
299
|
+
// (See note at: https://nodejs.org/docs/latest-v24.x/api/http.html#servercloseallconnections)
|
|
300
|
+
await closePromise
|
|
237
301
|
}
|
|
238
|
-
|
|
302
|
+
server['__original_close__'] = server.close.bind(server)
|
|
303
|
+
server.close = closeAsync.bind(server)
|
|
239
304
|
|
|
305
|
+
// Return the server singleton.
|
|
306
|
+
return this.server
|
|
307
|
+
}
|
|
308
|
+
|
|
240
309
|
/**
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
static shutdown () {
|
|
245
|
-
this.
|
|
246
|
-
|
|
310
|
+
Shut Auto Encrypt down. Do this before app exit. Performs necessary clean-up and removes
|
|
311
|
+
any references that might cause the app to not exit.
|
|
312
|
+
*/
|
|
313
|
+
static async shutdown () {
|
|
314
|
+
if (this.certificate) {
|
|
315
|
+
this.certificate.stopCheckingForRenewal()
|
|
316
|
+
}
|
|
317
|
+
await HttpServer.destroySharedInstance()
|
|
318
|
+
AcmeRequest.uninitialise()
|
|
319
|
+
this.server = null
|
|
247
320
|
}
|
|
248
321
|
|
|
249
322
|
//
|
|
250
323
|
// Private.
|
|
251
324
|
//
|
|
252
325
|
|
|
253
|
-
/**
|
|
254
|
-
* Adds Online Certificate Status Protocol (OCSP) stapling (also known as TLS Certificate Status Request extension)
|
|
255
|
-
* support to the passed server instance.
|
|
256
|
-
*
|
|
257
|
-
* @private
|
|
258
|
-
* @param {https.Server} server HTTPS server instance without OCSP Stapling support.
|
|
259
|
-
* @returns {https.Server} HTTPS server instance with OCSP Stapling support.
|
|
260
|
-
*/
|
|
261
|
-
static addOcspStapling(server) {
|
|
262
|
-
// OCSP stapling
|
|
263
|
-
//
|
|
264
|
-
// Many browsers will fetch OCSP from Let’s Encrypt when they load your site. This is a performance and privacy
|
|
265
|
-
// problem. Ideally, connections to your site should not wait for a secondary connection to Let’s Encrypt. Also,
|
|
266
|
-
// OCSP requests tell Let’s Encrypt which sites people are visiting. We have a good privacy policy and do not record
|
|
267
|
-
// individually identifying details from OCSP requests, we’d rather not even receive the data in the first place.
|
|
268
|
-
// Additionally, we anticipate our bandwidth costs for serving OCSP every time a browser visits a Let’s Encrypt site
|
|
269
|
-
// for the first time will be a big part of our infrastructure expense.
|
|
270
|
-
//
|
|
271
|
-
// By turning on OCSP Stapling, you can improve the performance of your website, provide better privacy protections
|
|
272
|
-
// … and help Let’s Encrypt efficiently serve as many people as possible.
|
|
273
|
-
//
|
|
274
|
-
// (Source: https://letsencrypt.org/docs/integration-guide/implement-ocsp-stapling)
|
|
275
|
-
|
|
276
|
-
this.ocspCache = new ocsp.Cache()
|
|
277
|
-
const cache = this.ocspCache
|
|
278
|
-
|
|
279
|
-
server.on('OCSPRequest', (certificate, issuer, callback) => {
|
|
280
|
-
|
|
281
|
-
if (certificate == null) {
|
|
282
|
-
return callback(new Error('Cannot OCSP staple: certificate not yet provisioned.'))
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
ocsp.getOCSPURI(certificate, function(error, uri) {
|
|
286
|
-
if (error) return callback(error)
|
|
287
|
-
if (uri === null) return callback()
|
|
288
|
-
|
|
289
|
-
const request = ocsp.request.generate(certificate, issuer)
|
|
290
|
-
|
|
291
|
-
cache.probe(request.id, (error, cached) => {
|
|
292
|
-
if (error) return callback(error)
|
|
293
|
-
|
|
294
|
-
if (cached !== false) {
|
|
295
|
-
return callback(null, cached.response)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const options = {
|
|
299
|
-
url: uri,
|
|
300
|
-
ocsp: request.data
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
cache.request(request.id, options, callback);
|
|
304
|
-
})
|
|
305
|
-
})
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
return server
|
|
309
|
-
}
|
|
310
|
-
|
|
311
326
|
// Custom object description for console output (for debugging).
|
|
312
327
|
static [util.inspect.custom] () {
|
|
313
328
|
return `
|
package/lib/Account.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// URL used as the "kid" value in the JWS authenticating subsequent requests
|
|
9
|
-
// by this account after it is created using the JWT public key.
|
|
10
|
-
// See RFC 8555 § 6.2, 7.3.
|
|
11
|
-
//
|
|
12
|
-
// Copyright © 2020 Aral Balkan, Small Technology Foundation.
|
|
13
|
-
// License: AGPLv3 or later.
|
|
14
|
-
//
|
|
15
|
-
////////////////////////////////////////////////////////////////////////////////
|
|
1
|
+
/**
|
|
2
|
+
Represents a Let’s Encrypt account.
|
|
3
|
+
|
|
4
|
+
@module
|
|
5
|
+
@copyright Copyright © 2020-present Aral Balkan, Small Technology Foundation.
|
|
6
|
+
@license AGPLv3 or later.
|
|
7
|
+
*/
|
|
16
8
|
|
|
17
9
|
import fs from 'fs'
|
|
18
10
|
import Throws from './util/Throws.js'
|
|
@@ -22,11 +14,23 @@ const throws = new Throws({
|
|
|
22
14
|
// No custom errors are thrown by this class.
|
|
23
15
|
})
|
|
24
16
|
|
|
17
|
+
/**
|
|
18
|
+
(Async; please use Account.getInstanceAsync() factory method.)
|
|
19
|
+
|
|
20
|
+
Represents a Let’s Encrypt account. Currently this is limited to the account
|
|
21
|
+
URL used as the "kid" value in the JWS authenticating subsequent requests
|
|
22
|
+
by this account after it is created using the JWT public key.
|
|
23
|
+
See RFC 8555 § 6.2, 7.3.
|
|
24
|
+
|
|
25
|
+
@alias module:lib/Account
|
|
26
|
+
*/
|
|
25
27
|
export default class Account {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
//
|
|
28
|
+
/**
|
|
29
|
+
Async static factory method. __Use this to instantiate, not the constructor.__
|
|
29
30
|
|
|
31
|
+
@param { import('./Configuration.js').default } configuration
|
|
32
|
+
@public
|
|
33
|
+
*/
|
|
30
34
|
static async getInstanceAsync (configuration) {
|
|
31
35
|
Account.isBeingInstantiatedViaSingletonFactoryMethod = true
|
|
32
36
|
const account = new Account(configuration)
|
|
@@ -39,6 +43,10 @@ export default class Account {
|
|
|
39
43
|
//
|
|
40
44
|
static isBeingInstantiatedViaSingletonFactoryMethod = false
|
|
41
45
|
|
|
46
|
+
/**
|
|
47
|
+
@private
|
|
48
|
+
@param { import('./Configuration.js').default } configuration
|
|
49
|
+
*/
|
|
42
50
|
constructor (configuration) {
|
|
43
51
|
// Ensure factory method-based initialisation.
|
|
44
52
|
if (Account.isBeingInstantiatedViaSingletonFactoryMethod === false) {
|
|
@@ -61,7 +69,7 @@ export default class Account {
|
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
|
|
72
|
+
/** @type { string } */
|
|
65
73
|
get kid () { return this.data.kid }
|
|
66
|
-
set kid (
|
|
74
|
+
set kid (_value) { throws.error(Symbol.for('ReadOnlyAccessorError'), 'kid') }
|
|
67
75
|
}
|