@small-tech/auto-encrypt 4.2.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.
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Global configuration class. Use initialise() method to populate.
3
- *
4
- * @module lib/Configuration
5
- * @exports Configuration
6
- * @copyright © 2020 Aral Balkan, Small Technology Foundation.
7
- * @license AGPLv3 or later.
8
- */
2
+ Global configuration class. Use initialise() method to populate.
3
+
4
+ @module lib/Configuration
5
+ @exports Configuration
6
+ @copyright © 2020 Aral Balkan, Small Technology Foundation.
7
+ @license AGPLv3 or later.
8
+ */
9
9
 
10
10
  import os from 'os'
11
11
  import fs from 'fs'
@@ -21,11 +21,17 @@ const throws = new Throws({
21
21
  () => 'Domains array must be an array of strings'
22
22
  })
23
23
 
24
+ /**
25
+ * @param {any[]} object
26
+ */
24
27
  function isAnArrayOfStrings (object) {
25
- const containsOnlyStrings = arrayOfStrings => arrayOfStrings.length !== 0 && arrayOfStrings.filter(s => typeof s === 'string').length === arrayOfStrings.length
28
+ const containsOnlyStrings = (/** @type {any[]} */ arrayOfStrings) => arrayOfStrings.length !== 0 && arrayOfStrings.filter(s => typeof s === 'string').length === arrayOfStrings.length
26
29
  return Array.isArray(object) && containsOnlyStrings(object)
27
30
  }
28
31
 
32
+ /**
33
+ * @param {fs.PathLike} directory
34
+ */
29
35
  function ensureDirSync (directory) {
30
36
  if (!fs.existsSync(directory)) {
31
37
  fs.mkdirSync(directory, { recursive: true })
@@ -33,9 +39,9 @@ function ensureDirSync (directory) {
33
39
  }
34
40
 
35
41
  /**
36
- * @alias module:lib/Configuration
37
- * @hideconstructor
38
- */
42
+ @alias module:lib/Configuration
43
+ @hideconstructor
44
+ */
39
45
  export default class Configuration {
40
46
  #server = null
41
47
  #domains = null
@@ -47,35 +53,42 @@ export default class Configuration {
47
53
  #certificateIdentityPath = null
48
54
 
49
55
  /**
50
- * Initialise the configuration. Must be called before accessing settings. May be called more than once.
51
- *
52
- * @param {Object} settings Settings to initialise configuration with.
53
- * @param {String[]} settings.domains List of domains Auto Encrypt will manage TLS certs for.
54
- * @param {LetsEncryptServer} settings.server Let’s Encrypt Server to use.
55
- * @param {String} settings.settingsPath Root settings path to use. Will use default path if null.
56
- */
56
+ Initialise the configuration. Must be called before accessing settings. May be called more than once.
57
+
58
+ @typedef { import('./LetsEncryptServer.js').default } LetsEncryptServer
59
+
60
+ @typedef {{
61
+ domains: string[],
62
+ server: LetsEncryptServer,
63
+ settingsPath: string
64
+ }} Settings
65
+
66
+ @param {Settings | void} settings
67
+ */
57
68
  constructor (settings = throws.ifMissing()) {
58
69
 
70
+ const __settings = /** @type { Settings } */ (settings)
71
+
59
72
  // Additional argument validation.
60
- throws.ifUndefinedOrNull(settings.server, 'settings.server')
61
- throws.ifUndefined(settings.settingsPath, 'settings.settingsPath')
62
- throws.if(!isAnArrayOfStrings(settings.domains), Symbol.for('Configuration.domainsArrayIsNotAnArrayOfStringsError'))
73
+ throws.ifUndefinedOrNull(__settings.server, 'settings.server')
74
+ throws.ifUndefined(__settings.settingsPath, 'settings.settingsPath')
75
+ throws.if(!isAnArrayOfStrings(__settings.domains), Symbol.for('Configuration.domainsArrayIsNotAnArrayOfStringsError'))
63
76
 
64
- if (!util.inspect(settings.server).includes('Let’s Encrypt Server (instance)')) {
77
+ if (!util.inspect(__settings.server).includes('Let’s Encrypt Server (instance)')) {
65
78
  throws.error(Symbol.for('ArgumentError'), 'settings.server', 'must be an instance of LetsEncryptServer but isn’t')
66
79
  }
67
80
 
68
- this.#server = settings.server
69
- this.#domains = settings.domains
81
+ this.#server = __settings.server
82
+ this.#domains = __settings.domains
70
83
 
71
- const defaultPathFor = serverName => path.join(os.homedir(), '.small-tech.org', 'auto-encrypt', serverName)
84
+ const defaultPathFor = (/** @type {string} */ serverName) => path.join(os.homedir(), '.small-tech.org', 'auto-encrypt', serverName)
72
85
 
73
- if (settings.settingsPath === null) {
86
+ if (__settings.settingsPath === null) {
74
87
  // Use the correct default path based on staging state.
75
88
  this.#settingsPath = defaultPathFor(this.#server.name)
76
89
  } else {
77
90
  // Create the path to use based on the custom root path we’ve been passed.
78
- this.#settingsPath = path.join(settings.settingsPath, this.#server.name)
91
+ this.#settingsPath = path.join(__settings.settingsPath, this.#server.name)
79
92
  }
80
93
 
81
94
  // And ensure that the settings path exists in the file system.
@@ -104,7 +117,7 @@ export default class Configuration {
104
117
  // For more than 5 domains, show the first two, followed by the number of domains, and a
105
118
  // hash to ensure that the directory name is unique.
106
119
  // e.g., ar.al--www.ar.al--and--4--others--9364bd18ea526b3462e4cebc2a6d97c2ffd3b5312ef7b8bb6165c66a37975e46
107
- const hashOf = arrayOfStrings => crypto.createHash('blake2s256').update(arrayOfStrings.join('--')).digest('hex')
120
+ const hashOf = (/** @type {string[]} */ arrayOfStrings) => crypto.createHash('blake2s256').update(arrayOfStrings.join('--')).digest('hex')
108
121
  return domains.slice(0, 2).join('--').concat(`--and--${domains.length-2}--others--${hashOf(domains)}`)
109
122
  }
110
123
  })(this.#domains)
@@ -125,91 +138,94 @@ export default class Configuration {
125
138
  //
126
139
 
127
140
  /**
128
- * The Let’s Encrypt Server instance.
129
- *
130
- * @type {LetsEncryptServer}
131
- * @readonly
132
- */
141
+ The Let’s Encrypt Server instance.
142
+
143
+ @type {LetsEncryptServer}
144
+ @readonly
145
+ */
133
146
  get server () {
134
147
  return this.#server
135
148
  }
136
149
 
137
150
  /**
138
- * List of domains that Auto Encrypt will manage TLS certificates for.
139
- *
140
- * @type {String[]}
141
- * @readonly
142
- */
151
+ List of domains that Auto Encrypt will manage TLS certificates for.
152
+
153
+ @type {String[]}
154
+ @readonly
155
+ */
143
156
  get domains () {
144
157
  return this.#domains
145
158
  }
146
159
 
147
160
  /**
148
- * The root settings path. There is a different root settings path for pebble, staging and production modes.
149
- *
150
- * @type {String}
151
- * @readonly
152
- */
161
+ The root settings path. There is a different root settings path for pebble, staging and production modes.
162
+
163
+ @type {String}
164
+ @readonly
165
+ */
153
166
  get settingsPath () {
154
167
  return this.#settingsPath
155
168
  }
156
169
 
157
170
  /**
158
- * Path to the account.json file that contains the Key Id that uniquely identifies and authorises your account
159
- * in the absence of a JWT (see RFC 8555 § 6.2. Request Authentication).
160
- *
161
- * @type {String}
162
- * @readonly
163
- */
171
+ Path to the account.json file that contains the Key Id that uniquely identifies and authorises your account
172
+ in the absence of a JWT (see RFC 8555 § 6.2. Request Authentication).
173
+
174
+ @type {String}
175
+ @readonly
176
+ */
164
177
  get accountPath () {
165
178
  return this.#accountPath
166
179
  }
167
180
 
168
181
  /**
169
- * The path to the account-identity.pem file that contains the private key for the account.
170
- *
171
- * @type {String}
172
- * @readonly
173
- */
182
+ The path to the account-identity.pem file that contains the private key for the account.
183
+
184
+ @type {String}
185
+ @readonly
186
+ */
174
187
  get accountIdentityPath () { return this.#accountIdentityPath }
175
188
 
176
189
  /**
177
- * The path to the certificate.pem file that contains the certificate chain provisioned from Let’s Encrypt.
178
- *
179
- * @type {String}
180
- * @readonly
181
- */
190
+ The path to the certificate.pem file that contains the certificate chain provisioned from Let’s Encrypt.
191
+
192
+ @type {String}
193
+ @readonly
194
+ */
182
195
  get certificatePath () { return this.#certificatePath }
183
196
 
184
197
  /**
185
- * The directory the certificate and certificate identity (private key) PEM files are stored in.
186
- *
187
- * @type {String}
188
- * @readonly
189
- */
198
+ The directory the certificate and certificate identity (private key) PEM files are stored in.
199
+
200
+ @type {String}
201
+ @readonly
202
+ */
190
203
  get certificateDirectoryPath () { return this.#certificateDirectoryPath }
191
204
 
192
205
  /**
193
- * The path to the certificate-identity.pem file that holds the private key for the TLS certificate.
194
- *
195
- * @type {String}
196
- * @readonly
197
- */
206
+ The path to the certificate-identity.pem file that holds the private key for the TLS certificate.
207
+
208
+ @type {String}
209
+ @readonly
210
+ */
198
211
  get certificateIdentityPath () { return this.#certificateIdentityPath }
199
212
 
200
213
  //
201
214
  // Enforce read-only access.
202
215
  //
203
216
 
204
- set server (state) { this.throwReadOnlyAccessorError('server') }
205
- set domains (state) { this.throwReadOnlyAccessorError('domains') }
206
- set settingsPath (state) { this.throwReadOnlyAccessorError('settingsPath') }
207
- set accountPath (state) { this.throwReadOnlyAccessorError('accountPath') }
208
- set accountIdentityPath (state) { this.throwReadOnlyAccessorError('accountIdentityPath') }
209
- set certificatePath (state) { this.throwReadOnlyAccessorError('certificatePath') }
210
- set certificateDirectoryPath (state) { this.throwReadOnlyAccessorError('certificateDirectoryPath') }
211
- set certificateIdentityPath (state) { this.throwReadOnlyAccessorError('certificateIdentityPath') }
217
+ set server (_state) { this.throwReadOnlyAccessorError('server') }
218
+ set domains (_state) { this.throwReadOnlyAccessorError('domains') }
219
+ set settingsPath (_state) { this.throwReadOnlyAccessorError('settingsPath') }
220
+ set accountPath (_state) { this.throwReadOnlyAccessorError('accountPath') }
221
+ set accountIdentityPath (_state) { this.throwReadOnlyAccessorError('accountIdentityPath') }
222
+ set certificatePath (_state) { this.throwReadOnlyAccessorError('certificatePath') }
223
+ set certificateDirectoryPath (_state) { this.throwReadOnlyAccessorError('certificateDirectoryPath') }
224
+ set certificateIdentityPath (_state) { this.throwReadOnlyAccessorError('certificateIdentityPath') }
212
225
 
226
+ /**
227
+ * @param {string} setterName
228
+ */
213
229
  throwReadOnlyAccessorError (setterName) {
214
230
  throws.error(Symbol.for('ReadOnlyAccessorError'), setterName, 'All configuration accessors are read-only.')
215
231
  }
package/lib/Directory.js CHANGED
@@ -1,18 +1,15 @@
1
- ////////////////////////////////////////////////////////////////////////////////
2
- //
3
- // Directory
4
- //
5
- // In order to help clients configure themselves with the right URLs for
6
- // each ACME operation, ACME servers provide a directory object. This
7
- // should be the only URL needed to configure clients. (RFC §7.1.1)
8
- //
9
- // Copyright © 2020 Aral Balkan, Small Technology Foundation.
10
- // License: AGPLv3 or later.
11
- //
12
- ////////////////////////////////////////////////////////////////////////////////
1
+ /**
2
+ Directory
3
+
4
+ In order to help clients configure themselves with the right URLs for
5
+ each ACME operation, ACME servers provide a directory object. This
6
+ should be the only URL needed to configure clients. (RFC §7.1.1)
7
+
8
+ Copyright © 2020 Aral Balkan, Small Technology Foundation.
9
+ License: AGPLv3 or later.
10
+ */
13
11
 
14
12
  import util from 'util'
15
- import prepareRequest from 'bent'
16
13
  import log from './util/log.js'
17
14
  import Throws from './util/Throws.js'
18
15
 
@@ -21,16 +18,18 @@ const throws = new Throws()
21
18
  export default class Directory {
22
19
  directory = null
23
20
  letsEncryptServer = null
24
- directoryRequest = null
25
21
 
26
22
  //
27
23
  // Factory method access (async).
28
24
  //
29
25
  static isBeingInstantiatedViaAsyncFactoryMethod = false
30
26
 
27
+ /**
28
+ @param {import('./Configuration.js').default|void} configuration
29
+ */
31
30
  static async getInstanceAsync (configuration = throws.ifMissing()) {
32
31
  Directory.isBeingInstantiatedViaAsyncFactoryMethod = true
33
- const directory = new Directory(configuration)
32
+ const directory = new Directory(/** @type {import('./Configuration.js').default} */ (configuration))
34
33
  await directory.getUrls()
35
34
  return directory
36
35
  }
@@ -47,11 +46,16 @@ export default class Directory {
47
46
  get revokeCertUrl() { return this.directory.revokeCert }
48
47
  get termsOfServiceUrl() { return this.directory.meta.termsOfService }
49
48
  get websiteUrl() { return this.directory.meta.website }
49
+ get renewalInfoUrl() { return this.directory.renewalInfo }
50
+ get profiles() { return this.directory.meta.profiles }
50
51
 
51
52
  //
52
53
  // Private.
53
54
  //
54
55
 
56
+ /**
57
+ @param {import('./Configuration.js').default} configuration
58
+ */
55
59
  constructor(configuration) {
56
60
  // Ensure async factory method instantiation.
57
61
  if (Directory.isBeingInstantiatedViaAsyncFactoryMethod === false) {
@@ -60,7 +64,6 @@ export default class Directory {
60
64
  Directory.isBeingInstantiatedViaAsyncFactoryMethod = false
61
65
 
62
66
  this.letsEncryptServer = configuration.server
63
- this.directoryRequest = prepareRequest('GET', 'json', this.letsEncryptServer.endpoint)
64
67
 
65
68
  log(` 📕 ❨auto-encrypt❩ Directory is using endpoint ${this.letsEncryptServer.endpoint}`)
66
69
  }
@@ -68,7 +71,13 @@ export default class Directory {
68
71
  // (Async) Fetches the latest Urls from the Let’s Encrypt ACME endpoint being used.
69
72
  // This will throw if the request fails. Ensure that you catch the error when
70
73
  // using it.
71
- async getUrls() { this.directory = await this.directoryRequest() }
74
+ async getUrls() {
75
+ const response = await fetch(this.letsEncryptServer.endpoint)
76
+ if (!response.ok) {
77
+ throw new Error(`HTTP Error: ${response.status} ${response.statusText}`)
78
+ }
79
+ this.directory = await response.json()
80
+ }
72
81
 
73
82
  // Custom object description for console output (for debugging).
74
83
  [util.inspect.custom] () {
@@ -86,6 +95,8 @@ export default class Directory {
86
95
  - revokeCertUrl : ${this.revokeCertUrl}
87
96
  - termsOfServiceUrl: ${this.termsOfServiceUrl}
88
97
  - websiteUrl : ${this.websiteUrl}
98
+ - renewalInfoUrl : ${this.renewalInfoUrl}
99
+ - profiles : ${Object.entries(this.profiles).map(([key, value], index) => `${index > 0 ? ', ' : ''}${key} (${value})`)}
89
100
  `
90
101
  }
91
102
  }
package/lib/HttpServer.js CHANGED
@@ -1,29 +1,27 @@
1
+ /**
1
2
 
2
- ////////////////////////////////////////////////////////////////////////////////
3
- //
4
- // HttpServer
5
- //
6
- // (Singleton; please use HttpServer.getSharedInstance() to access.)
7
- //
8
- // A simple HTTP server that:
9
- //
10
- // A. While provisioning Let’s Encrypt certificates:
11
- // =================================================
12
- //
13
- // Acts as a challenge server. See RFC 8555 § 8.3 (HTTP Challenge)
14
- //
15
- // Responds to http-01 challenges and forwards all other
16
- // requests to an HTTPS server that it expects to be active at the same domain.
17
- //
18
- // B. At all other times:
19
- // ======================
20
- //
21
- // Forwards http requests to https requests using a 307 redirect.
22
- //
23
- // Copyright © 2020 Aral Balkan, Small Technology Foundation.
24
- // License: AGPLv3 or later.
25
- //
26
- ////////////////////////////////////////////////////////////////////////////////
3
+ HttpServer
4
+
5
+ (Singleton; please use HttpServer.getSharedInstance() to access.)
6
+
7
+ A simple HTTP server that:
8
+
9
+ A. While provisioning Let’s Encrypt certificates:
10
+ =================================================
11
+
12
+ Acts as a challenge server. See RFC 8555 § 8.3 (HTTP Challenge)
13
+
14
+ Responds to http-01 challenges and forwards all other
15
+ requests to an HTTPS server that it expects to be active at the same domain.
16
+
17
+ B. At all other times:
18
+ ======================
19
+
20
+ Forwards http requests to https requests using a 307 redirect.
21
+
22
+ Copyright © 2020 Aral Balkan, Small Technology Foundation.
23
+ License: AGPLv3 or later.
24
+ */
27
25
 
28
26
  import http from 'http'
29
27
  import encodeUrl from 'encodeurl'
@@ -33,12 +31,18 @@ export default class HttpServer {
33
31
  //
34
32
  // Singleton access (async).
35
33
  //
34
+ /** @type { HttpServer } */
36
35
  static instance = null
37
36
  static isBeingInstantiatedViaSingletonFactoryMethod = false
38
37
 
39
38
  // Is the HTTP server acting as a Let’s Encrypt challenge server?
40
39
  #isChallengeServer = false
41
40
 
41
+ /** @typedef { (request: http.IncomingMessage, response: http.ServerResponse) => Boolean } Responder
42
+
43
+ /** @type Array<Responder> */
44
+ responders
45
+
42
46
  static async getSharedInstance () {
43
47
  if (HttpServer.instance === null) {
44
48
  HttpServer.isBeingInstantiatedViaSingletonFactoryMethod = true
@@ -59,6 +63,11 @@ export default class HttpServer {
59
63
  log(' 🚮 ❨auto-encrypt❩ HTTP Server is destroyed.')
60
64
  }
61
65
 
66
+ /**
67
+ Adds a responder to this server’s responders list.
68
+
69
+ @param { Responder } responder
70
+ */
62
71
  addResponder (responder) {
63
72
  this.responders.push(responder)
64
73
  }
@@ -85,6 +94,8 @@ export default class HttpServer {
85
94
  // Act as a Let’s Encrypt challenge server.
86
95
  let responded = false
87
96
 
97
+ log(` 📥 ❨auto-encrypt❩ Received HTTP request: ${request.url} on ${request.headers.host}`)
98
+
88
99
  for (let i = 0; i < this.responders.length; i++) {
89
100
  const responder = this.responders[i]
90
101
  responded = responder(request, response)
@@ -113,14 +124,19 @@ export default class HttpServer {
113
124
  }
114
125
 
115
126
  // Redirect HTTP to HTTPS.
116
- log(` 👉 ❨auto-encrypt❩ Redirecting HTTP request to HTTPS.`)
127
+ log(` 👉 ❨auto-encrypt❩ Redirecting HTTP request to HTTPS: ${httpsUrl.toString()}`)
117
128
  response.statusCode = 307
118
- response.setHeader('Location', encodeUrl(httpsUrl))
129
+ response.setHeader('Location', encodeUrl(httpsUrl.toString()))
119
130
  response.end()
120
131
  }
121
132
  })
122
133
  }
123
134
 
135
+ /**
136
+ Sets the state of the server (either to challenge server (true) or to HTTP -> HTTPS forwarding server (false)).
137
+
138
+ @param { Boolean } state
139
+ */
124
140
  set challengeServer (state) {
125
141
  if (state) {
126
142
  log(` 🔒 ❨auto-encrypt❩ HTTP server is now only responding to Let’s Encrypt challenges.`)
@@ -138,7 +154,8 @@ export default class HttpServer {
138
154
  // vulnerability at worst in the global digital network age.
139
155
  await new Promise((resolve, reject) => {
140
156
  try {
141
- this.server.listen(80, () => {
157
+ // Listen on all IP addresses, including IPv6.
158
+ this.server.listen(80, '::', () => {
142
159
  log(` 🔒 ❨auto-encrypt❩ HTTP server is listening on port 80.`)
143
160
  resolve()
144
161
  })
@@ -149,12 +166,22 @@ export default class HttpServer {
149
166
  }
150
167
 
151
168
  async destroy () {
169
+ if (!this.server.listening) {
170
+ log(' 🚮 ❨auto-encrypt❩ HTTP Server is not listening. Nothing to destroy.')
171
+ return
172
+ }
173
+
152
174
  // Starts killing all connections and closes the server.
153
175
  this.server.closeAllConnections()
154
176
 
155
177
  await new Promise((resolve, reject) => {
156
178
  this.server.close(error => {
157
179
  if (error) {
180
+ // If the server was already closed or not running, we don't want to throw an error.
181
+ if (error.code === 'ERR_SERVER_NOT_RUNNING') {
182
+ resolve()
183
+ return
184
+ }
158
185
  console.error(error)
159
186
  reject(error)
160
187
  }
@@ -0,0 +1,114 @@
1
+ /**
2
+ Gathers external IPv4 address information using https://ip.small-web.org/
3
+
4
+ And Ipv6 information using built-in `networkInterfaces` function from node:os.
5
+ */
6
+
7
+ import os from 'node:os'
8
+ import dns from 'node:dns'
9
+ import { execSync } from 'node:child_process'
10
+ import Throws from './util/Throws.js'
11
+
12
+ const throws = new Throws()
13
+
14
+ export default class IPAddresses {
15
+ /** @type {IPAddresses} */
16
+ static instance
17
+
18
+ static extractIPv6Address = "sed -n 's/.*inet6 \\([a-f0-9:]*\\).*/\\1/p'"
19
+ static platformSpecificStableIPv6DiscoveryCommands = {
20
+ 'linux': `ip -6 address show scope global | grep -v 'temporary' | ${IPAddresses.extractIPv6Address}`,
21
+ 'darwin': `ifconfig | grep 'inet6' | grep 'secured' | grep -v 'fe80:' | ${IPAddresses.extractIPv6Address}`
22
+ }
23
+
24
+ /** @type {string|undefined} */
25
+ ipv4Address
26
+
27
+ /** @type {Array<string>} */
28
+ ipv6Addresses = []
29
+
30
+ //
31
+ // Factory method access (async).
32
+ //
33
+ static isBeingInstantiatedViaAsyncFactoryMethod = false
34
+
35
+ static async getInstanceAsync () {
36
+ if (this.instance !== undefined) {
37
+ return this.instance
38
+ }
39
+
40
+ // Create singleton instance.
41
+ this.isBeingInstantiatedViaAsyncFactoryMethod = true
42
+
43
+ this.instance = new IPAddresses()
44
+
45
+ // Initialise it asynchronously.
46
+ await this.instance.discoverIpAddresses()
47
+
48
+ return this.instance
49
+ }
50
+
51
+ get hasIPv4Address () {
52
+ return this.ipv4Address !== undefined
53
+ }
54
+
55
+ get hasIPv6Addresses () {
56
+ return this.ipv6Addresses.length > 0
57
+ }
58
+
59
+ //
60
+ // Private.
61
+ //
62
+
63
+ constructor () {
64
+ // Ensure singelton access.
65
+ if (IPAddresses.isBeingInstantiatedViaAsyncFactoryMethod === false) {
66
+ throws.error(Symbol.for('MustBeInstantiatedViaAsyncFactoryMethodError'), 'IPAddresses')
67
+ }
68
+
69
+ // This reverts IP address sort order to pre-Node 17 behaviour.
70
+ // See https://github.com/nodejs/node/issues/40537
71
+ dns.setDefaultResultOrder('ipv4first')
72
+
73
+ IPAddresses.isBeingInstantiatedViaAsyncFactoryMethod = false
74
+ }
75
+
76
+ async discoverIpAddresses() {
77
+ await this.discoverIPv4Address()
78
+ this.discoverIPv6Addresses()
79
+ }
80
+
81
+ async discoverIPv4Address () {
82
+ try {
83
+ const response = await fetch('https://ip.small-web.org/json/')
84
+ if (response.status === 200) {
85
+ const ip = (await response.json()).ip
86
+ if (ip !== '::1') {
87
+ this.ipv4Address = ip
88
+ }
89
+ }
90
+ } catch (error) {
91
+ console.warn('Could not discover external IPv4 address.', error)
92
+ }
93
+ }
94
+
95
+ /**
96
+ Runs platform-specific command for retriving stable IPv6 addresses as we don’t
97
+ have a built-in way of differentiating between stable and temporary addresses
98
+ using Node.js alone (`os.networkInterfaces()` does not have that information.)
99
+ */
100
+ discoverIPv6Addresses () {
101
+ const platform = os.platform()
102
+ const platformSpecificStableIPv6DiscoveryCommand = IPAddresses.platformSpecificStableIPv6DiscoveryCommands[platform]
103
+
104
+ try {
105
+ const stdout = execSync(platformSpecificStableIPv6DiscoveryCommand, { encoding: 'utf-8' })
106
+ this.ipv6Addresses = stdout
107
+ .split('\n')
108
+ .map(address => address.trim())
109
+ .filter(address => address.length > 0)
110
+ } catch (error) {
111
+ console.warn(`[Auto Encrypt] Failed to discover IPv6 addresses using platform-specific (${platform}) command:`, platformSpecificStableIPv6DiscoveryCommand, error)
112
+ }
113
+ }
114
+ }