pairnet 0.1.1 → 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.
package/index.js CHANGED
@@ -4,28 +4,40 @@ const crypto = require("crypto");
4
4
  const tls = require("tls");
5
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, debug }) {
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
@@ -61,6 +67,7 @@ async function makeClientSocket({ hostname, port, options, onQuery, protocolConf
61
67
 
62
68
  return {
63
69
  peerFingerprint: getFingerprint(socket),
70
+ peerAddress: socket.remoteAddress,
64
71
  eventStream: api.eventStream,
65
72
  send: api.sender.sendEvent,
66
73
  query: api.sender.sendQuery,
@@ -75,18 +82,23 @@ function isStorageProvider(value) {
75
82
  }
76
83
 
77
84
  module.exports = {
85
+ NotSet: NotSet,
86
+ AlreadySet: AlreadySet,
78
87
  createQueryError: createQueryError,
79
88
  createMemoryStorage: createMemoryStorage,
80
89
  createFileStorage: createFileStorage,
81
90
  createPeer: async function createPeer(_options) {
82
91
  let debug = makeDebug("peer");
83
92
 
84
- let { storage, generateClientHello, onClientHello, onServerHello } = validateOptions(arguments, {
93
+ let { storage, generateClientHello, onClientHello, onServerHello } = validateOptions(arguments, [{
85
94
  storage: [ required, isStorageProvider ],
86
- generateClientHello: [ required, isFunction ],
87
- onClientHello: [ required, isFunction ],
88
- onServerHello: [ required, isFunction ]
89
- });
95
+ generateClientHello: [ isFunction ],
96
+ onClientHello: [ isFunction ],
97
+ onServerHello: [ isFunction ]
98
+ }, either([
99
+ allowExtraProperties({ generateClientHello: required, onServerHello: required }),
100
+ allowExtraProperties({ onClientHello: required })
101
+ ])]);
90
102
 
91
103
  // FIXME: Why do we still have this fragile 'background task' mechanism?
92
104
  let backgroundTasks = createBackgroundTaskManager();
@@ -130,104 +142,181 @@ module.exports = {
130
142
  }
131
143
  };
132
144
  }
133
-
134
- return {
135
- fingerprint: identity.fingerprint,
136
- generatePairingToken: async function () {
137
- let token = crypto.randomBytes(16).toString("hex");
138
- // TODO: Expiry
139
- await store.setValidPairingToken(token);
140
- debug(`[generatePairingToken] Generated pairing token ${token}`);
141
- return token;
142
- },
143
- pair: async function (_options) {
144
- let { hostname, port, fingerprint, pairingToken } = validateOptions(arguments, {
145
- hostname: [ required, isNonEmptyString ],
146
- port: [ required, isInteger ],
147
- fingerprint: [ required, isNonEmptyString ],
148
- pairingToken: [ required, isNonEmptyString ]
149
- });
150
145
 
151
- if (protocolConfiguration.isClient) {
152
- 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
+ }
153
154
 
154
- let socket = await makeClientSocket({
155
- debug, hostname, port, protocolConfiguration,
156
- // We don't care about anything other than pairing messages in this mode, which are in the form of a query reply
157
- onQuery: () => {}
158
- });
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
+ });
159
173
 
160
- debug(`[pair] Expected fingerprint: ${fingerprint}`);
161
- 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 ];
162
179
 
163
- if (socket.peerFingerprint !== fingerprint) {
164
- await Promise.all([
165
- socket.disconnect(),
166
- backgroundTasks.awaitCompletion()
167
- ]);
168
- throw new Error(`Fingerprint of server certificate does not match expected fingerprint`);
169
- }
170
-
171
- // We need to drive the read loop, even though we don't care about the events
172
- let reader = pipe([
173
- socket.eventStream,
174
- simpleSink(() => {
175
- debug("[pair] Ignored event message in pairing mode");
176
- })
177
- ]);
180
+ debug(`[pair] Expected fingerprint: ${fingerprint}`);
181
+ debug(`[pair] Actual fingerprint: ${socket.peerFingerprint}`);
178
182
 
183
+ if (socket.peerFingerprint !== fingerprint) {
179
184
  await Promise.all([
180
- // FIXME: Await completion of background tasks?
181
- reader.read(),
182
- (async () => {
183
- try {
184
- // Let any pairing errors throw
185
- await socket.query({ type: "PAIR", token: pairingToken });
186
- await store.trustPeer(fingerprint);
187
- } finally {
188
- await socket.disconnect();
189
- }
190
- })()
185
+ socket.disconnect(),
186
+ backgroundTasks.awaitCompletion()
191
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`);
192
239
  } else {
193
- throw new Error(`This peer is not configured as a client`);
240
+ return makeProtocolPeer(socket, backgroundTasks);
194
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
+ });
195
267
  },
196
- connect: async function (_options) {
197
- let { hostname, port, onQuery } = validateOptions(arguments, {
198
- hostname: [ required, isNonEmptyString ],
199
- port: [ required, isInteger ],
200
- onQuery: [ required, isFunction ],
268
+ generateRawPairingToken: makeRawPairingToken,
269
+ pair: function (_token) {
270
+ let [ token ] = validateArguments(arguments, {
271
+ token: [ required, isNonEmptyString ]
201
272
  });
202
273
 
203
- if (protocolConfiguration.isClient) {
204
- debug(`[connect] Initiating client connection to ${hostname}:${port}...`);
274
+ let tokenData = wrappedToken.decode(token);
205
275
 
206
- // connect with client certificate made from own identity, and validate server pubkey
207
- let socket = await makeClientSocket({
208
- debug, 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,
209
300
  onQuery: onQuery
210
301
  });
211
-
212
- let isPaired = await store.isPeerTrusted(socket.peerFingerprint);
213
-
214
- debug(`[connect] Peer fingerprint: ${socket.peerFingerprint}`);
215
- debug(`[connect] Is trusted: ${isPaired}`);
216
-
217
- if (isPaired !== true) {
218
- await socket.disconnect();
219
- throw new Error(`Not paired to specified peer`);
220
- } else {
221
- return makeProtocolPeer(socket, backgroundTasks);
222
- }
223
- } else {
224
- throw new Error(`This peer is not configured as a client`);
225
302
  }
226
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
+ },
227
316
  createServer: async function (_option) {
228
317
  let { hostname, port, onConnect, onDisconnect, onQuery } = validateOptions(arguments, {
229
- hostname: [ required, isNonEmptyString ],
230
- port: [ required, isInteger ],
318
+ hostname: [ isNonEmptyString ], /* undefined == listen on all public interfaces */
319
+ port: [ required, isPortNumber ],
231
320
  onConnect: [ required, isFunction ],
232
321
  onDisconnect: [ isFunction ],
233
322
  onQuery: [ required, isFunction ]
@@ -263,12 +352,13 @@ module.exports = {
263
352
  return matchValue(query.type, {
264
353
  PAIR: async () => {
265
354
  if (isPaired) {
355
+ // FIXME: Query error instead?
266
356
  throw new Error(`Already paired`);
267
357
  } else {
268
358
  let { token } = query;
269
359
 
270
360
  if (await store.isValidPairingToken(token) === true) {
271
- await store.trustPeer(clientFingerprint);
361
+ await store.trustPeer(clientFingerprint, { /* FIXME */ });
272
362
  await store.removeValidPairingToken(token);
273
363
  return true;
274
364
  } else {
@@ -282,6 +372,7 @@ module.exports = {
282
372
  return onQuery(clientFingerprint, query.m);
283
373
  } else {
284
374
  // FIXME: Force disconnect?
375
+ // FIXME: Query error instead?
285
376
  throw new Error(`Client unauthorized`);
286
377
  }
287
378
  },
@@ -305,7 +396,7 @@ module.exports = {
305
396
  await onConnect(protocolPeer);
306
397
 
307
398
  socket.on("close", () => {
308
- maybeCall(onDisconnect, protocolPeer);
399
+ onDisconnect?.(protocolPeer);
309
400
  });
310
401
  }
311
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
+ };
@@ -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
+ };