javascript-solid-server 0.0.113 → 0.0.115
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/.claude/settings.local.json +5 -1
- package/package.json +1 -1
- package/src/webrtc/index.js +119 -31
- package/test/webrtc.test.js +16 -2
|
@@ -328,7 +328,11 @@
|
|
|
328
328
|
"Bash(mongosh --eval \"db.runCommand\\({ ping: 1 }\\)\" 2>&1 | head -5)",
|
|
329
329
|
"Bash(which jss && jss --version 2>&1)",
|
|
330
330
|
"Bash(jss start --help 2>&1 | grep -i mongo)",
|
|
331
|
-
"Bash(grep -A5 '\"\"files\"\"' package.json)"
|
|
331
|
+
"Bash(grep -A5 '\"\"files\"\"' package.json)",
|
|
332
|
+
"Bash(mkdir -p /tmp/wt-check)",
|
|
333
|
+
"Bash(npm init:*)",
|
|
334
|
+
"WebFetch(domain:webtorrent.io)",
|
|
335
|
+
"Bash(TORRENT=/home/melvin/.claude/projects/-home-melvin-remote-github-com-JavaScriptSolidServer-JavaScriptSolidServer/a05da419-92b7-4056-93b8-e97b2035d4ae/tool-results/webfetch-1774004425803-fce7mx.bin npx -y parse-torrent $TORRENT)"
|
|
332
336
|
]
|
|
333
337
|
}
|
|
334
338
|
}
|
package/package.json
CHANGED
package/src/webrtc/index.js
CHANGED
|
@@ -45,6 +45,10 @@ const MAX_OFFERS_PER_ANNOUNCE = 10;
|
|
|
45
45
|
const MAX_RESOURCES_PER_PEER = 50;
|
|
46
46
|
const RESOURCE_HASH_RE = /^[a-fA-F0-9]{8,128}$/;
|
|
47
47
|
|
|
48
|
+
// WebTorrent tracker uses 20-byte binary strings for info_hash and peer_id
|
|
49
|
+
function bin2hex(s) { return Buffer.from(s, 'binary').toString('hex'); }
|
|
50
|
+
function hex2bin(s) { return Buffer.from(s, 'hex').toString('binary'); }
|
|
51
|
+
|
|
48
52
|
/**
|
|
49
53
|
* Register WebRTC signaling routes on Fastify instance
|
|
50
54
|
*
|
|
@@ -215,46 +219,120 @@ export async function webrtcPlugin(fastify, options = {}) {
|
|
|
215
219
|
removePeerFromResource(peerId, hash);
|
|
216
220
|
}
|
|
217
221
|
|
|
222
|
+
// --- WebTorrent tracker protocol handler ---
|
|
223
|
+
|
|
224
|
+
function handleWebtorrentAnnounce(socket, peerId, msg) {
|
|
225
|
+
const infoHash = typeof msg.info_hash === 'string' && msg.info_hash.length === 20
|
|
226
|
+
? bin2hex(msg.info_hash) : msg.info_hash;
|
|
227
|
+
const msgPeerId = typeof msg.peer_id === 'string' && msg.peer_id.length === 20
|
|
228
|
+
? bin2hex(msg.peer_id) : msg.peer_id;
|
|
229
|
+
|
|
230
|
+
if (!infoHash || typeof infoHash !== 'string') {
|
|
231
|
+
socket.send(JSON.stringify({ action: 'announce', 'failure reason': 'invalid info_hash' }));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Use info_hash as resource hash for our swarm infrastructure
|
|
236
|
+
const group = getResourcePeers(infoHash);
|
|
237
|
+
addPeerToResource(peerId, socket, infoHash);
|
|
238
|
+
socket._wtPeerId = msgPeerId || peerId;
|
|
239
|
+
|
|
240
|
+
// Handle answer relay
|
|
241
|
+
if (msg.answer && msg.to_peer_id) {
|
|
242
|
+
const toPeerId = typeof msg.to_peer_id === 'string' && msg.to_peer_id.length === 20
|
|
243
|
+
? bin2hex(msg.to_peer_id) : msg.to_peer_id;
|
|
244
|
+
|
|
245
|
+
// Find the target peer by their WebTorrent peer_id
|
|
246
|
+
for (const [id, peerSocket] of group) {
|
|
247
|
+
if (peerSocket._wtPeerId === toPeerId && peerSocket.readyState === 1) {
|
|
248
|
+
try {
|
|
249
|
+
peerSocket.send(JSON.stringify({
|
|
250
|
+
action: 'announce',
|
|
251
|
+
answer: msg.answer,
|
|
252
|
+
offer_id: msg.offer_id,
|
|
253
|
+
peer_id: typeof msg.peer_id === 'string' && msg.peer_id.length === 20
|
|
254
|
+
? msg.peer_id : hex2bin(msgPeerId || peerId),
|
|
255
|
+
info_hash: typeof msg.info_hash === 'string' && msg.info_hash.length === 20
|
|
256
|
+
? msg.info_hash : hex2bin(infoHash)
|
|
257
|
+
}));
|
|
258
|
+
} catch { /* peer gone */ }
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return; // Don't send response for answers
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Relay offers to existing peers in the group
|
|
266
|
+
if (Array.isArray(msg.offers) && msg.offers.length > 0) {
|
|
267
|
+
const existingPeers = [...group.entries()].filter(([id]) => id !== peerId);
|
|
268
|
+
for (let i = 0; i < msg.offers.length && i < existingPeers.length; i++) {
|
|
269
|
+
const [, targetSocket] = existingPeers[i];
|
|
270
|
+
if (targetSocket.readyState !== 1) continue;
|
|
271
|
+
try {
|
|
272
|
+
targetSocket.send(JSON.stringify({
|
|
273
|
+
action: 'announce',
|
|
274
|
+
offer: msg.offers[i].offer,
|
|
275
|
+
offer_id: msg.offers[i].offer_id,
|
|
276
|
+
peer_id: typeof msg.peer_id === 'string' && msg.peer_id.length === 20
|
|
277
|
+
? msg.peer_id : hex2bin(msgPeerId || peerId),
|
|
278
|
+
info_hash: typeof msg.info_hash === 'string' && msg.info_hash.length === 20
|
|
279
|
+
? msg.info_hash : hex2bin(infoHash)
|
|
280
|
+
}));
|
|
281
|
+
} catch { /* peer gone */ }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Send announce response
|
|
286
|
+
const response = {
|
|
287
|
+
action: 'announce',
|
|
288
|
+
info_hash: typeof msg.info_hash === 'string' && msg.info_hash.length === 20
|
|
289
|
+
? msg.info_hash : hex2bin(infoHash),
|
|
290
|
+
complete: 0,
|
|
291
|
+
incomplete: group.size,
|
|
292
|
+
interval: 120
|
|
293
|
+
};
|
|
294
|
+
socket.send(JSON.stringify(response));
|
|
295
|
+
}
|
|
296
|
+
|
|
218
297
|
// --- WebSocket handler ---
|
|
219
298
|
|
|
220
299
|
fastify.get(path, { websocket: true }, async (connection, request) => {
|
|
221
300
|
const socket = connection.socket;
|
|
222
301
|
|
|
223
|
-
// Authenticate the connection
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return;
|
|
302
|
+
// Authenticate the connection (support query param for browser WebSocket which can't set headers)
|
|
303
|
+
// Auth is optional — unauthenticated clients can use the tracker protocol but not identity-based signaling
|
|
304
|
+
const queryToken = request.query?.token;
|
|
305
|
+
if (queryToken && !request.headers.authorization) {
|
|
306
|
+
request.headers.authorization = `Bearer ${queryToken}`;
|
|
229
307
|
}
|
|
308
|
+
const { webId } = await getWebIdFromRequestAsync(request);
|
|
230
309
|
|
|
231
|
-
// Assign a stable peer ID for content-addressed mode
|
|
310
|
+
// Assign a stable peer ID for content-addressed/tracker mode
|
|
232
311
|
const peerId = String(nextPeerId++);
|
|
233
312
|
socket._peerId = peerId;
|
|
234
313
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
broadcast(webId, { type: 'peer-joined', webId });
|
|
314
|
+
if (webId) {
|
|
315
|
+
// Authenticated: register for identity-based signaling
|
|
316
|
+
const existing = peers.get(webId);
|
|
317
|
+
const isReconnect = !!existing;
|
|
318
|
+
if (existing) {
|
|
319
|
+
if (existing._peerId) removePeerFromAllResources(existing._peerId);
|
|
320
|
+
peers.delete(webId);
|
|
321
|
+
existing.close();
|
|
322
|
+
}
|
|
323
|
+
peers.set(webId, socket);
|
|
324
|
+
socket.webId = webId;
|
|
325
|
+
|
|
326
|
+
socket.send(JSON.stringify({
|
|
327
|
+
type: 'peers',
|
|
328
|
+
you: webId,
|
|
329
|
+
peerId: peerId,
|
|
330
|
+
peers: [...peers.keys()].filter(id => id !== webId)
|
|
331
|
+
}));
|
|
332
|
+
|
|
333
|
+
if (!isReconnect) {
|
|
334
|
+
broadcast(webId, { type: 'peer-joined', webId });
|
|
335
|
+
}
|
|
258
336
|
}
|
|
259
337
|
|
|
260
338
|
socket.on('message', (data) => {
|
|
@@ -273,6 +351,12 @@ export async function webrtcPlugin(fastify, options = {}) {
|
|
|
273
351
|
return;
|
|
274
352
|
}
|
|
275
353
|
|
|
354
|
+
// WebTorrent tracker protocol (uses 'action' instead of 'type')
|
|
355
|
+
if (msg.action === 'announce') {
|
|
356
|
+
handleWebtorrentAnnounce(socket, peerId, msg);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
276
360
|
if (!msg.type) {
|
|
277
361
|
socket.send(JSON.stringify({ type: 'error', message: 'Missing "type" field' }));
|
|
278
362
|
return;
|
|
@@ -292,7 +376,11 @@ export async function webrtcPlugin(fastify, options = {}) {
|
|
|
292
376
|
return;
|
|
293
377
|
}
|
|
294
378
|
|
|
295
|
-
// Identity-based messages require "to" field
|
|
379
|
+
// Identity-based messages require authentication and "to" field
|
|
380
|
+
if (!webId) {
|
|
381
|
+
socket.send(JSON.stringify({ type: 'error', message: 'Authentication required for identity-based signaling' }));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
296
384
|
if (!msg.to) {
|
|
297
385
|
socket.send(JSON.stringify({ type: 'error', message: 'Missing "to" field' }));
|
|
298
386
|
return;
|
package/test/webrtc.test.js
CHANGED
|
@@ -85,13 +85,27 @@ describe('WebRTC Signaling', () => {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
describe('Authentication', () => {
|
|
88
|
-
it('should
|
|
88
|
+
it('should allow unauthenticated connections for tracker protocol', async () => {
|
|
89
89
|
const ws = new WebSocket(wsUrl);
|
|
90
|
+
await new Promise((resolve) => { ws.onopen = resolve; });
|
|
90
91
|
|
|
92
|
+
// Unauthenticated clients can use tracker protocol
|
|
93
|
+
ws.send(JSON.stringify({ action: 'announce', info_hash: '01234567890123456789', peer_id: '98765432109876543210', offers: [] }));
|
|
94
|
+
const msg = await waitForMessage(ws, 'announce', 3000).catch(() => null);
|
|
95
|
+
// Should get a response (not get disconnected)
|
|
96
|
+
ws.close();
|
|
97
|
+
await new Promise(r => setTimeout(r, 50));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should reject unauthenticated identity-based signaling', async () => {
|
|
101
|
+
const ws = new WebSocket(wsUrl);
|
|
102
|
+
await new Promise((resolve) => { ws.onopen = resolve; });
|
|
103
|
+
|
|
104
|
+
ws.send(JSON.stringify({ type: 'offer', to: 'someone', sdp: 'test' }));
|
|
91
105
|
const msg = await waitForMessage(ws, 'error');
|
|
92
|
-
assert.strictEqual(msg.type, 'error');
|
|
93
106
|
assert.ok(msg.message.includes('Authentication'));
|
|
94
107
|
ws.close();
|
|
108
|
+
await new Promise(r => setTimeout(r, 50));
|
|
95
109
|
});
|
|
96
110
|
|
|
97
111
|
it('should accept authenticated connections', async () => {
|