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,992 @@
1
+ import net from "node:net";
2
+
3
+ import { DEFAULT_USER_AGENT, MAX_XTRY } from "../const";
4
+ import {
5
+ errMsg,
6
+ normalizeIpv4,
7
+ normalizePeer,
8
+ parsePeer,
9
+ toBuffer,
10
+ ts,
11
+ } from "../shared";
12
+ import type { PeerCapabilities, PeerRole } from "../types";
13
+ import {
14
+ blockedClientMessage,
15
+ blockedClientSignature,
16
+ } from "./client_blocking";
17
+ import {
18
+ buildHandshakeBlock,
19
+ describeHandshakeResponse,
20
+ findHeaderEnd,
21
+ hasToken,
22
+ lowerCaseHeaders,
23
+ mergeHeaders,
24
+ parseBoolHeader,
25
+ parseHandshakeBlock,
26
+ parseListenIpHeader,
27
+ parsePeerHeaderList,
28
+ parsePositiveIntHeader,
29
+ } from "./handshake";
30
+ import type { GnutellaServent } from "./node";
31
+ import type { ProbeCtx } from "./node_types";
32
+
33
+ function clearProbeListeners(ctx: ProbeCtx): void {
34
+ if (ctx.onData) ctx.socket.off("data", ctx.onData);
35
+ if (ctx.onEnd) ctx.socket.off("end", ctx.onEnd);
36
+ if (ctx.onClose) ctx.socket.off("close", ctx.onClose);
37
+ if (ctx.onError) ctx.socket.off("error", ctx.onError);
38
+ }
39
+
40
+ function blockedProbeMessage(ip: string): string {
41
+ return `blocked IP ${ip}`;
42
+ }
43
+
44
+ function maybeBlockClientHost(
45
+ node: GnutellaServent,
46
+ remoteHost: string | undefined,
47
+ ): string | undefined {
48
+ const ip = normalizeIpv4(remoteHost);
49
+ if (!ip) return undefined;
50
+ node.blockIp(ip);
51
+ return ip;
52
+ }
53
+
54
+ function probePreview(buf: Buffer): string | undefined {
55
+ const preview = buf
56
+ .toString("latin1")
57
+ .replace(/\r\n/g, "\\r\\n")
58
+ .replace(/\n/g, "\\n")
59
+ .trim();
60
+ if (!preview) return undefined;
61
+ return preview.length > 96 ? `${preview.slice(0, 96)}...` : preview;
62
+ }
63
+
64
+ function describeProbeState(
65
+ ctx: ProbeCtx,
66
+ reason: string,
67
+ ageMs: number,
68
+ detail?: string,
69
+ ): string {
70
+ const parts = [
71
+ `reason=${reason}`,
72
+ `mode=${ctx.mode}`,
73
+ `bytes=${ctx.receivedBytes}`,
74
+ `ageMs=${ageMs}`,
75
+ ];
76
+ if (detail) parts.push(detail);
77
+ const preview = probePreview(ctx.buf);
78
+ if (preview) parts.push(`preview=${JSON.stringify(preview)}`);
79
+ return parts.join(" ");
80
+ }
81
+
82
+ function finishProbe(ctx: ProbeCtx): void {
83
+ ctx.mode = "done";
84
+ clearProbeListeners(ctx);
85
+ }
86
+
87
+ function terminateProbeEarly(
88
+ node: GnutellaServent,
89
+ ctx: ProbeCtx,
90
+ reason: string,
91
+ detail?: string,
92
+ ): void {
93
+ if (ctx.mode === "done") return;
94
+ const ageMs = Math.max(0, node.now() - ctx.startedAtMs);
95
+ emitHandshakeDebug(
96
+ node,
97
+ "inbound",
98
+ "terminated-early",
99
+ handshakePeerLabel(ctx.socket),
100
+ describeProbeState(ctx, reason, ageMs, detail),
101
+ );
102
+ finishProbe(ctx);
103
+ }
104
+
105
+ function handshakePeerLabel(socket: net.Socket): string {
106
+ return `${socket.remoteAddress || "?"}:${socket.remotePort || "?"}`;
107
+ }
108
+
109
+ function emitHandshakeDebug(
110
+ node: GnutellaServent,
111
+ direction: "inbound" | "outbound",
112
+ phase: string,
113
+ peer: string,
114
+ message: string,
115
+ ): void {
116
+ node.emitEvent({
117
+ type: "HANDSHAKE_DEBUG",
118
+ at: ts(),
119
+ direction,
120
+ phase,
121
+ peer,
122
+ message,
123
+ });
124
+ }
125
+
126
+ function emitHandshakeBlock(
127
+ node: GnutellaServent,
128
+ direction: "inbound" | "outbound",
129
+ phase: string,
130
+ peer: string,
131
+ startLine: string,
132
+ headers: Record<string, string>,
133
+ ): void {
134
+ emitHandshakeDebug(
135
+ node,
136
+ direction,
137
+ phase,
138
+ peer,
139
+ describeHandshakeResponse(startLine, headers),
140
+ );
141
+ }
142
+
143
+ function ultrapeerNeededHeader(node: GnutellaServent): string | undefined {
144
+ if (node.nodeMode() !== "ultrapeer") return undefined;
145
+ if (
146
+ node.connectedMeshPeerCount() < node.config().maxUltrapeerConnections
147
+ )
148
+ return "True";
149
+ if (node.connectedLeafCount() < node.config().maxLeafConnections)
150
+ return "False";
151
+ return undefined;
152
+ }
153
+
154
+ const GTK_MODERN_ULTRAPEER_MIN_DEGREE = 16;
155
+
156
+ function baseRoleHeaders(node: GnutellaServent): Record<string, string> {
157
+ const headers: Record<string, string> = {
158
+ "x-ultrapeer": node.nodeMode() === "ultrapeer" ? "True" : "False",
159
+ };
160
+ const ultrapeerNeeded = ultrapeerNeededHeader(node);
161
+ if (ultrapeerNeeded) headers["x-ultrapeer-needed"] = ultrapeerNeeded;
162
+ if (node.nodeMode() === "ultrapeer") {
163
+ headers["x-ultrapeer-query-routing"] = "0.1";
164
+ headers["x-dynamic-querying"] = "0.1";
165
+ headers["x-ext-probes"] = "0.1";
166
+ headers["x-degree"] = String(
167
+ Math.max(
168
+ GTK_MODERN_ULTRAPEER_MIN_DEGREE,
169
+ node.config().maxUltrapeerConnections,
170
+ ),
171
+ );
172
+ }
173
+ return headers;
174
+ }
175
+
176
+ function baseFeatureHeaders(
177
+ node: GnutellaServent,
178
+ ): Record<string, string> {
179
+ const c = node.config();
180
+ const headers: Record<string, string> = {};
181
+ if (c.enableQrp)
182
+ headers["x-query-routing"] = c.queryRoutingVersion || "0.1";
183
+ if (c.enableCompression) headers["accept-encoding"] = "deflate";
184
+ if (c.enablePongCaching) headers["pong-caching"] = "0.1";
185
+ if (c.enableGgep) headers["ggep"] = "0.5";
186
+ if (c.enableBye) headers["bye-packet"] = "0.1";
187
+ return headers;
188
+ }
189
+
190
+ export function baseHandshakeHeaders(
191
+ node: GnutellaServent,
192
+ remoteIp?: string,
193
+ ): Record<string, string> {
194
+ const c = node.config();
195
+ const advertisedHost = node.currentAdvertisedHost();
196
+ const advertisedPort = node.currentAdvertisedPort();
197
+ const headers: Record<string, string> = {
198
+ "user-agent": c.userAgent || DEFAULT_USER_AGENT,
199
+ "listen-ip": `${advertisedHost}:${advertisedPort}`,
200
+ "x-max-ttl": String(c.maxTtl),
201
+ ...baseRoleHeaders(node),
202
+ ...baseFeatureHeaders(node),
203
+ };
204
+ const observedRemote = normalizeIpv4(remoteIp);
205
+ if (observedRemote) headers["remote-ip"] = observedRemote;
206
+ return headers;
207
+ }
208
+
209
+ export function buildServerHandshakeHeaders(
210
+ node: GnutellaServent,
211
+ requestHeaders: Record<string, string>,
212
+ remoteIp?: string,
213
+ ): Record<string, string> {
214
+ const headers = node.baseHandshakeHeaders(remoteIp);
215
+ if (node.tlsEnabled() && node.peerRequestedTlsUpgrade(requestHeaders)) {
216
+ headers.upgrade = node.tlsUpgradeToken();
217
+ headers.connection = "Upgrade";
218
+ }
219
+ if (
220
+ node.config().enableCompression &&
221
+ hasToken(requestHeaders["accept-encoding"], "deflate")
222
+ ) {
223
+ headers["content-encoding"] = "deflate";
224
+ }
225
+ return headers;
226
+ }
227
+
228
+ export function buildClientFinalHeaders(
229
+ node: GnutellaServent,
230
+ serverHeaders: Record<string, string>,
231
+ remoteIp?: string,
232
+ ): Record<string, string> {
233
+ const headers: Record<string, string> = {};
234
+ const observedRemote = normalizeIpv4(remoteIp);
235
+ if (observedRemote) headers["remote-ip"] = observedRemote;
236
+ if (node.tlsEnabled() && node.peerAcceptedTlsUpgrade(serverHeaders))
237
+ headers.connection = "Upgrade";
238
+ if (
239
+ node.config().enableCompression &&
240
+ hasToken(serverHeaders["accept-encoding"], "deflate")
241
+ ) {
242
+ headers["content-encoding"] = "deflate";
243
+ }
244
+ return headers;
245
+ }
246
+
247
+ export function buildCapabilities(
248
+ node: GnutellaServent,
249
+ version: string,
250
+ headers: Record<string, string>,
251
+ compressIn: boolean,
252
+ compressOut: boolean,
253
+ ): PeerCapabilities {
254
+ const h = lowerCaseHeaders(headers);
255
+ return {
256
+ version,
257
+ headers: h,
258
+ userAgent: h["user-agent"],
259
+ supportsGgep: !!h["ggep"],
260
+ supportsPongCaching: !!h["pong-caching"],
261
+ supportsBye: !!h["bye-packet"],
262
+ supportsTls: node.peerRequestedTlsUpgrade(h),
263
+ supportsCompression:
264
+ hasToken(h["accept-encoding"], "deflate") ||
265
+ hasToken(h["content-encoding"], "deflate"),
266
+ compressIn,
267
+ compressOut,
268
+ isUltrapeer: parseBoolHeader(h["x-ultrapeer"]),
269
+ ultrapeerNeeded: parseBoolHeader(h["x-ultrapeer-needed"]),
270
+ queryRoutingVersion: h["x-query-routing"],
271
+ ultrapeerQueryRoutingVersion: h["x-ultrapeer-query-routing"],
272
+ dynamicQueryingVersion: h["x-dynamic-querying"],
273
+ extProbesVersion: h["x-ext-probes"],
274
+ degree: parsePositiveIntHeader(h["x-degree"]),
275
+ isCrawler: !!h["crawler"],
276
+ listenIp: parseListenIpHeader(h["listen-ip"]),
277
+ };
278
+ }
279
+
280
+ export function selectTryPeers(
281
+ node: GnutellaServent,
282
+ limit = MAX_XTRY,
283
+ ): string[] {
284
+ const out: string[] = [];
285
+ const seen = new Set<string>();
286
+ const push = (peerSpec?: string) => {
287
+ if (!peerSpec) return;
288
+ const addr = parsePeer(peerSpec);
289
+ if (!addr) return;
290
+ const peer = normalizePeer(addr.host, addr.port);
291
+ if (node.isBlockedHost(addr.host)) return;
292
+ if (node.isSelfPeer(addr.host, addr.port) || seen.has(peer)) return;
293
+ seen.add(peer);
294
+ out.push(peer);
295
+ };
296
+
297
+ for (const peer of node.peers.values()) {
298
+ if (peer.capabilities.listenIp) {
299
+ push(
300
+ normalizePeer(
301
+ peer.capabilities.listenIp.host,
302
+ peer.capabilities.listenIp.port,
303
+ ),
304
+ );
305
+ } else if (peer.dialTarget) {
306
+ push(peer.dialTarget);
307
+ } else {
308
+ push(peer.remoteLabel);
309
+ }
310
+ if (out.length >= limit) return out;
311
+ }
312
+
313
+ for (const peerSpec of node.getKnownPeers()) {
314
+ push(peerSpec);
315
+ if (out.length >= limit) break;
316
+ }
317
+ return out;
318
+ }
319
+
320
+ export function maybeAbsorbTryHeaders(
321
+ node: GnutellaServent,
322
+ headers: Record<string, string>,
323
+ ): void {
324
+ for (const addr of [
325
+ ...parsePeerHeaderList(headers["x-try"]),
326
+ ...parsePeerHeaderList(headers["x-try-ultrapeers"]),
327
+ ]) {
328
+ node.addKnownPeer(addr.host, addr.port);
329
+ }
330
+ }
331
+
332
+ export function reject06(
333
+ node: GnutellaServent,
334
+ socket: net.Socket,
335
+ code: number,
336
+ reason: string,
337
+ extraHeaders: Record<string, string> = {},
338
+ ): void {
339
+ const tryPeers = node.selectTryPeers();
340
+ const headers = lowerCaseHeaders(extraHeaders);
341
+ const observedRemote = normalizeIpv4(socket.remoteAddress);
342
+ if (observedRemote) headers["remote-ip"] = observedRemote;
343
+ if (tryPeers.length) {
344
+ headers["x-try"] = tryPeers.join(",");
345
+ headers["x-try-ultrapeers"] = tryPeers.join(",");
346
+ }
347
+ emitHandshakeBlock(
348
+ node,
349
+ "inbound",
350
+ "reject-sent",
351
+ handshakePeerLabel(socket),
352
+ `GNUTELLA/0.6 ${code} ${reason}`,
353
+ headers,
354
+ );
355
+ socket.end(
356
+ buildHandshakeBlock(`GNUTELLA/0.6 ${code} ${reason}`, headers),
357
+ );
358
+ }
359
+
360
+ export function handleProbe(
361
+ node: GnutellaServent,
362
+ socket: net.Socket,
363
+ ): void {
364
+ const blockedIp = normalizeIpv4(socket.remoteAddress);
365
+ if (blockedIp && node.isBlockedHost(blockedIp)) {
366
+ const message = blockedProbeMessage(blockedIp);
367
+ emitHandshakeDebug(
368
+ node,
369
+ "inbound",
370
+ "blocked",
371
+ handshakePeerLabel(socket),
372
+ message,
373
+ );
374
+ node.emitEvent({
375
+ type: "PROBE_REJECTED",
376
+ at: ts(),
377
+ message,
378
+ });
379
+ socket.destroy();
380
+ return;
381
+ }
382
+ const ctx: ProbeCtx = {
383
+ socket,
384
+ buf: Buffer.alloc(0),
385
+ receivedBytes: 0,
386
+ startedAtMs: node.now(),
387
+ mode: "undecided",
388
+ };
389
+ emitHandshakeDebug(
390
+ node,
391
+ "inbound",
392
+ "probe-open",
393
+ handshakePeerLabel(socket),
394
+ "awaiting inbound protocol bytes",
395
+ );
396
+ socket.setNoDelay(true);
397
+ ctx.onData = (chunk) => {
398
+ if (ctx.mode === "done") return;
399
+ const data = toBuffer(chunk);
400
+ ctx.receivedBytes += data.length;
401
+ ctx.buf = Buffer.concat([ctx.buf, data]);
402
+ try {
403
+ node.tryDecideProbe(ctx);
404
+ } catch (error) {
405
+ const message = errMsg(error);
406
+ emitHandshakeDebug(
407
+ node,
408
+ "inbound",
409
+ "failed",
410
+ handshakePeerLabel(socket),
411
+ message,
412
+ );
413
+ node.emitEvent({
414
+ type: "PROBE_REJECTED",
415
+ at: ts(),
416
+ message,
417
+ });
418
+ finishProbe(ctx);
419
+ socket.destroy();
420
+ }
421
+ };
422
+ ctx.onEnd = () => terminateProbeEarly(node, ctx, "end");
423
+ ctx.onClose = (hadError) =>
424
+ terminateProbeEarly(
425
+ node,
426
+ ctx,
427
+ "close",
428
+ hadError ? "hadError=true" : undefined,
429
+ );
430
+ ctx.onError = (error) => {
431
+ terminateProbeEarly(node, ctx, "error", errMsg(error));
432
+ socket.destroy();
433
+ };
434
+ socket.on("data", ctx.onData);
435
+ socket.on("end", ctx.onEnd);
436
+ socket.on("close", ctx.onClose);
437
+ socket.on("error", ctx.onError);
438
+ }
439
+
440
+ export function handleUndecidedProbe(
441
+ node: GnutellaServent,
442
+ ctx: ProbeCtx,
443
+ ): void {
444
+ const raw = ctx.buf.toString("latin1");
445
+ if (raw.startsWith("GNUTELLA CONNECT/0.6")) {
446
+ node.handleInbound06Probe(ctx, raw);
447
+ return;
448
+ }
449
+ if (/^GNUTELLA CONNECT\/0\./i.test(raw)) {
450
+ node.rejectLegacyInboundProbe(raw);
451
+ return;
452
+ }
453
+ if (/^(GET|HEAD|POST)\s+/i.test(raw)) {
454
+ node.startHttpProbeSession(ctx, raw);
455
+ return;
456
+ }
457
+ if (raw.startsWith("GIV ")) {
458
+ node.startGivProbeSession(ctx, raw);
459
+ return;
460
+ }
461
+ if (ctx.buf.length > 8192) throw new Error("unknown inbound protocol");
462
+ }
463
+
464
+ export function handleInbound06Probe(
465
+ node: GnutellaServent,
466
+ ctx: ProbeCtx,
467
+ raw: string,
468
+ ): void {
469
+ const cut = findHeaderEnd(raw);
470
+ if (cut === -1) return;
471
+ const { startLine, headers } = parseHandshakeBlock(raw.slice(0, cut));
472
+ emitHandshakeBlock(
473
+ node,
474
+ "inbound",
475
+ "connect-recv",
476
+ handshakePeerLabel(ctx.socket),
477
+ startLine,
478
+ headers,
479
+ );
480
+ if (!/^GNUTELLA CONNECT\/0\.[0-9]+/i.test(startLine)) {
481
+ throw new Error(`unexpected 0.6 start line: ${startLine}`);
482
+ }
483
+ const blockedSignature = blockedClientSignature(headers);
484
+ if (blockedSignature) {
485
+ const message = blockedClientMessage(
486
+ blockedSignature,
487
+ ctx.socket.remoteAddress,
488
+ );
489
+ maybeBlockClientHost(node, ctx.socket.remoteAddress);
490
+ emitHandshakeDebug(
491
+ node,
492
+ "inbound",
493
+ "blocked-client",
494
+ handshakePeerLabel(ctx.socket),
495
+ message,
496
+ );
497
+ node.emitEvent({
498
+ type: "PROBE_REJECTED",
499
+ at: ts(),
500
+ message,
501
+ });
502
+ node.reject06(ctx.socket, 503, "Blocked client");
503
+ finishProbe(ctx);
504
+ return;
505
+ }
506
+ node.absorbHandshakeHeaders(headers, ctx.socket.remoteAddress);
507
+ const requestedCaps = node.buildCapabilities(
508
+ "0.6",
509
+ headers,
510
+ false,
511
+ false,
512
+ );
513
+ const requestedRole = node.classifyPeerRole(requestedCaps);
514
+ const acceptance = node.canAcceptPeerRole(requestedRole);
515
+ if (!acceptance.ok) {
516
+ node.reject06(ctx.socket, acceptance.code, acceptance.reason);
517
+ ctx.mode = "done";
518
+ clearProbeListeners(ctx);
519
+ return;
520
+ }
521
+ ctx.requestHeaders = headers;
522
+ ctx.serverHeaders = node.buildServerHandshakeHeaders(
523
+ headers,
524
+ ctx.socket.remoteAddress,
525
+ );
526
+ ctx.socket.write(
527
+ buildHandshakeBlock("GNUTELLA/0.6 200 OK", ctx.serverHeaders),
528
+ );
529
+ emitHandshakeBlock(
530
+ node,
531
+ "inbound",
532
+ "response-sent",
533
+ handshakePeerLabel(ctx.socket),
534
+ "GNUTELLA/0.6 200 OK",
535
+ ctx.serverHeaders,
536
+ );
537
+ ctx.buf = ctx.buf.subarray(cut);
538
+ ctx.mode = "await-final-0.6";
539
+ node.tryDecideProbe(ctx);
540
+ }
541
+
542
+ export function rejectLegacyInboundProbe(
543
+ _node: GnutellaServent,
544
+ raw: string,
545
+ ): void {
546
+ const cut = findHeaderEnd(raw);
547
+ if (cut === -1) return;
548
+ const { startLine } = parseHandshakeBlock(raw.slice(0, cut));
549
+ throw new Error(`unsupported inbound handshake: ${startLine}`);
550
+ }
551
+
552
+ export function startHttpProbeSession(
553
+ node: GnutellaServent,
554
+ ctx: ProbeCtx,
555
+ raw: string,
556
+ ): void {
557
+ const cut = findHeaderEnd(raw);
558
+ if (cut === -1) return;
559
+ ctx.mode = "done";
560
+ clearProbeListeners(ctx);
561
+ node.startHttpSession(
562
+ ctx.socket,
563
+ raw.slice(0, cut),
564
+ ctx.buf.subarray(cut),
565
+ );
566
+ }
567
+
568
+ export function startGivProbeSession(
569
+ node: GnutellaServent,
570
+ ctx: ProbeCtx,
571
+ raw: string,
572
+ ): void {
573
+ const cut = findHeaderEnd(raw);
574
+ if (cut === -1) return;
575
+ ctx.mode = "done";
576
+ clearProbeListeners(ctx);
577
+ void node
578
+ .handleIncomingGiv(ctx.socket, raw.slice(0, cut))
579
+ .catch(() => ctx.socket.destroy());
580
+ }
581
+
582
+ function finalHandshakeCode(startLine: string): number {
583
+ const match = /^GNUTELLA\/0\.[0-9]+\s+(\d+)/i.exec(startLine);
584
+ if (!match) throw new Error(`unexpected final 0.6 line: ${startLine}`);
585
+ return Number(match[1]);
586
+ }
587
+
588
+ function compressionAccepted(
589
+ enabled: boolean,
590
+ headers: Record<string, string>,
591
+ ): boolean {
592
+ return enabled && hasToken(headers["content-encoding"], "deflate");
593
+ }
594
+
595
+ type OutboundHandshakeResult = {
596
+ caps: PeerCapabilities;
597
+ role: PeerRole;
598
+ rest: Buffer;
599
+ finalHeadersWithRemote: Record<string, string>;
600
+ };
601
+
602
+ function shouldUpgradeSocketToTls(
603
+ node: GnutellaServent,
604
+ socket: net.Socket,
605
+ acceptedByServer: boolean,
606
+ acceptedByClient: boolean,
607
+ ): boolean {
608
+ return (
609
+ node.tlsEnabled() &&
610
+ node.canUpgradeSocketToTls(socket) &&
611
+ acceptedByServer &&
612
+ acceptedByClient
613
+ );
614
+ }
615
+
616
+ function attachInbound06Peer(
617
+ node: GnutellaServent,
618
+ socket: net.Socket,
619
+ remoteLabel: string,
620
+ role: PeerRole,
621
+ caps: PeerCapabilities,
622
+ rest: Buffer,
623
+ serverHeaders: Record<string, string>,
624
+ clientHeaders: Record<string, string>,
625
+ ): void {
626
+ const upgradeToTls = shouldUpgradeSocketToTls(
627
+ node,
628
+ socket,
629
+ node.peerAcceptedTlsUpgrade(serverHeaders),
630
+ node.clientAcceptedTlsUpgrade(clientHeaders),
631
+ );
632
+ if (!upgradeToTls) {
633
+ node.attachPeer(socket, false, remoteLabel, role, caps, rest);
634
+ return;
635
+ }
636
+ emitHandshakeDebug(
637
+ node,
638
+ "inbound",
639
+ "tls-upgrade-start",
640
+ remoteLabel,
641
+ "upgrading socket to TLS",
642
+ );
643
+ void node
644
+ .upgradeSocketToTls(socket, "server", rest)
645
+ .then((tlsSocket) => {
646
+ emitHandshakeDebug(
647
+ node,
648
+ "inbound",
649
+ "tls-upgrade-ok",
650
+ remoteLabel,
651
+ "TLS active",
652
+ );
653
+ node.attachPeer(tlsSocket, false, remoteLabel, role, caps);
654
+ })
655
+ .catch((error) => {
656
+ emitHandshakeDebug(
657
+ node,
658
+ "inbound",
659
+ "tls-upgrade-failed",
660
+ remoteLabel,
661
+ errMsg(error),
662
+ );
663
+ node.emitEvent({
664
+ type: "PROBE_REJECTED",
665
+ at: ts(),
666
+ message: `TLS upgrade failed: ${errMsg(error)}`,
667
+ });
668
+ socket.destroy();
669
+ });
670
+ }
671
+
672
+ function parseOutboundHandshakeResult(
673
+ node: GnutellaServent,
674
+ target: string,
675
+ socket: net.Socket,
676
+ buf: Buffer,
677
+ compressionEnabled: boolean,
678
+ ): OutboundHandshakeResult | undefined {
679
+ const raw = buf.toString("latin1");
680
+ const cut = findHeaderEnd(raw);
681
+ if (cut === -1) return undefined;
682
+
683
+ const { startLine, headers } = parseHandshakeBlock(raw.slice(0, cut));
684
+ emitHandshakeBlock(
685
+ node,
686
+ "outbound",
687
+ "response-recv",
688
+ target,
689
+ startLine,
690
+ headers,
691
+ );
692
+ const blockedSignature = blockedClientSignature(headers);
693
+ if (blockedSignature) {
694
+ const message = blockedClientMessage(
695
+ blockedSignature,
696
+ socket.remoteAddress,
697
+ );
698
+ maybeBlockClientHost(node, socket.remoteAddress);
699
+ emitHandshakeDebug(
700
+ node,
701
+ "outbound",
702
+ "blocked-client",
703
+ target,
704
+ message,
705
+ );
706
+ throw new Error(message);
707
+ }
708
+ node.absorbHandshakeHeaders(headers, socket.remoteAddress);
709
+ if (
710
+ /^GNUTELLA OK/i.test(startLine) ||
711
+ /^GNUTELLA\/0\.4 200/i.test(startLine)
712
+ ) {
713
+ throw new Error(
714
+ `unsupported 0.4 handshake response from ${target}: ${describeHandshakeResponse(startLine, headers)}`,
715
+ );
716
+ }
717
+
718
+ const match = /^GNUTELLA\/0\.([0-9]+)\s+(\d+)/i.exec(startLine);
719
+ if (!match) {
720
+ throw new Error(
721
+ `unexpected handshake response from ${target}: ${describeHandshakeResponse(startLine, headers)}`,
722
+ );
723
+ }
724
+
725
+ const code = Number(match[2]);
726
+ if (code !== 200) {
727
+ throw new Error(
728
+ `0.6 handshake rejected by ${target}: ${describeHandshakeResponse(startLine, headers)}`,
729
+ );
730
+ }
731
+
732
+ const finalHeadersWithRemote = node.buildClientFinalHeaders(
733
+ headers,
734
+ socket.remoteAddress,
735
+ );
736
+ const compressIn =
737
+ hasToken(headers["content-encoding"], "deflate") && compressionEnabled;
738
+ const compressOut =
739
+ hasToken(finalHeadersWithRemote["content-encoding"], "deflate") &&
740
+ compressionEnabled;
741
+ const caps = node.buildCapabilities(
742
+ `0.${match[1]}`,
743
+ mergeHeaders(headers, finalHeadersWithRemote),
744
+ compressIn,
745
+ compressOut,
746
+ );
747
+ return {
748
+ caps,
749
+ role: node.classifyPeerRole(caps),
750
+ rest: buf.subarray(cut),
751
+ finalHeadersWithRemote,
752
+ };
753
+ }
754
+
755
+ export function finishInbound06Probe(
756
+ node: GnutellaServent,
757
+ ctx: ProbeCtx,
758
+ ): void {
759
+ const raw = ctx.buf.toString("latin1");
760
+ const cut = findHeaderEnd(raw);
761
+ if (cut === -1) return;
762
+ const { startLine, headers } = parseHandshakeBlock(raw.slice(0, cut));
763
+ node.absorbHandshakeHeaders(headers, ctx.socket.remoteAddress);
764
+ emitHandshakeBlock(
765
+ node,
766
+ "inbound",
767
+ "final-recv",
768
+ handshakePeerLabel(ctx.socket),
769
+ startLine,
770
+ headers,
771
+ );
772
+ if (finalHandshakeCode(startLine) !== 200) {
773
+ throw new Error(`client rejected connection: ${startLine}`);
774
+ }
775
+
776
+ const requestHeaders = ctx.requestHeaders || {};
777
+ const serverHeaders = ctx.serverHeaders || {};
778
+ const compressionEnabled = !!node.config().enableCompression;
779
+ const compressIn = compressionAccepted(compressionEnabled, headers);
780
+ const compressOut = compressionAccepted(
781
+ compressionEnabled,
782
+ serverHeaders,
783
+ );
784
+ const caps = node.buildCapabilities(
785
+ "0.6",
786
+ mergeHeaders(requestHeaders, headers),
787
+ compressIn,
788
+ compressOut,
789
+ );
790
+ const role = node.classifyPeerRole(caps);
791
+ const rest = ctx.buf.subarray(cut);
792
+ ctx.mode = "done";
793
+ clearProbeListeners(ctx);
794
+ const remoteLabel = handshakePeerLabel(ctx.socket);
795
+ attachInbound06Peer(
796
+ node,
797
+ ctx.socket,
798
+ remoteLabel,
799
+ role,
800
+ caps,
801
+ rest,
802
+ serverHeaders,
803
+ headers,
804
+ );
805
+ }
806
+
807
+ export function tryDecideProbe(
808
+ node: GnutellaServent,
809
+ ctx: ProbeCtx,
810
+ ): void {
811
+ if (ctx.mode === "undecided") {
812
+ node.handleUndecidedProbe(ctx);
813
+ return;
814
+ }
815
+ if (ctx.mode === "await-final-0.6") node.finishInbound06Probe(ctx);
816
+ }
817
+
818
+ export async function connectPeer06(
819
+ node: GnutellaServent,
820
+ host: string,
821
+ port: number,
822
+ timeoutMs = node.config().connectTimeoutMs,
823
+ ): Promise<void> {
824
+ const c = node.config();
825
+ const target = normalizePeer(host, port);
826
+ if (node.isBlockedHost(host))
827
+ throw new Error(`peer ${target} is blocked`);
828
+ emitHandshakeDebug(
829
+ node,
830
+ "outbound",
831
+ "dial-start",
832
+ target,
833
+ `timeoutMs=${timeoutMs}`,
834
+ );
835
+ await new Promise<void>((resolve, reject) => {
836
+ const socket = node.createConnection({ host, port });
837
+ socket.setNoDelay(true);
838
+ let decided = false;
839
+ let buf = Buffer.alloc(0);
840
+
841
+ const cleanup = () => {
842
+ socket.off("error", fail);
843
+ socket.off("close", onClose);
844
+ socket.off("connect", onConnect);
845
+ socket.off("data", onData);
846
+ };
847
+
848
+ const fail = (error: unknown) => {
849
+ if (decided) return;
850
+ decided = true;
851
+ cleanup();
852
+ emitHandshakeDebug(
853
+ node,
854
+ "outbound",
855
+ "failed",
856
+ target,
857
+ errMsg(error),
858
+ );
859
+ socket.destroy();
860
+ reject(error instanceof Error ? error : new Error(errMsg(error)));
861
+ };
862
+ socket.setTimeout(timeoutMs, () => fail(new Error("connect timeout")));
863
+
864
+ const onConnect = () => {
865
+ if (node.isBlockedHost(socket.remoteAddress)) {
866
+ fail(
867
+ new Error(`blocked IP ${normalizeIpv4(socket.remoteAddress)}`),
868
+ );
869
+ return;
870
+ }
871
+ const headers = node.baseHandshakeHeaders(socket.remoteAddress);
872
+ if (node.tlsEnabled() && node.canUpgradeSocketToTls(socket))
873
+ headers.upgrade = node.tlsUpgradeToken();
874
+ socket.write(buildHandshakeBlock("GNUTELLA CONNECT/0.6", headers));
875
+ emitHandshakeBlock(
876
+ node,
877
+ "outbound",
878
+ "connect-sent",
879
+ target,
880
+ "GNUTELLA CONNECT/0.6",
881
+ headers,
882
+ );
883
+ };
884
+ const onClose = () =>
885
+ fail(new Error("socket closed during handshake"));
886
+ const onData = (chunk: string | Buffer) => {
887
+ if (decided) return;
888
+ buf = Buffer.concat([buf, toBuffer(chunk)]);
889
+ let result: OutboundHandshakeResult | undefined;
890
+ try {
891
+ result = parseOutboundHandshakeResult(
892
+ node,
893
+ target,
894
+ socket,
895
+ buf,
896
+ !!c.enableCompression,
897
+ );
898
+ } catch (error) {
899
+ fail(error);
900
+ return;
901
+ }
902
+ if (!result) return;
903
+
904
+ const { caps, role, rest, finalHeadersWithRemote } = result;
905
+ const acceptance = node.canAcceptPeerRole(role);
906
+ if (!acceptance.ok) {
907
+ socket.write(
908
+ buildHandshakeBlock(
909
+ `GNUTELLA/0.6 ${acceptance.code} ${acceptance.reason}`,
910
+ {},
911
+ ),
912
+ );
913
+ fail(
914
+ new Error(
915
+ `0.6 handshake rejected by ${target}: ${acceptance.reason}`,
916
+ ),
917
+ );
918
+ return;
919
+ }
920
+ socket.write(
921
+ buildHandshakeBlock("GNUTELLA/0.6 200 OK", finalHeadersWithRemote),
922
+ );
923
+ emitHandshakeBlock(
924
+ node,
925
+ "outbound",
926
+ "final-sent",
927
+ target,
928
+ "GNUTELLA/0.6 200 OK",
929
+ finalHeadersWithRemote,
930
+ );
931
+ decided = true;
932
+ socket.setTimeout(0);
933
+ cleanup();
934
+ const upgradeToTls = shouldUpgradeSocketToTls(
935
+ node,
936
+ socket,
937
+ node.peerAcceptedTlsUpgrade(caps.headers),
938
+ true,
939
+ );
940
+ if (!upgradeToTls) {
941
+ node.attachPeer(socket, true, target, role, caps, rest, target);
942
+ resolve();
943
+ return;
944
+ }
945
+ emitHandshakeDebug(
946
+ node,
947
+ "outbound",
948
+ "tls-upgrade-start",
949
+ target,
950
+ "upgrading socket to TLS",
951
+ );
952
+ void node
953
+ .upgradeSocketToTls(socket, "client", rest)
954
+ .then((tlsSocket) => {
955
+ emitHandshakeDebug(
956
+ node,
957
+ "outbound",
958
+ "tls-upgrade-ok",
959
+ target,
960
+ "TLS active",
961
+ );
962
+ node.attachPeer(
963
+ tlsSocket,
964
+ true,
965
+ target,
966
+ role,
967
+ caps,
968
+ Buffer.alloc(0),
969
+ target,
970
+ );
971
+ resolve();
972
+ })
973
+ .catch((error) => {
974
+ emitHandshakeDebug(
975
+ node,
976
+ "outbound",
977
+ "tls-upgrade-failed",
978
+ target,
979
+ errMsg(error),
980
+ );
981
+ socket.destroy();
982
+ reject(
983
+ error instanceof Error ? error : new Error(errMsg(error)),
984
+ );
985
+ });
986
+ };
987
+ socket.on("error", fail);
988
+ socket.on("close", onClose);
989
+ socket.on("connect", onConnect);
990
+ socket.on("data", onData);
991
+ });
992
+ }