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 +199 -108
- package/lib/create-query-error.js +8 -0
- package/lib/network/connect-multiple.js +74 -0
- package/lib/pairing-token/generate.js +7 -0
- package/lib/pairing-token/reduce-addresses.js +21 -0
- package/lib/pairing-token/wrapped.js +77 -0
- package/lib/protocol/evaluate-query.js +19 -0
- package/lib/protocol/index.js +161 -0
- package/lib/protocol/message-types.js +37 -0
- package/lib/reply-tracker.js +25 -10
- package/lib/sender.js +1 -1
- package/{storages → lib/storages}/memory.js +7 -0
- package/lib/store.js +12 -6
- package/notes.txt +3 -0
- package/package.json +23 -12
- package/test/index.js +54 -21
- package/test/wrapped-token.js +30 -0
- package/lib/maybe-call.js +0 -7
- package/lib/protocol.js +0 -191
- /package/{promisified → lib/promisified}/server-close.js +0 -0
- /package/{promisified → lib/promisified}/server-listen.js +0 -0
- /package/{storages → lib/storages}/file.js +0 -0
- /package/{storages → lib/storages}/symbols.js +0 -0
- /package/lib/{generate-keys.js → tls/generate-keys.js} +0 -0
- /package/lib/{get-fingerprint.js → tls/get-fingerprint.js} +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const pipe = require("@promistream/pipe");
|
|
4
|
+
const forkSequential = require("@promistream/fork-sequential");
|
|
5
|
+
const simpleQueue = require("@promistream/simple-queue");
|
|
6
|
+
const { fromDuplex } = require("@promistream/from-node-stream");
|
|
7
|
+
const encodeCBOR = require("@promistream/encode-cbor");
|
|
8
|
+
const decodeCBOR = require("@promistream/decode-cbor");
|
|
9
|
+
const map = require("@promistream/map");
|
|
10
|
+
const simpleSink = require("@promistream/simple-sink");
|
|
11
|
+
const NoValue = require("@promistream/no-value");
|
|
12
|
+
const createDebugStream = require("@promistream/debug");
|
|
13
|
+
|
|
14
|
+
const objid = require("@joepie91/objid");
|
|
15
|
+
|
|
16
|
+
const createSender = require("../sender");
|
|
17
|
+
const createReplyTracker = require("../reply-tracker");
|
|
18
|
+
const evaluateQuery = require("./evaluate-query");
|
|
19
|
+
const { isNegotiationMessage, isReplyMessage, isNegotiationQuery, isQuery, isEvent, isSuccessReply, isErrorReply } = require("./message-types");
|
|
20
|
+
|
|
21
|
+
module.exports = async function handleProtocol(socket, { debug, onQuery, protocolConfiguration, role } = {}) {
|
|
22
|
+
debug(`Attaching semantic protocol handler (role = ${role})`);
|
|
23
|
+
|
|
24
|
+
async function startNegotiation() {
|
|
25
|
+
let helloResponse = await sender.sendNegotiationQuery(await protocolConfiguration.generateClientHello());
|
|
26
|
+
let helloDecision = await protocolConfiguration.onServerHello(helloResponse);
|
|
27
|
+
|
|
28
|
+
if (helloDecision === false) {
|
|
29
|
+
// Connection rejected
|
|
30
|
+
negotiationStream.abort();
|
|
31
|
+
} else {
|
|
32
|
+
debug(`[readLoop] Negotiation completed on client side`);
|
|
33
|
+
negotiationStream.done();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let writeQueue = simpleQueue([], { autoEnd: false });
|
|
38
|
+
let replyTracker = createReplyTracker();
|
|
39
|
+
let sender = createSender(replyTracker, (message) => writeQueue.push(message));
|
|
40
|
+
let negotiationCompleted = false;
|
|
41
|
+
|
|
42
|
+
/* TODO: Allow passing streams into the `writeQueue` for sending streams of messages; this currently requires normalizing every non-stream value to a single-value stream and then using combine-sequential, but that seems like it could be prohibitively expensive performance-wise due to creating and ending a whole stream for every individual value in normal use. Perhaps we need a dedicated stream package for normalizing streams and non-stream values into a stream of streams, where subsequent non-stream values are buffered up into a single stream to avoid this performance penalty? Name it something like `mix-stream-values`? */
|
|
43
|
+
let [ negotiationStream, appReadStream ] = pipe([
|
|
44
|
+
writeQueue.stream,
|
|
45
|
+
map((message) => {
|
|
46
|
+
debug("Sending message:", message);
|
|
47
|
+
|
|
48
|
+
if (negotiationCompleted !== true && !isNegotiationMessage(message)) {
|
|
49
|
+
debug(`[handleProtocol] Prevented message send; not allowed type '${message.t}' during negotiation`);
|
|
50
|
+
throw new Error(`Cannot send messages in negotiation phase`);
|
|
51
|
+
} else {
|
|
52
|
+
return message;
|
|
53
|
+
}
|
|
54
|
+
}),
|
|
55
|
+
// createDebugStream(`pairnet:protocol:send-pipeline ${objid(socket)}`),
|
|
56
|
+
encodeCBOR(),
|
|
57
|
+
fromDuplex(socket),
|
|
58
|
+
// createDebugStream(`pairnet:protocol:receive-pipeline (pre-cbor) ${objid(socket)}`),
|
|
59
|
+
decodeCBOR(),
|
|
60
|
+
// createDebugStream(`pairnet:protocol:receive-pipeline ${objid(socket)}`),
|
|
61
|
+
map(async (message) => {
|
|
62
|
+
if (isReplyMessage(message)) {
|
|
63
|
+
try {
|
|
64
|
+
if (isSuccessReply(message)) {
|
|
65
|
+
replyTracker.registerReply(message.t, message.i).resolve(message.m);
|
|
66
|
+
} else if (isErrorReply(message)) {
|
|
67
|
+
let error = new Error(`Received error response from peer`);
|
|
68
|
+
error.code = "PEER_ERROR";
|
|
69
|
+
error.message = message.m;
|
|
70
|
+
|
|
71
|
+
replyTracker.registerReply(message.t, message.i).reject(error);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.code === "UNKNOWN_QUERY_ID") {
|
|
75
|
+
debug(`[readLoop] Received reply to unknown query ID '${message.i.toString("hex")}'`);
|
|
76
|
+
} else {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return NoValue;
|
|
82
|
+
} else {
|
|
83
|
+
return message;
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
86
|
+
forkSequential(2),
|
|
87
|
+
]).read();
|
|
88
|
+
|
|
89
|
+
// We read messages for as long as negotiation has not been completed yet
|
|
90
|
+
// TODO[security]: verify that we don't allow server/client confusion anywhere
|
|
91
|
+
let negotiationPipeline = pipe([
|
|
92
|
+
negotiationStream,
|
|
93
|
+
map(async (message) => {
|
|
94
|
+
debug("Received message (negotiation phase):", message);
|
|
95
|
+
|
|
96
|
+
if (!isNegotiationMessage(message)) {
|
|
97
|
+
debug(`[readLoop] Ignored received message; not allowed type '${message.t}' during negotiation`);
|
|
98
|
+
} else if (isNegotiationQuery(message)) {
|
|
99
|
+
if (role === "server") {
|
|
100
|
+
let result = await evaluateQuery(message.m, protocolConfiguration.onClientHello);
|
|
101
|
+
|
|
102
|
+
if (result.success) {
|
|
103
|
+
await sender.sendNegotiationReply(message.i, result.message);
|
|
104
|
+
|
|
105
|
+
debug(`[readLoop] Negotiation completed on server side`);
|
|
106
|
+
negotiationStream.done();
|
|
107
|
+
} else {
|
|
108
|
+
await sender.sendNegotiationReplyError(message.i, result.message);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
debug(`[readLoop] Processed negotiation query ID '${message.i.toString("hex")}'`);
|
|
112
|
+
} else {
|
|
113
|
+
await sender.sendNegotiationReplyError(message.i, `Only the client may send a negotiation query`);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
debug(`[readLoop] Received message with unknown type '${message.t}'`);
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
simpleSink(() => { /* discard */ })
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
await Promise.all([
|
|
123
|
+
negotiationPipeline.read(),
|
|
124
|
+
(role === "client") ? startNegotiation() : null
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
// Unlock the sender
|
|
128
|
+
negotiationCompleted = true;
|
|
129
|
+
debug("Negotiation completed; switching to regular messaging stream...");
|
|
130
|
+
|
|
131
|
+
let eventStream = pipe([
|
|
132
|
+
appReadStream,
|
|
133
|
+
map(async (message) => {
|
|
134
|
+
debug("Received message (operating phase):", message);
|
|
135
|
+
|
|
136
|
+
if (isEvent(message)) {
|
|
137
|
+
debug(`[readLoop] Passed through event message as-is`);
|
|
138
|
+
return message.m; // Unpack actual message content from event message
|
|
139
|
+
} else {
|
|
140
|
+
if (isQuery(message)) {
|
|
141
|
+
let result = await evaluateQuery(message.m, onQuery);
|
|
142
|
+
|
|
143
|
+
if (result.success) { await sender.sendReply(message.i, result.message); }
|
|
144
|
+
else { await sender.sendReplyError(message.i, result.message); }
|
|
145
|
+
|
|
146
|
+
debug(`[readLoop] Processed normal query ID '${message.i.toString("hex")}'`);
|
|
147
|
+
} else {
|
|
148
|
+
debug(`[readLoop] Received message with unknown type '${message.t}'`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return NoValue; /* We only want to emit event messages, not anything we've handled internally already */
|
|
152
|
+
}
|
|
153
|
+
}),
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
eventStream: eventStream,
|
|
158
|
+
sender: sender,
|
|
159
|
+
disconnect: () => writeQueue.end()
|
|
160
|
+
};
|
|
161
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
let NORMAL_QUERY_TYPE = "q";
|
|
4
|
+
let NEGOTIATION_QUERY_TYPE = "nq";
|
|
5
|
+
let EVENT_TYPE = "e";
|
|
6
|
+
// negotiation query, negotiation reply, negotiation reply error
|
|
7
|
+
let NEGOTIATION_TYPES = new Set([ "nq", "n", "N" ]);
|
|
8
|
+
// reply, reply error, negotiation reply, negotiation reply error
|
|
9
|
+
let REPLY_TYPES = new Set([ "r", "R", "n", "N" ]);
|
|
10
|
+
let SUCCESS_REPLIES = new Set([ "r", "n" ]);
|
|
11
|
+
let ERROR_REPLIES = new Set([ "R", "N" ]);
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
NORMAL_QUERY_TYPE, NEGOTIATION_QUERY_TYPE, EVENT_TYPE,
|
|
15
|
+
NEGOTIATION_TYPES, REPLY_TYPES, SUCCESS_REPLIES, ERROR_REPLIES,
|
|
16
|
+
isNegotiationMessage(message) {
|
|
17
|
+
return NEGOTIATION_TYPES.has(message.t);
|
|
18
|
+
},
|
|
19
|
+
isNegotiationQuery(message) {
|
|
20
|
+
return message.t === NEGOTIATION_QUERY_TYPE;
|
|
21
|
+
},
|
|
22
|
+
isReplyMessage(message) {
|
|
23
|
+
return REPLY_TYPES.has(message.t);
|
|
24
|
+
},
|
|
25
|
+
isSuccessReply(message) {
|
|
26
|
+
return SUCCESS_REPLIES.has(message.t);
|
|
27
|
+
},
|
|
28
|
+
isErrorReply(message) {
|
|
29
|
+
return ERROR_REPLIES.has(message.t);
|
|
30
|
+
},
|
|
31
|
+
isQuery(message) {
|
|
32
|
+
return message.t === NORMAL_QUERY_TYPE;
|
|
33
|
+
},
|
|
34
|
+
isEvent(message) {
|
|
35
|
+
return message.t === EVENT_TYPE;
|
|
36
|
+
}
|
|
37
|
+
};
|
package/lib/reply-tracker.js
CHANGED
|
@@ -4,38 +4,53 @@ const pDefer = require("p-defer");
|
|
|
4
4
|
const crypto = require("crypto");
|
|
5
5
|
const debug = require("debug")("pairnet:reply-tracker");
|
|
6
6
|
|
|
7
|
+
function validateReplyType(queryType, replyType) {
|
|
8
|
+
/* We check that either both the query and reply are the normal kind, *or* both are the negotiation type. This is an extra defense against unforeseen (potentially exploitable) protocol confusion bugs. */
|
|
9
|
+
if (queryType === "nq") {
|
|
10
|
+
return replyType === "n" || replyType === "N";
|
|
11
|
+
} else if (queryType === "q") {
|
|
12
|
+
return replyType === "r" || replyType === "R";
|
|
13
|
+
} else {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
module.exports = function createReplyTracker() {
|
|
8
19
|
let map = new Map();
|
|
9
20
|
|
|
10
21
|
return {
|
|
11
22
|
_map: map,
|
|
12
|
-
registerQuery: function () {
|
|
23
|
+
registerQuery: function (type) {
|
|
13
24
|
let defer = pDefer();
|
|
14
|
-
let id = crypto.randomBytes(6);
|
|
25
|
+
let id = crypto.randomBytes(6); // TODO: Should we use an incrementing ID instead?
|
|
15
26
|
let hexID = id.toString("hex");
|
|
16
27
|
|
|
17
28
|
// Store under string key due to Map not behaving like we want for value-identical Buffers
|
|
18
29
|
debug(`Storing query ID '${hexID}'`);
|
|
19
|
-
map.set(hexID, defer);
|
|
30
|
+
map.set(hexID, { ... defer, type });
|
|
20
31
|
|
|
21
32
|
return {
|
|
22
33
|
id: id,
|
|
23
34
|
promise: defer.promise
|
|
24
35
|
};
|
|
25
36
|
},
|
|
26
|
-
registerReply: function (id) {
|
|
37
|
+
registerReply: function (type, id) {
|
|
27
38
|
let hexID = id.toString("hex");
|
|
28
39
|
let defer = map.get(hexID);
|
|
29
40
|
|
|
30
|
-
if (defer
|
|
31
|
-
debug(`Registering reply for query ID '${hexID}'`);
|
|
32
|
-
map.delete(hexID);
|
|
33
|
-
return defer;
|
|
34
|
-
} else {
|
|
41
|
+
if (defer == null) {
|
|
35
42
|
debug(`Unknown query ID '${hexID}'`);
|
|
36
|
-
let error = Error(`Unknown ID '${hexID}'`);
|
|
43
|
+
let error = new Error(`Unknown ID '${hexID}'`);
|
|
37
44
|
error.code = "UNKNOWN_QUERY_ID";
|
|
38
45
|
throw error;
|
|
46
|
+
} else if (!validateReplyType(defer.type, type)) {
|
|
47
|
+
let error = new Error(`Incorrect reply type '${type}' for query of type '${defer.type}'`);
|
|
48
|
+
error.code = "INVALID_REPLY_TYPE";
|
|
49
|
+
throw error;
|
|
50
|
+
} else {
|
|
51
|
+
debug(`Registering reply for query ID '${hexID}'`);
|
|
52
|
+
map.delete(hexID);
|
|
53
|
+
return defer;
|
|
39
54
|
}
|
|
40
55
|
}
|
|
41
56
|
};
|
package/lib/sender.js
CHANGED
|
@@ -12,7 +12,7 @@ module.exports = function createSender(replyTracker, send) {
|
|
|
12
12
|
});
|
|
13
13
|
},
|
|
14
14
|
sendQuery: function sendQuery(message, type = "q") {
|
|
15
|
-
let { promise, id } = replyTracker.registerQuery();
|
|
15
|
+
let { promise, id } = replyTracker.registerQuery(type);
|
|
16
16
|
|
|
17
17
|
send({
|
|
18
18
|
t: type,
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const util = require("node:util");
|
|
4
|
+
const makeDebug = require("debug-instanced")("pairnet:storage:memory");
|
|
5
|
+
|
|
3
6
|
const { NotSet, AlreadySet } = require("./symbols");
|
|
4
7
|
|
|
5
8
|
module.exports = async function createMemoryStorage() {
|
|
9
|
+
let debug = makeDebug();
|
|
6
10
|
let storage = new Map();
|
|
7
11
|
|
|
8
12
|
return {
|
|
9
13
|
_pairnetStorage: true,
|
|
10
14
|
set: async function (key, value) {
|
|
15
|
+
debug(`setting: ${key} = ${util.inspect(value, { compact: true })}`);
|
|
11
16
|
if (storage.has(key)) {
|
|
12
17
|
return AlreadySet;
|
|
13
18
|
} else {
|
|
@@ -15,6 +20,7 @@ module.exports = async function createMemoryStorage() {
|
|
|
15
20
|
}
|
|
16
21
|
},
|
|
17
22
|
get: async function (key) {
|
|
23
|
+
debug(`getting: ${key}`);
|
|
18
24
|
if (storage.has(key)) {
|
|
19
25
|
return storage.get(key);
|
|
20
26
|
} else {
|
|
@@ -22,6 +28,7 @@ module.exports = async function createMemoryStorage() {
|
|
|
22
28
|
}
|
|
23
29
|
},
|
|
24
30
|
delete: async function (key) {
|
|
31
|
+
debug(`deleting: ${key}`);
|
|
25
32
|
return storage.delete(key);
|
|
26
33
|
}
|
|
27
34
|
};
|
package/lib/store.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { NotSet } = require("./storages/symbols");
|
|
4
|
+
|
|
3
5
|
module.exports = function (storage) {
|
|
6
|
+
// TODO: Have Buffer assertions for tokens to catch potential implementation bugs?
|
|
4
7
|
return {
|
|
5
8
|
getIdentity: async function () {
|
|
6
9
|
return storage.get("pairnet_identity");
|
|
@@ -9,22 +12,25 @@ module.exports = function (storage) {
|
|
|
9
12
|
return storage.set("pairnet_identity", identity);
|
|
10
13
|
},
|
|
11
14
|
isValidPairingToken: async function (token) {
|
|
12
|
-
return (await storage.get(`pairnet_token_${token}`) === true);
|
|
15
|
+
return (await storage.get(`pairnet_token_${token.toString("hex")}`) === true);
|
|
13
16
|
},
|
|
14
17
|
setValidPairingToken: async function (token) {
|
|
15
|
-
return storage.set(`pairnet_token_${token}`, true);
|
|
18
|
+
return storage.set(`pairnet_token_${token.toString("hex")}`, true);
|
|
16
19
|
},
|
|
17
20
|
removeValidPairingToken: async function (token) {
|
|
18
|
-
return storage.delete(`pairnet_token_${token}`);
|
|
21
|
+
return storage.delete(`pairnet_token_${token.toString("hex")}`);
|
|
19
22
|
},
|
|
20
|
-
trustPeer: async function (fingerprint) {
|
|
21
|
-
return storage.set(`pairnet_peer_${fingerprint}`,
|
|
23
|
+
trustPeer: async function (fingerprint, data) {
|
|
24
|
+
return storage.set(`pairnet_peer_${fingerprint}`, data ?? {});
|
|
22
25
|
},
|
|
23
26
|
untrustPeer: async function (fingerprint) {
|
|
24
27
|
return storage.delete(`pairnet_peer_${fingerprint}`);
|
|
25
28
|
},
|
|
26
29
|
isPeerTrusted: async function (fingerprint) {
|
|
27
|
-
return (await storage.get(`pairnet_peer_${fingerprint}`)
|
|
30
|
+
return (await storage.get(`pairnet_peer_${fingerprint}`) !== NotSet);
|
|
31
|
+
},
|
|
32
|
+
getPeerData: async function (fingerprint) {
|
|
33
|
+
return storage.get(`pairnet_peer_${fingerprint}`);
|
|
28
34
|
}
|
|
29
35
|
};
|
|
30
36
|
};
|
package/notes.txt
CHANGED
|
@@ -4,3 +4,6 @@
|
|
|
4
4
|
- Client sends clientHello (custom) including protocol negotiation information
|
|
5
5
|
- Server either breaks connection (optionally with an error) or responds with serverHello (custom) including protocol negotiation information
|
|
6
6
|
- At this point, client must either already be recognized by the server, or it will get locked into pairing-only mode
|
|
7
|
+
|
|
8
|
+
TODO:
|
|
9
|
+
- Implement re-pairing with wrapped tokens for eg. when a system's IP has changed
|
package/package.json
CHANGED
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pairnet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "node --test"
|
|
8
|
-
},
|
|
9
6
|
"keywords": [],
|
|
10
7
|
"author": "Sven Slootweg <admin@cryto.net>",
|
|
11
8
|
"license": "WTFPL",
|
|
12
9
|
"dependencies": {
|
|
13
10
|
"@joepie91/objid": "^1.0.1",
|
|
14
|
-
"@promistream/decode-cbor": "^0.1.
|
|
15
|
-
"@promistream/encode-cbor": "^0.1.
|
|
16
|
-
"@promistream/
|
|
17
|
-
"@promistream/from-node-stream": "^0.1.
|
|
11
|
+
"@promistream/decode-cbor": "^0.1.1",
|
|
12
|
+
"@promistream/encode-cbor": "^0.1.1",
|
|
13
|
+
"@promistream/fork-sequential": "^0.1.1",
|
|
14
|
+
"@promistream/from-node-stream": "^0.1.12",
|
|
18
15
|
"@promistream/map": "^0.1.10",
|
|
16
|
+
"@promistream/no-value": "^1.0.0",
|
|
19
17
|
"@promistream/pipe": "^0.1.8",
|
|
20
18
|
"@promistream/simple-queue": "^0.1.7",
|
|
21
19
|
"@promistream/simple-sink": "^0.3.5",
|
|
20
|
+
"@validatem/allow-extra-properties": "^0.1.0",
|
|
21
|
+
"@validatem/array-of": "^0.1.3",
|
|
22
22
|
"@validatem/core": "^0.6.3",
|
|
23
|
+
"@validatem/default-to": "^1.0.0",
|
|
24
|
+
"@validatem/either": "^0.1.10",
|
|
23
25
|
"@validatem/error": "^1.1.0",
|
|
26
|
+
"@validatem/is-boolean": "^0.1.2",
|
|
27
|
+
"@validatem/is-buffer": "^0.1.1",
|
|
24
28
|
"@validatem/is-function": "^0.1.0",
|
|
25
|
-
"@validatem/is-
|
|
26
|
-
"@validatem/is-
|
|
29
|
+
"@validatem/is-non-empty-string": "^0.1.1",
|
|
30
|
+
"@validatem/is-port-number": "^1.0.0",
|
|
31
|
+
"@validatem/require-either": "^0.1.0",
|
|
27
32
|
"@validatem/required": "^0.1.1",
|
|
28
33
|
"cbor": "^10.0.12",
|
|
29
34
|
"debug": "^4.4.3",
|
|
30
35
|
"debug-instanced": "^1.0.0",
|
|
36
|
+
"get-ips": "^1.0.1",
|
|
37
|
+
"is-local-ip": "^1.1.0",
|
|
31
38
|
"match-value": "^1.1.0",
|
|
32
39
|
"nanoid": "^3.3.18",
|
|
33
40
|
"p-defer": "^3.0.0",
|
|
@@ -36,6 +43,10 @@
|
|
|
36
43
|
"devDependencies": {
|
|
37
44
|
"@joepie91/eslint-config": "^2.0.3",
|
|
38
45
|
"@promistream/debug": "^0.1.2",
|
|
39
|
-
"
|
|
46
|
+
"@promistream/from-iterable": "^0.1.1",
|
|
47
|
+
"eslint": "^10.9.1"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"test": "node --test"
|
|
40
51
|
}
|
|
41
|
-
}
|
|
52
|
+
}
|
package/test/index.js
CHANGED
|
@@ -22,15 +22,6 @@ async function withYeetedStream(stream, callback) {
|
|
|
22
22
|
]);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
async function expectFailure(errorPattern, callback) {
|
|
26
|
-
try {
|
|
27
|
-
await assert.rejects(callback, errorPattern);
|
|
28
|
-
return true;
|
|
29
|
-
} catch (error) {
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
25
|
async function makePeer(overrides) {
|
|
35
26
|
return pairnet.createPeer({
|
|
36
27
|
storage: await pairnet.createMemoryStorage(),
|
|
@@ -69,23 +60,24 @@ test("full procedure", async (t) => {
|
|
|
69
60
|
}
|
|
70
61
|
});
|
|
71
62
|
|
|
72
|
-
let token = await peer1.generatePairingToken();
|
|
63
|
+
// let token = await peer1.generatePairingToken(4005);
|
|
64
|
+
let rawToken = await peer1.generateRawPairingToken();
|
|
73
65
|
|
|
74
|
-
await t.test("fails on wrong fingerprint", async (
|
|
75
|
-
|
|
76
|
-
await peer2.
|
|
77
|
-
});
|
|
66
|
+
await t.test("fails on wrong fingerprint", { timeout: 500 }, async () => {
|
|
67
|
+
await assert.rejects(async () => {
|
|
68
|
+
await peer2.pairRaw({ hostname: "localhost", port: 4005, fingerprint: "wrong", pairingToken: rawToken });
|
|
69
|
+
}, { message: "Fingerprint of server certificate does not match expected fingerprint" });
|
|
78
70
|
});
|
|
79
71
|
|
|
80
|
-
await t.test("fails on wrong token", async (
|
|
81
|
-
|
|
82
|
-
await peer2.
|
|
83
|
-
});
|
|
72
|
+
await t.test("fails on wrong token", { timeout: 500 }, async () => {
|
|
73
|
+
await assert.rejects(async () => {
|
|
74
|
+
await peer2.pairRaw({ hostname: "localhost", port: 4005, fingerprint: peer1.fingerprint, pairingToken: Buffer.from("wrong") });
|
|
75
|
+
}, { message: "Invalid pairing token" });
|
|
84
76
|
});
|
|
85
77
|
|
|
86
|
-
await t.test("happy path", async (
|
|
87
|
-
await peer2.
|
|
88
|
-
let peer = await peer2.
|
|
78
|
+
await t.test("happy path", { timeout: 500 }, async () => {
|
|
79
|
+
await peer2.pairRaw({ hostname: "localhost", port: 4005, fingerprint: peer1.fingerprint, pairingToken: rawToken });
|
|
80
|
+
let peer = await peer2.connectRaw({ hostname: "localhost", port: 4005, onQuery: noop });
|
|
89
81
|
|
|
90
82
|
await withYeetedStream(peer.eventStream, async () => {
|
|
91
83
|
let response = await peer.query({ number: 21 });
|
|
@@ -97,3 +89,44 @@ test("full procedure", async (t) => {
|
|
|
97
89
|
|
|
98
90
|
await server.stop();
|
|
99
91
|
});
|
|
92
|
+
|
|
93
|
+
test("full procedure with wrapped token", async (t) => {
|
|
94
|
+
let peer1 = await makePeer();
|
|
95
|
+
let peer2 = await makePeer();
|
|
96
|
+
|
|
97
|
+
let server = await peer1.createServer({
|
|
98
|
+
hostname: "localhost", port: 4005,
|
|
99
|
+
onQuery: (fingerprint, query) => ({ number: query.number * 2, hasFingerprint: (fingerprint != null) }),
|
|
100
|
+
onConnect: async (peer) => {
|
|
101
|
+
yeetStream(peer.eventStream);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let fingerprint;
|
|
106
|
+
|
|
107
|
+
await t.test("pair", { timeout: 500 }, async () => {
|
|
108
|
+
let token = await peer1.generatePairingToken(4005);
|
|
109
|
+
fingerprint = await peer2.pair(token);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await t.test("connect", { timeout: 500 }, async () => {
|
|
113
|
+
let peer = await peer2.connect({ fingerprint: fingerprint, onQuery: noop });
|
|
114
|
+
|
|
115
|
+
await withYeetedStream(peer.eventStream, async () => {
|
|
116
|
+
let response = await peer.query({ number: 21 });
|
|
117
|
+
assert.deepStrictEqual(response, { number: 42, hasFingerprint: true });
|
|
118
|
+
await peer.disconnect();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await server.stop();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("compact token", async () => {
|
|
126
|
+
let fakePeer = await makePeer();
|
|
127
|
+
let normalToken = await fakePeer.generatePairingToken(4005);
|
|
128
|
+
let compactToken = await fakePeer.generatePairingToken(4005, { compact: true });
|
|
129
|
+
|
|
130
|
+
console.log(`compact token ${compactToken.length} characters, vs. normal token ${normalToken.length}`);
|
|
131
|
+
assert(compactToken.length < normalToken.length);
|
|
132
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const test = require("node:test");
|
|
4
|
+
const assert = require("node:assert");
|
|
5
|
+
|
|
6
|
+
const getIPs = require("get-ips");
|
|
7
|
+
const wrappedToken = require("../lib/pairing-token/wrapped");
|
|
8
|
+
const makeRawPairingToken = require("../lib/pairing-token/generate");
|
|
9
|
+
|
|
10
|
+
test("roundtrip", async () => {
|
|
11
|
+
let inputData = {
|
|
12
|
+
addresses: (await getIPs()).all,
|
|
13
|
+
port: 5892,
|
|
14
|
+
rawToken: await makeRawPairingToken(),
|
|
15
|
+
fingerprint: "9D:FD:F7:FB:97:21:AD:36:35:4E:52:6B:3E:C9:BA:11:E5:BA:D6:B4:14:D6:85:D5:D3:23:5C:93:41:A6:9A:73",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let encoded = wrappedToken.encode(inputData);
|
|
19
|
+
let decoded = wrappedToken.decode(encoded);
|
|
20
|
+
|
|
21
|
+
/* We lose the address type in the encoding, but this is acceptable */
|
|
22
|
+
let inputToCompare = {
|
|
23
|
+
... inputData,
|
|
24
|
+
addresses: inputData.addresses.map(({ family, address }) => ({ family, address }))
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
console.log(`naive JSON size: ${JSON.stringify(inputToCompare).length}, actual token size: ${encoded.length}`);
|
|
28
|
+
|
|
29
|
+
assert.deepStrictEqual(inputToCompare, decoded);
|
|
30
|
+
});
|