javascript-solid-server 0.0.114 → 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 +114 -30
- 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,50 +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
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
|
|
224
304
|
const queryToken = request.query?.token;
|
|
225
305
|
if (queryToken && !request.headers.authorization) {
|
|
226
306
|
request.headers.authorization = `Bearer ${queryToken}`;
|
|
227
307
|
}
|
|
228
308
|
const { webId } = await getWebIdFromRequestAsync(request);
|
|
229
|
-
if (!webId) {
|
|
230
|
-
socket.send(JSON.stringify({ type: 'error', message: 'Authentication required' }));
|
|
231
|
-
socket.close();
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
309
|
|
|
235
|
-
// Assign a stable peer ID for content-addressed mode
|
|
310
|
+
// Assign a stable peer ID for content-addressed/tracker mode
|
|
236
311
|
const peerId = String(nextPeerId++);
|
|
237
312
|
socket._peerId = peerId;
|
|
238
313
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
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
|
+
}
|
|
262
336
|
}
|
|
263
337
|
|
|
264
338
|
socket.on('message', (data) => {
|
|
@@ -277,6 +351,12 @@ export async function webrtcPlugin(fastify, options = {}) {
|
|
|
277
351
|
return;
|
|
278
352
|
}
|
|
279
353
|
|
|
354
|
+
// WebTorrent tracker protocol (uses 'action' instead of 'type')
|
|
355
|
+
if (msg.action === 'announce') {
|
|
356
|
+
handleWebtorrentAnnounce(socket, peerId, msg);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
280
360
|
if (!msg.type) {
|
|
281
361
|
socket.send(JSON.stringify({ type: 'error', message: 'Missing "type" field' }));
|
|
282
362
|
return;
|
|
@@ -296,7 +376,11 @@ export async function webrtcPlugin(fastify, options = {}) {
|
|
|
296
376
|
return;
|
|
297
377
|
}
|
|
298
378
|
|
|
299
|
-
// 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
|
+
}
|
|
300
384
|
if (!msg.to) {
|
|
301
385
|
socket.send(JSON.stringify({ type: 'error', message: 'Missing "to" field' }));
|
|
302
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 () => {
|