gnutella 1.0.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.
Files changed (45) hide show
  1. package/CLI.md +189 -0
  2. package/DEVELOPER.md +193 -0
  3. package/LICENSE +674 -0
  4. package/QUICKSTART.md +133 -0
  5. package/README.md +74 -0
  6. package/bin/gnutella.ts +15 -0
  7. package/gnutella.json.example +18 -0
  8. package/package.json +72 -0
  9. package/src/cli.ts +692 -0
  10. package/src/cli_shared.ts +359 -0
  11. package/src/const.ts +138 -0
  12. package/src/gwebcache/bootstrap.ts +491 -0
  13. package/src/gwebcache/response.ts +391 -0
  14. package/src/gwebcache/shared.ts +116 -0
  15. package/src/gwebcache/types.ts +187 -0
  16. package/src/gwebcache_client.ts +13 -0
  17. package/src/protocol/browse_host.ts +552 -0
  18. package/src/protocol/client_blocking.ts +29 -0
  19. package/src/protocol/codec.ts +715 -0
  20. package/src/protocol/content_urn.ts +170 -0
  21. package/src/protocol/core_utils.ts +43 -0
  22. package/src/protocol/file_server.ts +245 -0
  23. package/src/protocol/ggep.ts +168 -0
  24. package/src/protocol/handshake.ts +199 -0
  25. package/src/protocol/http_download_reader.ts +112 -0
  26. package/src/protocol/magnet.ts +176 -0
  27. package/src/protocol/node.ts +416 -0
  28. package/src/protocol/node_handshake.ts +992 -0
  29. package/src/protocol/node_lifecycle.ts +210 -0
  30. package/src/protocol/node_protocol_runtime.ts +949 -0
  31. package/src/protocol/node_qrp_runtime.ts +97 -0
  32. package/src/protocol/node_query_routing.ts +208 -0
  33. package/src/protocol/node_state.ts +745 -0
  34. package/src/protocol/node_tls.ts +257 -0
  35. package/src/protocol/node_topology.ts +141 -0
  36. package/src/protocol/node_transfer.ts +455 -0
  37. package/src/protocol/node_types.ts +106 -0
  38. package/src/protocol/peer_state.ts +675 -0
  39. package/src/protocol/qrp.ts +549 -0
  40. package/src/protocol/query_search.ts +29 -0
  41. package/src/protocol/share_index.ts +131 -0
  42. package/src/protocol/share_library.ts +246 -0
  43. package/src/protocol.ts +36 -0
  44. package/src/shared.ts +236 -0
  45. package/src/types.ts +452 -0
@@ -0,0 +1,949 @@
1
+ import crypto from "node:crypto";
2
+ import type net from "node:net";
3
+ import zlib from "node:zlib";
4
+
5
+ import { HEADER_LEN, LOCAL_ROUTE, TYPE, TYPE_NAME } from "../const";
6
+ import { errMsg, toBuffer, ts } from "../shared";
7
+ import type {
8
+ PendingPush,
9
+ PeerRole,
10
+ PeerCapabilities,
11
+ QueryDescriptor,
12
+ Route,
13
+ SearchHit,
14
+ } from "../types";
15
+ import {
16
+ buildHeader,
17
+ encodeBye,
18
+ encodePong,
19
+ encodeQueryHit,
20
+ parseBye,
21
+ parseHeader,
22
+ parsePong,
23
+ parsePush,
24
+ parseQuery,
25
+ parseQueryHit,
26
+ } from "./codec";
27
+ import {
28
+ findHeaderEnd,
29
+ parseHttpHeaders,
30
+ socketCanEnd,
31
+ } from "./handshake";
32
+ import type { GnutellaServent } from "./node";
33
+ import {
34
+ broadcastPingToPeers,
35
+ routeQueryToPeers,
36
+ sendPublishedQrpToMeshPeers,
37
+ } from "./node_query_routing";
38
+ import type {
39
+ DescriptorHeader,
40
+ HttpSession,
41
+ HttpSessionRequest,
42
+ Peer,
43
+ } from "./node_types";
44
+ import { firstSha1Urn } from "./content_urn";
45
+ import {
46
+ initialRemoteQrpState,
47
+ matchQuery as shareMatchesQuery,
48
+ splitSearchTerms,
49
+ } from "./qrp";
50
+
51
+ function descriptorTypeName(payloadType: number): string {
52
+ return TYPE_NAME[payloadType] || `0x${payloadType.toString(16)}`;
53
+ }
54
+
55
+ type RoutedDescriptor = Pick<
56
+ DescriptorHeader,
57
+ "descriptorId" | "descriptorIdHex" | "payloadType" | "ttl" | "hops"
58
+ >;
59
+
60
+ const MAX_HTTP_REQUEST_BODY_BYTES = 256 * 1024;
61
+
62
+ export function attachPeer(
63
+ node: GnutellaServent,
64
+ socket: net.Socket,
65
+ outbound: boolean,
66
+ remoteLabel: string,
67
+ role: PeerRole,
68
+ capabilities: PeerCapabilities,
69
+ initialBuf: Buffer = Buffer.alloc(0),
70
+ dialTarget?: string,
71
+ ): Peer {
72
+ const connectedAt = node.now();
73
+ const key = `p${++node.peerSeq}`;
74
+ const peer: Peer = {
75
+ key,
76
+ socket,
77
+ buf: Buffer.alloc(0),
78
+ outbound,
79
+ remoteLabel,
80
+ dialTarget,
81
+ role,
82
+ capabilities,
83
+ remoteQrp: initialRemoteQrpState(),
84
+ lastPingAt: 0,
85
+ connectedAt,
86
+ };
87
+ node.peers.set(key, peer);
88
+ socket.setNoDelay(true);
89
+ socket.setTimeout(0);
90
+
91
+ let closed = false;
92
+ const drop = (message: string) => {
93
+ if (closed) return;
94
+ closed = true;
95
+ const hadLeafQrp =
96
+ node.nodeMode() === "ultrapeer" &&
97
+ peer.role === "leaf" &&
98
+ peer.remoteQrp.table != null;
99
+ node.markPeerSeenIfStable(peer);
100
+ node.peers.delete(peer.key);
101
+ node.refreshGWebCacheReport();
102
+ if (hadLeafQrp) sendPublishedQrpToMeshPeers(node);
103
+ if (!node.stopped) {
104
+ node.emitEvent({
105
+ type: "PEER_DROPPED",
106
+ at: ts(),
107
+ peer: node.peerInfo(peer),
108
+ message,
109
+ });
110
+ }
111
+ };
112
+
113
+ if (peer.capabilities.compressOut) {
114
+ peer.deflater = zlib.createDeflate();
115
+ peer.deflater.on("data", (chunk) => {
116
+ if (!socket.destroyed) socket.write(chunk);
117
+ });
118
+ peer.deflater.on("error", (error) => {
119
+ drop(`deflater error: ${errMsg(error)}`);
120
+ socket.destroy();
121
+ });
122
+ }
123
+
124
+ const feedDecoded = (chunk: Buffer) => {
125
+ peer.buf = Buffer.concat([peer.buf, chunk]);
126
+ try {
127
+ node.consumePeerBuffer(peer);
128
+ } catch (error) {
129
+ drop(errMsg(error));
130
+ socket.destroy();
131
+ }
132
+ };
133
+
134
+ if (peer.capabilities.compressIn) {
135
+ peer.inflater = zlib.createInflate();
136
+ peer.inflater.on("data", (chunk) => feedDecoded(toBuffer(chunk)));
137
+ peer.inflater.on("error", (error) => {
138
+ drop(`inflater error: ${errMsg(error)}`);
139
+ socket.destroy();
140
+ });
141
+ }
142
+
143
+ socket.on("data", (chunk) => {
144
+ if (closed) return;
145
+ const data = toBuffer(chunk);
146
+ if (peer.inflater) peer.inflater.write(data);
147
+ else feedDecoded(data);
148
+ });
149
+ socket.on("close", () => drop("socket closed"));
150
+ socket.on("error", (error) => drop(errMsg(error)));
151
+
152
+ if (initialBuf.length) {
153
+ if (peer.inflater) peer.inflater.write(initialBuf);
154
+ else feedDecoded(initialBuf);
155
+ }
156
+
157
+ node.rememberPeerAddresses(peer);
158
+ node.refreshGWebCacheReport();
159
+ node.emitEvent({
160
+ type: "PEER_CONNECTED",
161
+ at: ts(),
162
+ peer: node.peerInfo(peer),
163
+ });
164
+ node.scheduleOnce(300, () => node.sendPing(1));
165
+ if (
166
+ node.config().enableQrp &&
167
+ (capabilities.queryRoutingVersion ||
168
+ capabilities.ultrapeerQueryRoutingVersion)
169
+ ) {
170
+ node.scheduleOnce(
171
+ 500,
172
+ () => void node.sendQrpTable(peer).catch(() => void 0),
173
+ );
174
+ }
175
+ return peer;
176
+ }
177
+
178
+ export function startHttpSession(
179
+ node: GnutellaServent,
180
+ socket: net.Socket,
181
+ firstHead: string,
182
+ initialBuf: Buffer = Buffer.alloc(0),
183
+ ): void {
184
+ const session: HttpSession = {
185
+ socket,
186
+ buf: Buffer.from(initialBuf),
187
+ busy: false,
188
+ closed: false,
189
+ };
190
+
191
+ const closeSession = () => {
192
+ if (session.closed) return;
193
+ session.closed = true;
194
+ socket.off("data", onData);
195
+ socket.off("close", closeSession);
196
+ socket.off("end", closeSession);
197
+ socket.off("error", onError);
198
+ };
199
+
200
+ const onData = (chunk: string | Buffer) => {
201
+ if (session.closed) return;
202
+ session.buf = Buffer.concat([session.buf, toBuffer(chunk)]);
203
+ void node.drainHttpSession(session, closeSession);
204
+ };
205
+
206
+ const onError = () => closeSession();
207
+
208
+ socket.on("data", onData);
209
+ socket.on("close", closeSession);
210
+ socket.on("end", closeSession);
211
+ socket.on("error", onError);
212
+
213
+ void node.drainHttpSession(session, closeSession, firstHead);
214
+ }
215
+
216
+ export function pendingHttpSessionHeadEnd(
217
+ _node: GnutellaServent,
218
+ session: HttpSession,
219
+ ): number {
220
+ return findHeaderEnd(session.buf.toString("latin1"));
221
+ }
222
+
223
+ export function shiftHttpSessionHead(
224
+ node: GnutellaServent,
225
+ session: HttpSession,
226
+ ): string | undefined {
227
+ const cut = node.pendingHttpSessionHeadEnd(session);
228
+ if (cut === -1) return undefined;
229
+ const raw = session.buf.toString("latin1");
230
+ const head = raw.slice(0, cut);
231
+ session.buf = session.buf.subarray(cut);
232
+ return head;
233
+ }
234
+
235
+ function httpRequestContentLength(head: string): number {
236
+ const raw = parseHttpHeaders(head)["content-length"];
237
+ if (!raw) return 0;
238
+ const length = Number(raw);
239
+ if (!Number.isInteger(length) || length < 0) {
240
+ throw new Error("invalid http content-length");
241
+ }
242
+ if (length > MAX_HTTP_REQUEST_BODY_BYTES) {
243
+ throw new Error("http request body too large");
244
+ }
245
+ return length;
246
+ }
247
+
248
+ export function shiftHttpSessionRequest(
249
+ node: GnutellaServent,
250
+ session: HttpSession,
251
+ ): HttpSessionRequest | undefined {
252
+ const cut = node.pendingHttpSessionHeadEnd(session);
253
+ if (cut === -1) return undefined;
254
+ const raw = session.buf.toString("latin1");
255
+ const head = raw.slice(0, cut);
256
+ const contentLength = httpRequestContentLength(head);
257
+ if (session.buf.length < cut + contentLength) return undefined;
258
+ const body = Buffer.from(session.buf.subarray(cut, cut + contentLength));
259
+ session.buf = session.buf.subarray(cut + contentLength);
260
+ return { head, body };
261
+ }
262
+
263
+ export async function processHttpSessionRequests(
264
+ node: GnutellaServent,
265
+ session: HttpSession,
266
+ closeSession: () => void,
267
+ nextHead?: string,
268
+ ): Promise<void> {
269
+ let pendingHead = nextHead;
270
+ let queued: HttpSessionRequest | undefined;
271
+ while (!session.closed) {
272
+ if (!queued && pendingHead) {
273
+ const contentLength = httpRequestContentLength(pendingHead);
274
+ if (session.buf.length < contentLength) return;
275
+ queued = {
276
+ head: pendingHead,
277
+ body: Buffer.from(session.buf.subarray(0, contentLength)),
278
+ };
279
+ session.buf = session.buf.subarray(contentLength);
280
+ pendingHead = undefined;
281
+ }
282
+ queued ||= node.shiftHttpSessionRequest(session);
283
+ if (!queued) return;
284
+ const keepAlive = await node.handleIncomingGet(
285
+ session.socket,
286
+ queued.head,
287
+ queued.body,
288
+ );
289
+ queued = undefined;
290
+ if (keepAlive) continue;
291
+ closeSession();
292
+ if (socketCanEnd(session.socket)) session.socket.end();
293
+ return;
294
+ }
295
+ }
296
+
297
+ export async function drainHttpSession(
298
+ node: GnutellaServent,
299
+ session: HttpSession,
300
+ closeSession: () => void,
301
+ nextHead?: string,
302
+ ): Promise<void> {
303
+ if (session.closed || session.busy) return;
304
+ session.busy = true;
305
+ try {
306
+ await node.processHttpSessionRequests(session, closeSession, nextHead);
307
+ } catch (error) {
308
+ closeSession();
309
+ session.socket.destroy(error instanceof Error ? error : undefined);
310
+ } finally {
311
+ session.busy = false;
312
+ }
313
+ if (session.closed) return;
314
+ if (node.pendingHttpSessionHeadEnd(session) !== -1) {
315
+ void node.drainHttpSession(session, closeSession);
316
+ }
317
+ }
318
+
319
+ export function consumePeerBuffer(
320
+ node: GnutellaServent,
321
+ peer: Peer,
322
+ ): void {
323
+ while (peer.buf.length >= HEADER_LEN) {
324
+ const hdr = parseHeader(peer.buf.subarray(0, HEADER_LEN));
325
+ if (hdr.payloadLength > node.config().maxPayloadBytes) {
326
+ throw new Error(`payload too large: ${hdr.payloadLength}`);
327
+ }
328
+ if (peer.buf.length < HEADER_LEN + hdr.payloadLength) return;
329
+ const payload = peer.buf.subarray(
330
+ HEADER_LEN,
331
+ HEADER_LEN + hdr.payloadLength,
332
+ );
333
+ peer.buf = peer.buf.subarray(HEADER_LEN + hdr.payloadLength);
334
+ if (!node.validateDescriptor(hdr.payloadType, payload)) {
335
+ throw new Error(
336
+ `invalid ${descriptorTypeName(hdr.payloadType)} payload`,
337
+ );
338
+ }
339
+ if (hdr.payloadType !== TYPE.QUERY) {
340
+ hdr.ttl = Math.min(hdr.ttl, node.config().maxTtl);
341
+ }
342
+ node.emitEvent({
343
+ type: "PEER_MESSAGE_RECEIVED",
344
+ at: ts(),
345
+ peer: node.peerInfo(peer),
346
+ payloadType: hdr.payloadType,
347
+ payloadTypeName: descriptorTypeName(hdr.payloadType),
348
+ descriptorIdHex: hdr.descriptorIdHex,
349
+ ttl: hdr.ttl,
350
+ hops: hdr.hops,
351
+ payloadLength: payload.length,
352
+ });
353
+ node.handleDescriptor(peer, hdr, payload);
354
+ }
355
+ }
356
+
357
+ export function validateDescriptor(
358
+ _node: GnutellaServent,
359
+ payloadType: number,
360
+ payload: Buffer,
361
+ ): boolean {
362
+ switch (payloadType) {
363
+ case TYPE.PING:
364
+ return true;
365
+ case TYPE.PONG:
366
+ return payload.length >= 14;
367
+ case TYPE.BYE:
368
+ return payload.length >= 2;
369
+ case TYPE.ROUTE_TABLE_UPDATE:
370
+ return payload.length >= 1;
371
+ case TYPE.PUSH:
372
+ return payload.length >= 26;
373
+ case TYPE.QUERY:
374
+ return payload.length >= 3;
375
+ case TYPE.QUERY_HIT:
376
+ return payload.length >= 27;
377
+ default:
378
+ return true;
379
+ }
380
+ }
381
+
382
+ export function sendRaw(
383
+ _node: GnutellaServent,
384
+ peer: Peer,
385
+ frame: Buffer,
386
+ ): void {
387
+ if (peer.deflater) {
388
+ peer.deflater.write(frame);
389
+ peer.deflater.flush(zlib.constants.Z_SYNC_FLUSH);
390
+ return;
391
+ }
392
+ peer.socket.write(frame);
393
+ }
394
+
395
+ export function sendToPeer(
396
+ node: GnutellaServent,
397
+ peer: Peer,
398
+ payloadType: number,
399
+ descriptorId: Buffer,
400
+ ttl: number,
401
+ hops: number,
402
+ payload: Buffer,
403
+ ): void {
404
+ if (peer.closingAfterBye && payloadType !== TYPE.BYE) return;
405
+ const frame = buildHeader(descriptorId, payloadType, ttl, hops, payload);
406
+ node.sendRaw(peer, frame);
407
+ node.emitEvent({
408
+ type: "PEER_MESSAGE_SENT",
409
+ at: ts(),
410
+ peer: node.peerInfo(peer),
411
+ payloadType,
412
+ payloadTypeName: descriptorTypeName(payloadType),
413
+ descriptorIdHex: descriptorId.toString("hex"),
414
+ ttl,
415
+ hops,
416
+ payloadLength: payload.length,
417
+ });
418
+ }
419
+
420
+ export function forwardToRoute(
421
+ node: GnutellaServent,
422
+ route: Route,
423
+ payloadType: number,
424
+ descriptorId: Buffer,
425
+ ttl: number,
426
+ hops: number,
427
+ payload: Buffer,
428
+ ): void {
429
+ if (ttl <= 0) return;
430
+ const peer = node.peers.get(route.peerKey);
431
+ if (!peer) return;
432
+ node.sendToPeer(
433
+ peer,
434
+ payloadType,
435
+ descriptorId,
436
+ Math.max(0, ttl - 1),
437
+ hops + 1,
438
+ payload,
439
+ );
440
+ }
441
+
442
+ export function broadcast(
443
+ node: GnutellaServent,
444
+ payloadType: number,
445
+ descriptorId: Buffer,
446
+ ttl: number,
447
+ hops: number,
448
+ payload: Buffer,
449
+ exceptPeerKey?: string,
450
+ ): void {
451
+ for (const peer of node.peers.values()) {
452
+ if (exceptPeerKey && peer.key === exceptPeerKey) continue;
453
+ node.sendToPeer(peer, payloadType, descriptorId, ttl, hops, payload);
454
+ }
455
+ }
456
+
457
+ export function broadcastQuery(
458
+ node: GnutellaServent,
459
+ descriptorId: Buffer,
460
+ ttl: number,
461
+ hops: number,
462
+ payload: Buffer,
463
+ _search: string,
464
+ exceptPeerKey?: string,
465
+ ): void {
466
+ routeQueryToPeers(
467
+ node,
468
+ descriptorId,
469
+ ttl,
470
+ hops,
471
+ payload,
472
+ parseQuery(payload),
473
+ exceptPeerKey,
474
+ true,
475
+ );
476
+ }
477
+
478
+ export function normalizeQueryLifetime(
479
+ node: GnutellaServent,
480
+ ttl: number,
481
+ hops: number,
482
+ ): { ttl: number; hops: number } | null {
483
+ if (ttl > 15) return null;
484
+ const maxLife = Math.max(1, node.config().maxTtl);
485
+ if (hops > maxLife) return null;
486
+ return { ttl: Math.max(0, Math.min(ttl, maxLife - hops)), hops };
487
+ }
488
+
489
+ export function isIndexQuery(
490
+ _node: GnutellaServent,
491
+ hdr: Pick<DescriptorHeader, "ttl" | "hops">,
492
+ q: QueryDescriptor,
493
+ ): boolean {
494
+ return hdr.ttl === 1 && hdr.hops === 0 && q.search === " ";
495
+ }
496
+
497
+ export function shouldIgnoreQuery(
498
+ node: GnutellaServent,
499
+ hdr: Pick<DescriptorHeader, "ttl" | "hops">,
500
+ q: QueryDescriptor,
501
+ ): boolean {
502
+ if (q.urns.length) return false;
503
+ if (node.isIndexQuery(hdr, q)) return false;
504
+ if (!q.search.trim()) return true;
505
+ const words = splitSearchTerms(q.search);
506
+ if (!words.length) return true;
507
+ return words.every((word) => word.length <= 1);
508
+ }
509
+
510
+ export function enqueuePendingPush(
511
+ node: GnutellaServent,
512
+ pending: PendingPush,
513
+ ): void {
514
+ const queue = node.pendingPushes.get(pending.serventIdHex) || [];
515
+ queue.push(pending);
516
+ node.pendingPushes.set(pending.serventIdHex, queue);
517
+ }
518
+
519
+ export function shiftPendingPush(
520
+ node: GnutellaServent,
521
+ serventIdHex: string,
522
+ ): PendingPush | undefined {
523
+ const queue = node.pendingPushes.get(serventIdHex);
524
+ if (!queue?.length) return undefined;
525
+ const pending = queue.shift();
526
+ if (queue.length) node.pendingPushes.set(serventIdHex, queue);
527
+ else node.pendingPushes.delete(serventIdHex);
528
+ return pending;
529
+ }
530
+
531
+ export function cachePongPayload(
532
+ node: GnutellaServent,
533
+ payload: Buffer,
534
+ ): void {
535
+ const digest = crypto.createHash("sha1").update(payload).digest("hex");
536
+ node.pongCache.set(digest, {
537
+ payload: Buffer.from(payload),
538
+ at: node.now(),
539
+ });
540
+ if (node.pongCache.size <= 64) return;
541
+ const oldest = [...node.pongCache.entries()]
542
+ .sort((a, b) => a[1].at - b[1].at)
543
+ .slice(0, node.pongCache.size - 64);
544
+ for (const [key] of oldest) node.pongCache.delete(key);
545
+ }
546
+
547
+ export function shouldIgnoreDescriptor(
548
+ node: GnutellaServent,
549
+ peer: Peer,
550
+ hdr: RoutedDescriptor,
551
+ payload: Buffer,
552
+ ): boolean {
553
+ if (
554
+ peer.closingAfterBye &&
555
+ hdr.payloadType !== TYPE.QUERY_HIT &&
556
+ hdr.payloadType !== TYPE.PUSH
557
+ ) {
558
+ return true;
559
+ }
560
+ if (hdr.payloadType === TYPE.ROUTE_TABLE_UPDATE) return false;
561
+ return node.hasSeen(hdr.payloadType, hdr.descriptorIdHex, payload);
562
+ }
563
+
564
+ export function rejectRelayedLeafDescriptor(
565
+ node: GnutellaServent,
566
+ peer: Peer,
567
+ hdr: RoutedDescriptor,
568
+ ): boolean {
569
+ if (!node.isLeafPeer(peer) || hdr.hops === 0) return false;
570
+ if (peer.capabilities.supportsBye)
571
+ node.sendBye(
572
+ peer,
573
+ 414,
574
+ `Leaf node relayed ${descriptorTypeName(hdr.payloadType)}`,
575
+ );
576
+ else peer.socket.end();
577
+ return true;
578
+ }
579
+
580
+ export function onPingDescriptor(
581
+ node: GnutellaServent,
582
+ peer: Peer,
583
+ hdr: RoutedDescriptor,
584
+ payload: Buffer,
585
+ ): void {
586
+ node.pingRoutes.set(hdr.descriptorIdHex, {
587
+ peerKey: peer.key,
588
+ ts: node.now(),
589
+ });
590
+ node.respondPong(peer, hdr);
591
+ if (!node.shouldRelayPings()) return;
592
+ if (hdr.ttl <= 1 || node.now() - peer.lastPingAt < 1000) return;
593
+ peer.lastPingAt = node.now();
594
+ broadcastPingToPeers(
595
+ node,
596
+ hdr.descriptorId,
597
+ hdr.ttl - 1,
598
+ hdr.hops + 1,
599
+ payload,
600
+ peer.key,
601
+ );
602
+ }
603
+
604
+ export function onQueryDescriptor(
605
+ node: GnutellaServent,
606
+ peer: Peer,
607
+ hdr: RoutedDescriptor,
608
+ payload: Buffer,
609
+ ): void {
610
+ const q = parseQuery(payload);
611
+ const normalized = node.normalizeQueryLifetime(hdr.ttl, hdr.hops);
612
+ node.emitEvent({
613
+ type: "QUERY_RECEIVED",
614
+ at: ts(),
615
+ peer: node.peerInfo(peer),
616
+ descriptorIdHex: hdr.descriptorIdHex,
617
+ ttl: normalized?.ttl ?? hdr.ttl,
618
+ hops: hdr.hops,
619
+ search: q.search,
620
+ urns: q.urns,
621
+ });
622
+ if (!normalized) return;
623
+ hdr.ttl = normalized.ttl;
624
+ hdr.hops = normalized.hops;
625
+ if (node.shouldIgnoreQuery(hdr, q)) return;
626
+ node.queryRoutes.set(hdr.descriptorIdHex, {
627
+ peerKey: peer.key,
628
+ ts: node.now(),
629
+ });
630
+ node.respondQueryHit(peer, hdr, q);
631
+ if (!node.shouldRelayQueries()) return;
632
+ routeQueryToPeers(
633
+ node,
634
+ hdr.descriptorId,
635
+ hdr.ttl,
636
+ hdr.hops,
637
+ payload,
638
+ q,
639
+ peer.key,
640
+ );
641
+ }
642
+
643
+ export function dispatchDescriptor(
644
+ node: GnutellaServent,
645
+ peer: Peer,
646
+ hdr: RoutedDescriptor,
647
+ payload: Buffer,
648
+ ): void {
649
+ switch (hdr.payloadType) {
650
+ case TYPE.PING:
651
+ node.onPingDescriptor(peer, hdr, payload);
652
+ return;
653
+ case TYPE.PONG:
654
+ node.onPong(peer, hdr, payload);
655
+ return;
656
+ case TYPE.BYE:
657
+ node.onBye(peer, payload);
658
+ return;
659
+ case TYPE.ROUTE_TABLE_UPDATE:
660
+ node.onRouteTableUpdate(peer, payload);
661
+ return;
662
+ case TYPE.QUERY:
663
+ node.onQueryDescriptor(peer, hdr, payload);
664
+ return;
665
+ case TYPE.QUERY_HIT:
666
+ node.onQueryHit(peer, hdr, payload);
667
+ return;
668
+ case TYPE.PUSH:
669
+ void node.onPush(peer, hdr, payload);
670
+ return;
671
+ default:
672
+ return;
673
+ }
674
+ }
675
+
676
+ export function handleDescriptor(
677
+ node: GnutellaServent,
678
+ peer: Peer,
679
+ hdr: RoutedDescriptor,
680
+ payload: Buffer,
681
+ ): void {
682
+ if (node.rejectRelayedLeafDescriptor(peer, hdr)) return;
683
+ if (node.shouldIgnoreDescriptor(peer, hdr, payload)) return;
684
+ if (hdr.payloadType !== TYPE.ROUTE_TABLE_UPDATE) {
685
+ node.markSeen(hdr.payloadType, hdr.descriptorIdHex, payload);
686
+ }
687
+ node.dispatchDescriptor(peer, hdr, payload);
688
+ }
689
+
690
+ export function sendBye(
691
+ node: GnutellaServent,
692
+ peer: Peer,
693
+ code: number,
694
+ message: string,
695
+ ): void {
696
+ peer.closingAfterBye = true;
697
+ node.sendToPeer(
698
+ peer,
699
+ TYPE.BYE,
700
+ node.randomId16(),
701
+ 1,
702
+ 0,
703
+ encodeBye(code, message),
704
+ );
705
+ }
706
+
707
+ export function respondPong(
708
+ node: GnutellaServent,
709
+ peer: Peer,
710
+ hdr: Pick<DescriptorHeader, "descriptorId" | "hops">,
711
+ ): void {
712
+ const ttl = Math.max(1, hdr.hops);
713
+ const own = encodePong(
714
+ node.currentAdvertisedPort(),
715
+ node.currentAdvertisedHost(),
716
+ node.shares.length,
717
+ node.totalSharedKBytes(),
718
+ );
719
+ node.sendToPeer(peer, TYPE.PONG, hdr.descriptorId, ttl, 0, own);
720
+ if (!node.config().enablePongCaching) return;
721
+ let sent = 1;
722
+ const cached = [...node.pongCache.values()].sort((a, b) => b.at - a.at);
723
+ for (const entry of cached) {
724
+ if (sent >= 10) break;
725
+ node.sendToPeer(
726
+ peer,
727
+ TYPE.PONG,
728
+ hdr.descriptorId,
729
+ ttl,
730
+ 0,
731
+ entry.payload,
732
+ );
733
+ sent++;
734
+ }
735
+ }
736
+
737
+ export function respondQueryHit(
738
+ node: GnutellaServent,
739
+ peer: Peer,
740
+ hdr: Pick<DescriptorHeader, "descriptorId" | "hops" | "ttl">,
741
+ payloadOrQuery: Buffer | QueryDescriptor,
742
+ ): void {
743
+ const q = Buffer.isBuffer(payloadOrQuery)
744
+ ? parseQuery(payloadOrQuery)
745
+ : payloadOrQuery;
746
+ const matches = node.isIndexQuery(hdr, q)
747
+ ? node.shares
748
+ : node.shares.filter((share) => shareMatchesQuery(q, share));
749
+ if (!matches.length) return;
750
+ const limit = Math.max(1, node.config().maxResultsPerQuery);
751
+ const batchSize = 16;
752
+ const chosen = matches.slice(0, limit);
753
+ const replyTtl = Math.min(
754
+ node.config().maxTtl,
755
+ Math.max(1, hdr.hops + 2),
756
+ );
757
+ for (let off = 0; off < chosen.length; off += batchSize) {
758
+ const batch = chosen.slice(off, off + batchSize);
759
+ const out = encodeQueryHit(
760
+ node.currentAdvertisedPort(),
761
+ node.currentAdvertisedHost(),
762
+ node.config().advertisedSpeedKBps,
763
+ batch,
764
+ node.serventId,
765
+ {
766
+ vendorCode: node.config().vendorCode,
767
+ push: false,
768
+ busy: false,
769
+ haveUploaded: false,
770
+ measuredSpeed: true,
771
+ ggepHashes: q.ggepHAllowed && !!node.config().enableGgep,
772
+ browseHost: !!node.config().enableGgep,
773
+ },
774
+ );
775
+ node.sendToPeer(
776
+ peer,
777
+ TYPE.QUERY_HIT,
778
+ hdr.descriptorId,
779
+ replyTtl,
780
+ 0,
781
+ out,
782
+ );
783
+ }
784
+ }
785
+
786
+ export function onPong(
787
+ node: GnutellaServent,
788
+ _peer: Peer,
789
+ hdr: RoutedDescriptor,
790
+ payload: Buffer,
791
+ ): void {
792
+ const pong = parsePong(payload);
793
+ node.cachePongPayload(payload);
794
+ node.addKnownPeer(pong.ip, pong.port);
795
+ const route = node.pingRoutes.get(hdr.descriptorIdHex);
796
+ if (!route) return;
797
+ if (route === LOCAL_ROUTE) {
798
+ node.emitEvent({
799
+ type: "PONG",
800
+ at: ts(),
801
+ ip: pong.ip,
802
+ port: pong.port,
803
+ files: pong.files,
804
+ kbytes: pong.kbytes,
805
+ });
806
+ return;
807
+ }
808
+ node.forwardToRoute(
809
+ route,
810
+ TYPE.PONG,
811
+ hdr.descriptorId,
812
+ hdr.ttl,
813
+ hdr.hops,
814
+ payload,
815
+ );
816
+ }
817
+
818
+ export function onQueryHit(
819
+ node: GnutellaServent,
820
+ peer: Peer,
821
+ hdr: RoutedDescriptor,
822
+ payload: Buffer,
823
+ ): void {
824
+ const qh = parseQueryHit(payload);
825
+ node.pushRoutes.set(qh.serventIdHex, {
826
+ peerKey: peer.key,
827
+ ts: node.now(),
828
+ });
829
+ const route = node.queryRoutes.get(hdr.descriptorIdHex);
830
+ if (!route) return;
831
+ if (route === LOCAL_ROUTE) {
832
+ for (const result of qh.results) {
833
+ const hit: SearchHit = {
834
+ resultNo: node.resultSeq++,
835
+ queryIdHex: hdr.descriptorIdHex,
836
+ queryHops: hdr.hops,
837
+ remoteHost: qh.ip,
838
+ remotePort: qh.port,
839
+ speedKBps: qh.speedKBps,
840
+ fileIndex: result.fileIndex,
841
+ fileName: result.fileName,
842
+ fileSize: result.fileSize,
843
+ serventIdHex: qh.serventIdHex,
844
+ viaPeerKey: peer.key,
845
+ sha1Urn: firstSha1Urn(result.urns),
846
+ urns: result.urns,
847
+ metadata: result.metadata,
848
+ vendorCode: qh.vendorCode,
849
+ needsPush: qh.flagPush,
850
+ busy: qh.flagBusy,
851
+ };
852
+ node.lastResults.push(hit);
853
+ node.emitEvent({ type: "QUERY_RESULT", at: ts(), hit });
854
+ }
855
+ return;
856
+ }
857
+ if (node.nodeMode() === "leaf") return;
858
+ node.forwardToRoute(
859
+ route,
860
+ TYPE.QUERY_HIT,
861
+ hdr.descriptorId,
862
+ hdr.ttl,
863
+ hdr.hops,
864
+ payload,
865
+ );
866
+ }
867
+
868
+ export async function onPush(
869
+ node: GnutellaServent,
870
+ _peer: Peer,
871
+ hdr: RoutedDescriptor,
872
+ payload: Buffer,
873
+ ): Promise<void> {
874
+ const push = parsePush(payload);
875
+ if (push.serventIdHex === node.serventId.toString("hex")) {
876
+ await node.fulfillPush(push);
877
+ return;
878
+ }
879
+ if (node.nodeMode() === "leaf") return;
880
+ const route = node.pushRoutes.get(push.serventIdHex);
881
+ if (!route) return;
882
+ node.forwardToRoute(
883
+ route,
884
+ TYPE.PUSH,
885
+ hdr.descriptorId,
886
+ hdr.ttl,
887
+ hdr.hops,
888
+ payload,
889
+ );
890
+ }
891
+
892
+ export function onBye(
893
+ _node: GnutellaServent,
894
+ peer: Peer,
895
+ payload: Buffer,
896
+ ): void {
897
+ try {
898
+ parseBye(payload);
899
+ } catch {
900
+ // ignore parse failure and close anyway
901
+ }
902
+ peer.socket.end();
903
+ }
904
+
905
+ export async function fulfillPush(
906
+ node: GnutellaServent,
907
+ push: ReturnType<typeof parsePush>,
908
+ ): Promise<void> {
909
+ const share = node.sharesByIndex.get(push.fileIndex);
910
+ if (!share) return;
911
+ node.emitEvent({
912
+ type: "PUSH_REQUESTED",
913
+ at: ts(),
914
+ fileIndex: share.index,
915
+ fileName: share.name,
916
+ ip: push.ip,
917
+ port: push.port,
918
+ });
919
+ const socket = node.createConnection({ host: push.ip, port: push.port });
920
+ socket.setNoDelay(true);
921
+ socket.setTimeout(node.config().downloadTimeoutMs, () =>
922
+ socket.destroy(new Error("push connect timeout")),
923
+ );
924
+ socket.on("error", (error) =>
925
+ node.emitEvent({
926
+ type: "PUSH_CALLBACK_FAILED",
927
+ at: ts(),
928
+ message: errMsg(error),
929
+ }),
930
+ );
931
+ socket.on("connect", () => {
932
+ socket.write(
933
+ `GIV ${share.index}:${node.serventId.toString("hex")}/${share.name}\n\n`,
934
+ );
935
+ });
936
+
937
+ let buf = Buffer.alloc(0);
938
+ const onData = (chunk: string | Buffer) => {
939
+ buf = Buffer.concat([buf, toBuffer(chunk)]);
940
+ const raw = buf.toString("latin1");
941
+ const cut = findHeaderEnd(raw);
942
+ if (cut === -1) return;
943
+ const head = raw.slice(0, cut);
944
+ const rest = buf.subarray(cut);
945
+ socket.off("data", onData);
946
+ node.startHttpSession(socket, head, rest);
947
+ };
948
+ socket.on("data", onData);
949
+ }