Skip to content

Commit 98b4304

Browse files
authored
fix: add retries to certificate provisioning (#2841)
The p2p-forge service at libp2p.direct sometimes rejects requests with 401 errors, the only thing to do is to retry. This can probably be reverted in future if it becomes clear why some requests are rejected.
1 parent a82b07d commit 98b4304

File tree

5 files changed

+82
-25
lines changed

5 files changed

+82
-25
lines changed

packages/auto-tls/README.md

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ repo and examine the changes made.
2525
-->
2626

2727
When a publicly dialable address is detected, use the p2p-forge service at
28-
<https://registration.libp2p.direct> to acquire a valid Let's Encrypted-backed
28+
<https://registration.libp2p.direct> to acquire a valid Let's Encrypt-backed
2929
TLS certificate, which the node can then use with the relevant transports.
3030

3131
The node must be configured with a listener for at least one of the following
@@ -82,27 +82,19 @@ console.info(node.getMultiaddrs())
8282
# Install
8383

8484
```console
85-
$ npm i @libp2p/plaintext
86-
```
87-
88-
## Browser `<script>` tag
89-
90-
Loading this module through a script tag will make it's exports available as `Libp2pPlaintext` in the global namespace.
91-
92-
```html
93-
<script src="https://unpkg.com/@libp2p/plaintext/dist/index.min.js"></script>
85+
$ npm i @libp2p/auto-tls
9486
```
9587

9688
# API Docs
9789

98-
- <https://libp2p.github.io/js-libp2p/modules/_libp2p_plaintext.html>
90+
- <https://libp2p.github.io/js-libp2p/modules/_libp2p_auto-tls.html>
9991

10092
# License
10193

10294
Licensed under either of
10395

104-
- Apache 2.0, ([LICENSE-APACHE](https://github.com/libp2p/js-libp2p/blob/main/packages/connection-encrypter-plaintext/LICENSE-APACHE) / <http://www.apache.org/licenses/LICENSE-2.0>)
105-
- MIT ([LICENSE-MIT](https://github.com/libp2p/js-libp2p/blob/main/packages/connection-encrypter-plaintext/LICENSE-MIT) / <http://opensource.org/licenses/MIT>)
96+
- Apache 2.0, ([LICENSE-APACHE](https://github.com/libp2p/js-libp2p/blob/main/packages/auto-tls/LICENSE-APACHE) / <http://www.apache.org/licenses/LICENSE-2.0>)
97+
- MIT ([LICENSE-MIT](https://github.com/libp2p/js-libp2p/blob/main/packages/auto-tls/LICENSE-MIT) / <http://opensource.org/licenses/MIT>)
10698

10799
# Contribution
108100

packages/auto-tls/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
"@multiformats/multiaddr-matcher": "^1.4.0",
5858
"@peculiar/x509": "^1.12.3",
5959
"acme-client": "^5.4.0",
60+
"any-signal": "^4.1.1",
61+
"delay": "^6.0.0",
6062
"interface-datastore": "^8.3.1",
6163
"multiformats": "^13.3.1",
6264
"uint8arrays": "^5.1.0"
@@ -66,7 +68,6 @@
6668
"@libp2p/peer-id": "^5.0.7",
6769
"aegir": "^44.0.1",
6870
"datastore-core": "^10.0.2",
69-
"delay": "^6.0.0",
7071
"p-event": "^6.0.1",
7172
"sinon": "^19.0.2",
7273
"sinon-ts": "^2.0.0"

packages/auto-tls/src/auto-tls.ts

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { ClientAuth } from '@libp2p/http-fetch/auth'
2-
import { serviceCapabilities, serviceDependencies, start, stop } from '@libp2p/interface'
2+
import { serviceCapabilities, serviceDependencies, setMaxListeners, start, stop } from '@libp2p/interface'
33
import { debounce } from '@libp2p/utils/debounce'
44
import { X509Certificate } from '@peculiar/x509'
55
import * as acme from 'acme-client'
6+
import { anySignal } from 'any-signal'
7+
import delay from 'delay'
68
import { Key } from 'interface-datastore'
79
import { base36 } from 'multiformats/bases/base36'
810
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
911
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
1012
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
11-
import { DEFAULT_ACCOUNT_PRIVATE_KEY_BITS, DEFAULT_ACCOUNT_PRIVATE_KEY_NAME, DEFAULT_ACME_DIRECTORY, DEFAULT_CERTIFICATE_DATASTORE_KEY, DEFAULT_CERTIFICATE_PRIVATE_KEY_BITS, DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME, DEFAULT_FORGE_DOMAIN, DEFAULT_FORGE_ENDPOINT, DEFAULT_PROVISION_DELAY, DEFAULT_PROVISION_TIMEOUT, DEFAULT_RENEWAL_THRESHOLD } from './constants.js'
13+
import { DEFAULT_ACCOUNT_PRIVATE_KEY_BITS, DEFAULT_ACCOUNT_PRIVATE_KEY_NAME, DEFAULT_ACME_DIRECTORY, DEFAULT_CERTIFICATE_DATASTORE_KEY, DEFAULT_CERTIFICATE_PRIVATE_KEY_BITS, DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME, DEFAULT_FORGE_DOMAIN, DEFAULT_FORGE_ENDPOINT, DEFAULT_PROVISION_DELAY, DEFAULT_PROVISION_REQUEST_TIMEOUT, DEFAULT_PROVISION_TIMEOUT, DEFAULT_RENEWAL_THRESHOLD } from './constants.js'
1214
import { DomainMapper } from './domain-mapper.js'
1315
import { createCsr, importFromPem, loadOrCreateKey, supportedAddressesFilter } from './utils.js'
1416
import type { AutoTLSComponents, AutoTLSInit, AutoTLS as AutoTLSInterface } from './index.js'
@@ -19,6 +21,8 @@ import type { DebouncedFunction } from '@libp2p/utils/debounce'
1921
import type { Multiaddr } from '@multiformats/multiaddr'
2022
import type { Datastore } from 'interface-datastore'
2123

24+
const RETRY_DELAY = 5_000
25+
2226
type CertificateEvent = 'certificate:provision' | 'certificate:renew'
2327

2428
interface Certificate {
@@ -40,6 +44,7 @@ export class AutoTLS implements AutoTLSInterface {
4044
private readonly acmeDirectory: URL
4145
private readonly clientAuth: ClientAuth
4246
private readonly provisionTimeout: number
47+
private readonly provisionRequestTimeout: number
4348
private readonly renewThreshold: number
4449
private started: boolean
4550
private shutdownController?: AbortController
@@ -68,6 +73,7 @@ export class AutoTLS implements AutoTLSInterface {
6873
this.forgeDomain = init.forgeDomain ?? DEFAULT_FORGE_DOMAIN
6974
this.acmeDirectory = new URL(init.acmeDirectory ?? DEFAULT_ACME_DIRECTORY)
7075
this.provisionTimeout = init.provisionTimeout ?? DEFAULT_PROVISION_TIMEOUT
76+
this.provisionRequestTimeout = init.provisionRequestTimeout ?? DEFAULT_PROVISION_REQUEST_TIMEOUT
7177
this.renewThreshold = init.renewThreshold ?? DEFAULT_RENEWAL_THRESHOLD
7278
this.accountPrivateKeyName = init.accountPrivateKeyName ?? DEFAULT_ACCOUNT_PRIVATE_KEY_NAME
7379
this.accountPrivateKeyBits = init.accountPrivateKeyBits ?? DEFAULT_ACCOUNT_PRIVATE_KEY_BITS
@@ -108,6 +114,7 @@ export class AutoTLS implements AutoTLSInterface {
108114
await start(this.domainMapper)
109115
this.events.addEventListener('self:peer:update', this.onSelfPeerUpdate)
110116
this.shutdownController = new AbortController()
117+
setMaxListeners(Infinity, this.shutdownController.signal)
111118
this.started = true
112119
}
113120

@@ -120,7 +127,8 @@ export class AutoTLS implements AutoTLSInterface {
120127
}
121128

122129
private _onSelfPeerUpdate (): void {
123-
const addresses = this.addressManager.getAddresses().filter(supportedAddressesFilter)
130+
const addresses = this.addressManager.getAddresses()
131+
.filter(supportedAddressesFilter)
124132

125133
if (addresses.length === 0) {
126134
this.log('not fetching certificate as we have no public addresses')
@@ -139,11 +147,29 @@ export class AutoTLS implements AutoTLSInterface {
139147

140148
this.fetching = true
141149

142-
this.fetchCertificate(addresses, {
143-
signal: AbortSignal.timeout(this.provisionTimeout)
150+
Promise.resolve().then(async () => {
151+
let attempt = 0
152+
153+
while (true) {
154+
if (this.shutdownController?.signal.aborted === true) {
155+
throw this.shutdownController.signal.reason
156+
}
157+
158+
try {
159+
await this.fetchCertificate(addresses, {
160+
signal: AbortSignal.timeout(this.provisionTimeout)
161+
})
162+
163+
return
164+
} catch (err) {
165+
this.log.error('provisioning certificate failed on attempt %d - %e', attempt++, err)
166+
}
167+
168+
await delay(RETRY_DELAY)
169+
}
144170
})
145171
.catch(err => {
146-
this.log.error('error fetching certificates - %e', err)
172+
this.log.error('giving up provisioning certificate - %e', err)
147173
})
148174
.finally(() => {
149175
this.fetching = false
@@ -190,7 +216,9 @@ export class AutoTLS implements AutoTLSInterface {
190216
// emit a certificate event
191217
this.log('dispatching %s', event)
192218
this.events.safeDispatchEvent(event, {
193-
detail: this.certificate
219+
detail: {
220+
...this.certificate
221+
}
194222
})
195223
}
196224

@@ -271,7 +299,33 @@ export class AutoTLS implements AutoTLSInterface {
271299
email: this.email,
272300
termsOfServiceAgreed: true,
273301
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
274-
await this.configureAcmeChallengeResponse(multiaddrs, keyAuthorization, options)
302+
const signal = anySignal([this.shutdownController?.signal, options?.signal])
303+
setMaxListeners(Infinity, signal)
304+
305+
let attempt = 0
306+
307+
while (true) {
308+
if (signal.aborted) {
309+
throw signal.reason
310+
}
311+
312+
try {
313+
const timeout = AbortSignal.timeout(this.provisionRequestTimeout)
314+
const signal = anySignal([timeout, options?.signal])
315+
setMaxListeners(Infinity, timeout, signal)
316+
317+
await this.configureAcmeChallengeResponse(multiaddrs, keyAuthorization, {
318+
...options,
319+
signal
320+
})
321+
322+
return
323+
} catch (err: any) {
324+
this.log.error('contacting %s failed on attempt %d - %e', this.forgeEndpoint, attempt++, err.cause ?? err)
325+
}
326+
327+
await delay(RETRY_DELAY)
328+
}
275329
},
276330
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
277331
// no-op
@@ -285,8 +339,9 @@ export class AutoTLS implements AutoTLSInterface {
285339
const addresses = multiaddrs.map(ma => ma.toString())
286340

287341
const endpoint = `${this.forgeEndpoint}v1/_acme-challenge`
288-
this.log('asking %sv1/_acme-challenge to respond to the acme DNS challenge on our behalf', endpoint)
342+
this.log('asking %s to respond to the acme DNS challenge on our behalf', endpoint)
289343
this.log('dialback public addresses: %s', addresses.join(', '))
344+
290345
const response = await this.clientAuth.authenticatedFetch(endpoint, {
291346
method: 'POST',
292347
headers: {

packages/auto-tls/src/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
export const DEFAULT_FORGE_ENDPOINT = 'https://registration.libp2p.direct'
22
export const DEFAULT_FORGE_DOMAIN = 'libp2p.direct'
33
export const DEFAULT_ACME_DIRECTORY = 'https://acme-v02.api.letsencrypt.org/directory'
4-
export const DEFAULT_PROVISION_TIMEOUT = 10_000
4+
export const DEFAULT_PROVISION_TIMEOUT = 120_000
5+
export const DEFAULT_PROVISION_REQUEST_TIMEOUT = 10_000
56
export const DEFAULT_PROVISION_DELAY = 5_000
67
export const DEFAULT_RENEWAL_THRESHOLD = 86_400_000
78
export const DEFAULT_ACCOUNT_PRIVATE_KEY_NAME = 'auto-tls-acme-account-private-key'

packages/auto-tls/src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,18 @@ export interface AutoTLSInit {
104104
/**
105105
* How long to attempt to acquire a certificate before timing out in ms
106106
*
107-
* @default 10000
107+
* @default 120_000
108108
*/
109109
provisionTimeout?: number
110110

111+
/**
112+
* How long asking the forge endpoint to answer a DNS challenge can take
113+
* before we retry
114+
*
115+
* @default 10_000
116+
*/
117+
provisionRequestTimeout?: number
118+
111119
/**
112120
* Certificates are acquired when the `self:peer:update` event fires, which
113121
* happens when the node's addresses change. To avoid starting to map ports

0 commit comments

Comments
 (0)