pairnet 0.1.0 → 0.2.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.
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+
3
+ module.exports = require("@joepie91/eslint-config");
package/index.js CHANGED
@@ -2,30 +2,42 @@
2
2
 
3
3
  const crypto = require("crypto");
4
4
  const tls = require("tls");
5
- const debug = require("debug")("pairnet");
5
+ const makeDebug = require("debug-instanced")("pairnet");
6
6
  const matchValue = require("match-value");
7
+ const getIPs = require("get-ips");
7
8
  const pipe = require("@promistream/pipe");
8
9
  const simpleSink = require("@promistream/simple-sink");
9
10
 
10
- const createMemoryStorage = require("./storages/memory");
11
- const createFileStorage = require("./storages/file");
12
- const { NotSet } = require("./storages/symbols");
11
+ const createMemoryStorage = require("./lib/storages/memory");
12
+ const createFileStorage = require("./lib/storages/file");
13
+ const { NotSet, AlreadySet } = require("./lib/storages/symbols");
13
14
 
14
- const generateKeys = require("./lib/generate-keys");
15
+ const generateKeys = require("./lib/tls/generate-keys");
15
16
  const createBackgroundTaskManager = require("./lib/background-tasks");
16
17
  const createStore = require("./lib/store");
17
- const getFingerprint = require("./lib/get-fingerprint");
18
- const maybeCall = require("./lib/maybe-call");
18
+ const getFingerprint = require("./lib/tls/get-fingerprint");
19
19
  const handleProtocol = require("./lib/protocol");
20
- const serverListen = require("./promisified/server-listen");
21
- const serverClose = require("./promisified/server-close");
22
-
23
- const { validateOptions } = require("@validatem/core");
20
+ const serverListen = require("./lib/promisified/server-listen");
21
+ const serverClose = require("./lib/promisified/server-close");
22
+ const wrappedToken = require("./lib/pairing-token/wrapped");
23
+ const generatePairingToken = require("./lib/pairing-token/generate");
24
+ const connectMultiple = require("./lib/network/connect-multiple");
25
+ const reduceAddresses = require("./lib/pairing-token/reduce-addresses");
26
+ const createQueryError = require("./lib/create-query-error");
27
+
28
+ const { validateOptions, validateArguments } = require("@validatem/core");
29
+ const ValidationError = require("@validatem/error");
24
30
  const required = require("@validatem/required");
31
+ const defaultTo = require("@validatem/default-to");
32
+ const requireEither = require("@validatem/require-either");
33
+ const either = require("@validatem/either");
34
+ const arrayOf = require("@validatem/array-of");
35
+ const allowExtraProperties = require("@validatem/allow-extra-properties");
25
36
  const isFunction = require("@validatem/is-function");
26
- const ValidationError = require("@validatem/error");
27
- const isInteger = require("@validatem/is-integer");
37
+ const isBuffer = require("@validatem/is-buffer");
38
+ const isBoolean = require("@validatem/is-boolean");
28
39
  const isNonEmptyString = require("@validatem/is-non-empty-string");
40
+ const isPortNumber = require("@validatem/is-port-number");
29
41
 
30
42
  /* TODO:
31
43
  - Verify that errors don't get eaten anywhere
@@ -33,16 +45,10 @@ const isNonEmptyString = require("@validatem/is-non-empty-string");
33
45
  - Auto-reconnection
34
46
  */
35
47
 
36
- function createQueryError(message) {
37
- let error = new Error("Query resulted in an error");
38
- error.code = "QUERY_ERROR_RESPONSE";
39
- error.messageData = message;
40
- return error;
41
- }
42
-
43
- async function makeClientSocket({ hostname, port, options, onQuery, protocolConfiguration }) {
44
- let socket = tls.connect({
45
- host: hostname,
48
+ async function makeClientSocket({ addresses, port, options, onQuery, protocolConfiguration, debug }) {
49
+ // FIXME: tolerate protocol mismatches in negotiation, and fall over to the next address in that case as well
50
+ // TODO: Skip IPv6 addresses if we can determine that the system doesn't have IPv6 connectivity anyway, and likewise for IPv4; and figure out how to test that that works correctly
51
+ let socket = await connectMultiple(addresses, {
46
52
  port: port,
47
53
  rejectUnauthorized: false,
48
54
  // We manually pin the certificate, so we don't have to care about the subject name at all; that's why checkServerIdentity always passes
@@ -53,12 +59,15 @@ async function makeClientSocket({ hostname, port, options, onQuery, protocolConf
53
59
  });
54
60
 
55
61
  let api = await handleProtocol(socket, {
62
+ debug,
56
63
  protocolConfiguration: protocolConfiguration,
64
+ role: "client",
57
65
  onQuery: onQuery
58
66
  });
59
67
 
60
68
  return {
61
69
  peerFingerprint: getFingerprint(socket),
70
+ peerAddress: socket.remoteAddress,
62
71
  eventStream: api.eventStream,
63
72
  send: api.sender.sendEvent,
64
73
  query: api.sender.sendQuery,
@@ -73,16 +82,23 @@ function isStorageProvider(value) {
73
82
  }
74
83
 
75
84
  module.exports = {
85
+ NotSet: NotSet,
86
+ AlreadySet: AlreadySet,
76
87
  createQueryError: createQueryError,
77
88
  createMemoryStorage: createMemoryStorage,
78
89
  createFileStorage: createFileStorage,
79
90
  createPeer: async function createPeer(_options) {
80
- let { storage, generateClientHello, onClientHello, onServerHello } = validateOptions(arguments, {
91
+ let debug = makeDebug("peer");
92
+
93
+ let { storage, generateClientHello, onClientHello, onServerHello } = validateOptions(arguments, [{
81
94
  storage: [ required, isStorageProvider ],
82
- generateClientHello: [ required, isFunction ],
83
- onClientHello: [ required, isFunction ],
84
- onServerHello: [ required, isFunction ]
85
- });
95
+ generateClientHello: [ isFunction ],
96
+ onClientHello: [ isFunction ],
97
+ onServerHello: [ isFunction ]
98
+ }, either([
99
+ allowExtraProperties({ generateClientHello: required, onServerHello: required }),
100
+ allowExtraProperties({ onClientHello: required })
101
+ ])]);
86
102
 
87
103
  // FIXME: Why do we still have this fragile 'background task' mechanism?
88
104
  let backgroundTasks = createBackgroundTaskManager();
@@ -126,104 +142,181 @@ module.exports = {
126
142
  }
127
143
  };
128
144
  }
129
-
130
- return {
131
- fingerprint: identity.fingerprint,
132
- generatePairingToken: async function () {
133
- let token = crypto.randomBytes(16).toString("hex");
134
- // TODO: Expiry
135
- await store.setValidPairingToken(token);
136
- debug(`[generatePairingToken] Generated pairing token ${token}`);
137
- return token;
138
- },
139
- pair: async function (_options) {
140
- let { hostname, port, fingerprint, pairingToken } = validateOptions(arguments, {
141
- hostname: [ required, isNonEmptyString ],
142
- port: [ required, isInteger ],
143
- fingerprint: [ required, isNonEmptyString ],
144
- pairingToken: [ required, isNonEmptyString ]
145
- });
146
145
 
147
- if (protocolConfiguration.isClient) {
148
- debug(`[pair] Initiating pairing connection to ${hostname}:${port}...`);
146
+ async function makeRawPairingToken() {
147
+ let token = generatePairingToken();
148
+ // TODO: Expiry
149
+ // TODO: Allow data to be associated with the token?
150
+ await store.setValidPairingToken(token);
151
+ debug(`[generatePairingToken] Generated pairing token ${token}`);
152
+ return token;
153
+ }
149
154
 
150
- let socket = await makeClientSocket({
151
- hostname, port, protocolConfiguration,
152
- // We don't care about anything other than pairing messages in this mode, which are in the form of a query reply
153
- onQuery: () => {}
154
- });
155
+ async function pairRaw(_options) {
156
+ let { hostname, addresses, port, fingerprint, pairingToken } = validateOptions(arguments, [{
157
+ hostname: [ isNonEmptyString ],
158
+ addresses: [ arrayOf([ required, isNonEmptyString ]) ],
159
+ port: [ required, isPortNumber ],
160
+ fingerprint: [ required, isNonEmptyString ],
161
+ pairingToken: [ required, isBuffer ]
162
+ }, requireEither([ "hostname", "addresses" ])]);
163
+
164
+ if (protocolConfiguration.isClient) {
165
+ debug(`[pair] Initiating pairing connection to ${hostname}:${port}...`);
166
+
167
+ let socket = await makeClientSocket({
168
+ addresses: addresses ?? [ hostname ],
169
+ debug, port, protocolConfiguration,
170
+ // We don't care about anything other than pairing messages in this mode, which are in the form of a query reply
171
+ onQuery: () => {}
172
+ });
155
173
 
156
- debug(`[pair] Expected fingerprint: ${fingerprint}`);
157
- debug(`[pair] Actual fingerprint: ${socket.peerFingerprint}`);
174
+ /* NOTE: We put the address that actually worked at the front of the list, so that that's the first thing we try upon connecting. But we leave the rest of the list in our store as well, in case we need to fall back (eg. due to temporary connectivity issues over IPv4 or IPv6 specifically). */
175
+ // TODO: Detect when the successful address does not exist in the original address list? To detect potential wrong assumptions in our implementation.
176
+ let orderedAddresses = (addresses != null)
177
+ ? [ socket.peerAddress, ... addresses.filter((addr) => addr !== socket.peerAddress) ]
178
+ : [ hostname ];
158
179
 
159
- if (socket.peerFingerprint !== fingerprint) {
160
- await Promise.all([
161
- socket.disconnect(),
162
- backgroundTasks.awaitCompletion()
163
- ]);
164
- throw new Error(`Fingerprint of server certificate does not match expected fingerprint`);
165
- }
166
-
167
- // We need to drive the read loop, even though we don't care about the events
168
- let reader = pipe([
169
- socket.eventStream,
170
- simpleSink(() => {
171
- debug("[pair] Ignored event message in pairing mode");
172
- })
173
- ]);
180
+ debug(`[pair] Expected fingerprint: ${fingerprint}`);
181
+ debug(`[pair] Actual fingerprint: ${socket.peerFingerprint}`);
174
182
 
183
+ if (socket.peerFingerprint !== fingerprint) {
175
184
  await Promise.all([
176
- // FIXME: Await completion of background tasks?
177
- reader.read(),
178
- (async () => {
179
- try {
180
- // Let any pairing errors throw
181
- await socket.query({ type: "PAIR", token: pairingToken });
182
- await store.trustPeer(fingerprint);
183
- } finally {
184
- await socket.disconnect();
185
- }
186
- })()
185
+ socket.disconnect(),
186
+ backgroundTasks.awaitCompletion()
187
187
  ]);
188
+ throw new Error(`Fingerprint of server certificate does not match expected fingerprint`);
189
+ }
190
+
191
+ // We need to drive the read loop, even though we don't care about the events
192
+ let reader = pipe([
193
+ socket.eventStream,
194
+ simpleSink(() => {
195
+ debug("[pair] Ignored event message in pairing mode");
196
+ })
197
+ ]);
198
+
199
+ await Promise.all([
200
+ // FIXME: Await completion of background tasks?
201
+ reader.read(),
202
+ (async () => {
203
+ try {
204
+ // Let any pairing errors throw
205
+ await socket.query({ type: "PAIR", token: pairingToken });
206
+ await store.trustPeer(fingerprint, { addresses: orderedAddresses, port });
207
+ } finally {
208
+ await socket.disconnect();
209
+ }
210
+ })()
211
+ ]);
212
+
213
+ return fingerprint;
214
+ } else {
215
+ throw new Error(`This peer is not configured as a client`);
216
+ }
217
+ }
218
+
219
+ async function connectRaw({ addresses, port, onQuery }) {
220
+ /* FIXME[security]: Verify that there are no exploitable issues if eg. one trusted peer presents the certificate of another trusted peer on an unexpected address (especially when connecting by fingerprint!) */
221
+ if (protocolConfiguration.isClient) {
222
+ debug(`[connect] Initiating client connection to (${addresses.join(", ")}):${port}...`);
223
+
224
+ // connect with client certificate made from own identity, and validate server pubkey
225
+ let socket = await makeClientSocket({
226
+ addresses: addresses,
227
+ debug, port, protocolConfiguration,
228
+ onQuery: onQuery
229
+ });
230
+
231
+ let isPaired = await store.isPeerTrusted(socket.peerFingerprint);
232
+
233
+ debug(`[connect] Peer fingerprint: ${socket.peerFingerprint}`);
234
+ debug(`[connect] Is trusted: ${isPaired}`);
235
+
236
+ if (isPaired !== true) {
237
+ await socket.disconnect();
238
+ throw new Error(`Not paired to specified peer`);
188
239
  } else {
189
- throw new Error(`This peer is not configured as a client`);
240
+ return makeProtocolPeer(socket, backgroundTasks);
190
241
  }
242
+ } else {
243
+ throw new Error(`This peer is not configured as a client`);
244
+ }
245
+ }
246
+
247
+ return {
248
+ fingerprint: identity.fingerprint,
249
+ generatePairingToken: async function (_port, _options) {
250
+ let [ port, { compact }] = validateArguments(arguments, {
251
+ port: [ required, isPortNumber ],
252
+ options: [ defaultTo({}), {
253
+ compact: [ defaultTo(false), isBoolean ]
254
+ }]
255
+ });
256
+
257
+ let ips = await getIPs();
258
+
259
+ return wrappedToken.encode({
260
+ addresses: (compact === true)
261
+ ? reduceAddresses(ips.all)
262
+ : ips.all,
263
+ port: port,
264
+ rawToken: await makeRawPairingToken(),
265
+ fingerprint: identity.fingerprint,
266
+ });
191
267
  },
192
- connect: async function (_options) {
193
- let { hostname, port, onQuery } = validateOptions(arguments, {
194
- hostname: [ required, isNonEmptyString ],
195
- port: [ required, isInteger ],
196
- onQuery: [ required, isFunction ],
268
+ generateRawPairingToken: makeRawPairingToken,
269
+ pair: function (_token) {
270
+ let [ token ] = validateArguments(arguments, {
271
+ token: [ required, isNonEmptyString ]
197
272
  });
198
273
 
199
- if (protocolConfiguration.isClient) {
200
- debug(`[connect] Initiating client connection to ${hostname}:${port}...`);
274
+ let tokenData = wrappedToken.decode(token);
201
275
 
202
- // connect with client certificate made from own identity, and validate server pubkey
203
- let socket = await makeClientSocket({
204
- hostname, port, protocolConfiguration,
276
+ return pairRaw({
277
+ addresses: tokenData.addresses.map((item) => item.address),
278
+ port: tokenData.port,
279
+ fingerprint: tokenData.fingerprint,
280
+ pairingToken: tokenData.rawToken
281
+ });
282
+ },
283
+ pairRaw: pairRaw,
284
+ async connect(_options) {
285
+ let { fingerprint, onQuery } = validateOptions(arguments, {
286
+ fingerprint: [ required, isNonEmptyString ], // FIXME: Validate stricter
287
+ onQuery: [ required, isFunction ],
288
+ });
289
+
290
+ let storedData = await store.getPeerData(fingerprint);
291
+
292
+ if (storedData === NotSet) {
293
+ throw new Error(`Specified fingerprint is not known, cannot connect`);
294
+ } else if (storedData.addresses == null) {
295
+ throw new Error(`No stored addresses known for peer; try connectRaw instead`);
296
+ } else {
297
+ return connectRaw({
298
+ addresses: storedData.addresses,
299
+ port: storedData.port,
205
300
  onQuery: onQuery
206
301
  });
207
-
208
- let isPaired = await store.isPeerTrusted(socket.peerFingerprint);
209
-
210
- debug(`[connect] Peer fingerprint: ${socket.peerFingerprint}`);
211
- debug(`[connect] Is trusted: ${isPaired}`);
212
-
213
- if (isPaired !== true) {
214
- await socket.disconnect();
215
- throw new Error(`Not paired to specified peer`);
216
- } else {
217
- return makeProtocolPeer(socket, backgroundTasks);
218
- }
219
- } else {
220
- throw new Error(`This peer is not configured as a client`);
221
302
  }
222
303
  },
304
+ connectRaw: async function (_options) {
305
+ let { hostname, port, onQuery } = validateOptions(arguments, {
306
+ hostname: [ required, isNonEmptyString ],
307
+ port: [ required, isPortNumber ],
308
+ onQuery: [ required, isFunction ],
309
+ });
310
+
311
+ return connectRaw({
312
+ addresses: [ hostname ],
313
+ port, onQuery,
314
+ });
315
+ },
223
316
  createServer: async function (_option) {
224
317
  let { hostname, port, onConnect, onDisconnect, onQuery } = validateOptions(arguments, {
225
- hostname: [ required, isNonEmptyString ],
226
- port: [ required, isInteger ],
318
+ hostname: [ isNonEmptyString ], /* undefined == listen on all public interfaces */
319
+ port: [ required, isPortNumber ],
227
320
  onConnect: [ required, isFunction ],
228
321
  onDisconnect: [ isFunction ],
229
322
  onQuery: [ required, isFunction ]
@@ -252,17 +345,20 @@ module.exports = {
252
345
  debug(`[server] Is trusted: ${isPaired}`);
253
346
 
254
347
  let api = await handleProtocol(socket, {
348
+ debug,
255
349
  protocolConfiguration,
350
+ role: "server",
256
351
  onQuery: async (query) => {
257
352
  return matchValue(query.type, {
258
353
  PAIR: async () => {
259
354
  if (isPaired) {
355
+ // FIXME: Query error instead?
260
356
  throw new Error(`Already paired`);
261
357
  } else {
262
358
  let { token } = query;
263
359
 
264
360
  if (await store.isValidPairingToken(token) === true) {
265
- await store.trustPeer(clientFingerprint);
361
+ await store.trustPeer(clientFingerprint, { /* FIXME */ });
266
362
  await store.removeValidPairingToken(token);
267
363
  return true;
268
364
  } else {
@@ -276,6 +372,7 @@ module.exports = {
276
372
  return onQuery(clientFingerprint, query.m);
277
373
  } else {
278
374
  // FIXME: Force disconnect?
375
+ // FIXME: Query error instead?
279
376
  throw new Error(`Client unauthorized`);
280
377
  }
281
378
  },
@@ -299,7 +396,7 @@ module.exports = {
299
396
  await onConnect(protocolPeer);
300
397
 
301
398
  socket.on("close", () => {
302
- maybeCall(onDisconnect, protocolPeer);
399
+ onDisconnect?.(protocolPeer);
303
400
  });
304
401
  }
305
402
 
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+
3
+ module.exports = function createQueryError(message) {
4
+ let error = new Error("Query resulted in an error");
5
+ error.code = "QUERY_ERROR_RESPONSE";
6
+ error.messageData = message;
7
+ return error;
8
+ };
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ const assert = require("node:assert");
4
+ const tls = require("node:tls");
5
+ const timers = require("node:timers/promises");
6
+
7
+ function noop() {}
8
+
9
+ module.exports = async function connectMultiple(addresses, options) {
10
+ return new Promise(async (resolveWinner, rejectAll) => {
11
+ /* attempt a connection to a series of addresses, until one succeeds. start a new connection every time a previous connection fails *or* every X milliseconds, whichever comes first, and then consider the 'winning' connection to be whichever one successfully completes connecting first. this mechanism ensures that when some addresses are very slow to respond (eg. due to network weather), this doesn't hold up any faster options that may appear after it in the list, keeping the delay from having to fail over to a different address minimal. */
12
+ /* NOTE: This function assumes that only IP addresses are passed in the `addresses` array; in particular, it assumes that if any connection attempt failed for a given address, that entire address has failed. This is not compatible with the family auto-selection that may occur for DNS names, where only the second IP address attempted may succeed. */
13
+ let pendingConnections = new Set();
14
+ let allConnectionPromises = [];
15
+ let connectionErrors = [];
16
+ let winner;
17
+
18
+ for (let address of addresses) {
19
+ assert(address != null); /* undefined == implicit localhost */
20
+ let connection = tls.connect({ ... options, host: address });
21
+ pendingConnections.add(connection);
22
+
23
+ // eslint-disable-next-line no-loop-func
24
+ let connectionPromise = new Promise((resolve, reject) => {
25
+ let timeout = setTimeout(() => connection.destroy(), 5000);
26
+
27
+ connection.on("error", noop);
28
+
29
+ connection.on("connect", () => {
30
+ clearTimeout(timeout);
31
+
32
+ /* Abort any other connection attempts (whether or not they have already connected, although the latter is very unlikely to happen) */
33
+ for (let otherCandidate of pendingConnections) {
34
+ if (otherCandidate !== connection) {
35
+ otherCandidate.destroy();
36
+ }
37
+ }
38
+
39
+ winner = connection;
40
+ resolve();
41
+ resolveWinner(connection);
42
+ });
43
+
44
+ connection.on("connectionAttemptFailed", (ip, port, family, error) => {
45
+ pendingConnections.delete(connection);
46
+ reject(error);
47
+ });
48
+ });
49
+
50
+ allConnectionPromises.push(connectionPromise.catch(() => {/*ignore*/}));
51
+
52
+ await Promise.race([
53
+ timers.setTimeout(100),
54
+ connectionPromise.catch((error) => { connectionErrors.push(error); })
55
+ ]);
56
+
57
+ if (winner != null) {
58
+ break;
59
+ }
60
+ }
61
+
62
+ /* Wait for every connection attempt to complete, one of them may just be slow (and we want all of the connection errors in any case) */
63
+ await Promise.all(allConnectionPromises);
64
+
65
+ if (winner == null) {
66
+ let aggregateError = new Error("Could not connect to any of the specified addresses");
67
+ aggregateError.errors = connectionErrors;
68
+ rejectAll(aggregateError);
69
+ } else {
70
+ /* By this point the socket is no longer under our control, and we want to make sure that we don't swallow any errors after this point */
71
+ winner.removeListener("error", noop);
72
+ }
73
+ });
74
+ };
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ const crypto = require("node:crypto");
4
+
5
+ module.exports = function generatePairingToken() {
6
+ return crypto.randomBytes(16);
7
+ };
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ const isLocalIP = require("is-local-ip");
4
+
5
+ module.exports = function reduceAddresses(addresses) {
6
+ let seen4 = false;
7
+ let seen6 = false;
8
+ let reducedList = [];
9
+
10
+ for (let address of addresses) {
11
+ if (address.family === 4 && !seen4 && !isLocalIP(address.address)) {
12
+ seen4 = true;
13
+ reducedList.push(address);
14
+ } else if (address.family === 6 && !seen6 && !isLocalIP(address.address)) {
15
+ seen6 = true;
16
+ reducedList.push(address);
17
+ }
18
+ }
19
+
20
+ return reducedList;
21
+ };
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+
3
+ const assert = require("node:assert");
4
+ const cbor = require("cbor");
5
+
6
+ function uncompactAddress(compactedAddress) {
7
+ if (Buffer.isBuffer(compactedAddress)) {
8
+ /* IPv4 address */
9
+ return {
10
+ family: 4,
11
+ address: Array.from(compactedAddress)
12
+ .map((byte) => byte.toString(10))
13
+ .join(".")
14
+ };
15
+ } else {
16
+ /* IPv6 address */
17
+ return {
18
+ family: 6,
19
+ address: compactedAddress
20
+ .map((value) => value == null ? "" : value.toString(16))
21
+ .join(":")
22
+ };
23
+ }
24
+ }
25
+
26
+ function uncompactFingerprint(compactedFingerprint) {
27
+ return Array.from(compactedFingerprint)
28
+ .map((byte) => byte.toString(16).padStart(2, "0"))
29
+ .join(":")
30
+ .toUpperCase();
31
+ }
32
+
33
+ function compactAddress(address, family) {
34
+ /* This significantly reduces encoded token size */
35
+ // FIXME: Stricter validation/assertions to catch bugs loudly
36
+ if (family === 4) {
37
+ return Buffer.from(address.split(".").map((segment) => parseInt(segment, 10)));
38
+ } else if (family === 6) {
39
+ return address.split(":").map((segment) => segment === "" ? null : parseInt(segment, 16));
40
+ }
41
+ }
42
+
43
+ function compactFingerprint(fingerprint) {
44
+ // FIXME: Stricter validation/assertions to catch bugs loudly
45
+ return Buffer.from(fingerprint.split(":").map((byte) => parseInt(byte, 16)));
46
+ }
47
+
48
+ module.exports = {
49
+ encode({ addresses, port, rawToken, fingerprint }) {
50
+ /* We use an array to shave a couple bytes off the token size */
51
+ let tokenContents = [
52
+ 0, /* token version */
53
+ addresses.map((ip) => compactAddress(ip.address, ip.family)),
54
+ port,
55
+ compactFingerprint(fingerprint),
56
+ rawToken,
57
+ ];
58
+
59
+ return cbor.encode(tokenContents)
60
+ .toString("base64")
61
+ .replace(/\//g, "_")
62
+ .replace(/\+/g, "-")
63
+ .replace(/=+$/, "");
64
+ },
65
+ decode(wrappedToken) {
66
+ // FIXME: Validate decoded token strictly!
67
+ let data = cbor.decode(Buffer.from(wrappedToken, "base64"));
68
+ assert(Array.isArray(data) && data.length === 5);
69
+
70
+ let [ tokenVersion, compactedAddresses, port, compactedFingerprint, rawToken ] = data;
71
+ assert(tokenVersion === 0);
72
+ let addresses = compactedAddresses.map((address) => uncompactAddress(address));
73
+ let fingerprint = uncompactFingerprint(compactedFingerprint);
74
+
75
+ return { addresses, port, rawToken, fingerprint };
76
+ }
77
+ };
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
 
3
- const pEvent = require("p-event");
3
+ const { once } = require("node:events");
4
4
 
5
5
  module.exports = async function serverClose(server) {
6
6
  server.close();
7
- await pEvent(server, "close");
7
+ await once(server, "close");
8
8
  };
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
 
3
- const pEvent = require("p-event");
3
+ const { once } = require("node:events");
4
4
  const debug = require("debug")("pairnet:server-listen");
5
5
 
6
6
  module.exports = async function serverListen(server, port, hostname) {
7
7
  server.listen(port, hostname);
8
- await pEvent(server, "listening");
8
+ await once(server, "listening");
9
9
  debug(`[createServer] Listening on ${hostname}:${port}...`);
10
10
  };
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+
3
+ module.exports = async function evaluateQuery(query, handler) {
4
+ try {
5
+ return {
6
+ success: true,
7
+ message: await handler(query)
8
+ };
9
+ } catch (error) {
10
+ if (error.code === "QUERY_ERROR_RESPONSE") {
11
+ return {
12
+ success: false,
13
+ message: error.messageData
14
+ };
15
+ } else {
16
+ throw error;
17
+ }
18
+ }
19
+ };