1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
/**
* Does the browser's real `uploadFile` produce frames a real node can open —
* and does it read back what that node actually answered?
*
* Drives **the real `MeshBayTransport` over the real `crypto.js`**. Only the DOM
* and the DataChannel are stand-ins; the msgpack encode, the HKDF, the AES-GCM
* and the whole upload loop are the shipped code.
*
* It exists because a source-reading test cannot show either half of MNP 2.0's
* upload. `test_transport_contracts` can see that `sealGroup` is called; only
* this can show that what comes out opens under `meshbay_common.groupbox` — and
* that the caller of `uploadFile` is told the name the *node* chose, which now
* arrives sealed and would otherwise be `undefined` with nothing to notice it.
*
* node upload_seal_probe.mjs <static-dir> <input.json>
*
* Two modes, because the node half runs in Python between them:
* "send" — run the upload, print every frame it emits, answer nothing
* "receive" — run it again, answering with acks Python built. Their
* `upload_id` is retargeted to this run's; it is outside the
* seal, so the ciphertext stays exactly the one Python produced.
*/
import fs from 'fs';
const STATIC = process.argv[2];
const input = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
for (const level of ['log', 'warn', 'error', 'info', 'debug']) {
console[level] = (...args) => process.stderr.write(args.join(' ') + '\n');
}
globalThis.window = globalThis;
globalThis.addEventListener = () => {};
globalThis.removeEventListener = () => {};
globalThis.location = { hash: '' };
globalThis.document = {
addEventListener() {}, removeEventListener() {}, visibilityState: 'visible',
};
new Function(fs.readFileSync(`${STATIC}/crypto.js`, 'utf8'))();
const transportSrc = fs.readFileSync(`${STATIC}/transport.js`, 'utf8');
new Function(transportSrc)();
// The msgpack codec is private to transport.js — pulled out the same way the
// groupbox parity harness pulls out sealGroup, so this probe encodes and
// decodes with the codec that is actually shipped rather than a second one.
const { msgpack_encode, msgpack_decode } =
new Function(transportSrc + '\nreturn { msgpack_encode, msgpack_decode };')();
const hex = (s) => Uint8Array.from(s.match(/../g).map((b) => parseInt(b, 16)));
const toHex = (u8) =>
Array.from(u8).map((b) => b.toString(16).padStart(2, '0')).join('');
// Just enough of a File: a name, a size, and slices that yield ArrayBuffers.
const bytes = hex(input.file.data);
const file = {
name: input.file.name,
size: bytes.length,
slice(a, b) {
const part = bytes.slice(a, b);
return { arrayBuffer: async () => part.buffer.slice(
part.byteOffset, part.byteOffset + part.byteLength) };
},
};
const tp = new window.MeshBayTransport('', 'token');
tp._connected = true;
tp._channel = { readyState: 'open', bufferedAmount: 0, send() {}, close() {} };
tp._pc = { close() {} };
tp._gekRaw = hex(input.gek);
tp._connectArgs = { groupId: input.group_id };
const frames = [];
let uploadId = null;
let answered = 0;
tp._send = (msg) => {
frames.push(toHex(msgpack_encode(msg)));
if (msg.upload_id) uploadId = msg.upload_id;
if (input.mode !== 'receive') {
// Nothing answers in this mode -- except the probe, which the client waits
// five seconds for. A node that predates it refuses the index, and that
// refusal is a plain error rather than a sealed ack, so the harness can
// produce it honestly. It is also the degradation path worth exercising.
if (msg.chunk_index === -1) {
// With `probe_ack`, answer it the way a node holding part of this file
// does; without, the way one that predates the probe does.
const reply = input.probe_ack
? Object.assign(msgpack_decode(hex(input.probe_ack)),
{ upload_id: uploadId })
: { type: 'error', upload_id: uploadId,
code: 'bad_chunk_index', detail: 'Unexpected chunk index' };
setImmediate(() => tp._dispatch(reply));
}
return;
}
// Answer as the node did, on the next turn of the loop so the send path
// finishes first — which is also how a real ack arrives.
//
// By position, not by `chunk_index`: the node answers every frame including
// the probe, whose index is -1, and the two lists are built from the same
// sequence of frames.
const ack = msgpack_decode(hex(input.acks[answered++]));
ack.upload_id = uploadId;
// Through the real `_dispatch`, so the routing under test — matching an
// ack to its uploader by `upload_id` — is the shipped one.
setImmediate(() => tp._dispatch(ack));
};
const out = { frames, mode: input.mode };
const done = tp.uploadFile(file, { chunkSize: input.chunk_size,
dir: input.dir, root: input.root });
if (input.mode === 'receive') {
done.then((stored) => { out.state = 'resolved'; out.stored = stored; })
.catch((e) => { out.state = 'rejected'; out.message = e.message; })
.finally(() => { out.upload_id = uploadId;
process.stdout.write(JSON.stringify(out)); });
} else {
// Nothing will answer, so let the send loop run itself out and report.
done.catch((e) => { out.state = 'rejected'; out.message = e.message; });
setTimeout(() => { out.upload_id = uploadId;
process.stdout.write(JSON.stringify(out)); }, 250);
}
|