gnutella 1.0.0 → 1.1.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 (75) hide show
  1. package/CLI.md +1 -0
  2. package/gnutella.json.example +1 -0
  3. package/package.json +4 -3
  4. package/src/cli_shared.ts +32 -43
  5. package/src/const.ts +1 -9
  6. package/src/descriptor_routing/index.ts +17 -0
  7. package/src/descriptor_routing/pong_cache.ts +32 -0
  8. package/src/descriptor_routing/response_routes.ts +15 -0
  9. package/src/descriptor_routing/seen.ts +20 -0
  10. package/src/descriptor_routing/ttl.ts +37 -0
  11. package/src/descriptor_routing/types.ts +27 -0
  12. package/src/gwebcache/bootstrap.ts +21 -58
  13. package/src/gwebcache/types.ts +6 -10
  14. package/src/handshake_policy/admission.ts +17 -0
  15. package/src/handshake_policy/capabilities.ts +167 -0
  16. package/src/handshake_policy/headers.ts +157 -0
  17. package/src/handshake_policy/index.ts +21 -0
  18. package/src/handshake_policy/types.ts +36 -0
  19. package/src/peer_address.ts +68 -0
  20. package/src/peer_discovery/candidate_policy.ts +80 -0
  21. package/src/peer_discovery/index.ts +8 -0
  22. package/src/peer_discovery/types.ts +26 -0
  23. package/src/persistence/config_doc.ts +61 -0
  24. package/src/persistence/index.ts +14 -0
  25. package/src/persistence/peer_state.ts +113 -0
  26. package/src/persistence/types.ts +28 -0
  27. package/src/protocol/codec.ts +27 -67
  28. package/src/protocol/content_urn.ts +5 -1
  29. package/src/protocol/file_hash.ts +12 -0
  30. package/src/protocol/file_server.ts +1 -1
  31. package/src/protocol/ggep.ts +13 -8
  32. package/src/protocol/handshake.ts +18 -161
  33. package/src/protocol/http_download_reader.ts +9 -7
  34. package/src/protocol/magnet.ts +15 -13
  35. package/src/protocol/node.ts +1 -1
  36. package/src/protocol/node_handshake.ts +55 -113
  37. package/src/protocol/node_protocol_runtime.ts +69 -60
  38. package/src/protocol/node_qrp_runtime.ts +7 -6
  39. package/src/protocol/node_query_routing.ts +43 -132
  40. package/src/protocol/node_state.ts +2 -3
  41. package/src/protocol/node_topology.ts +38 -82
  42. package/src/protocol/node_transfer.ts +52 -35
  43. package/src/protocol/peer_state.ts +36 -207
  44. package/src/protocol/qrp.ts +1 -549
  45. package/src/protocol/query_matching.ts +22 -0
  46. package/src/protocol/share_index.ts +8 -70
  47. package/src/protocol/share_library.ts +30 -73
  48. package/src/query_routing/dynamic_query.ts +117 -0
  49. package/src/query_routing/index.ts +27 -0
  50. package/src/query_routing/qrp/constants.ts +9 -0
  51. package/src/query_routing/qrp/hash.ts +27 -0
  52. package/src/query_routing/qrp/patch_values.ts +29 -0
  53. package/src/query_routing/qrp/remote_state.ts +98 -0
  54. package/src/query_routing/qrp/routing.ts +46 -0
  55. package/src/query_routing/qrp/table.ts +319 -0
  56. package/src/query_routing/qrp/terms.ts +62 -0
  57. package/src/query_routing/qrp/types.ts +31 -0
  58. package/src/query_routing/qrp.ts +13 -0
  59. package/src/share_catalog/catalog.ts +108 -0
  60. package/src/share_catalog/index.ts +16 -0
  61. package/src/share_catalog/keywords.ts +15 -0
  62. package/src/share_catalog/manifest.ts +81 -0
  63. package/src/share_catalog/types.ts +43 -0
  64. package/src/shared.ts +9 -68
  65. package/src/topology/admission.ts +51 -0
  66. package/src/topology/classify.ts +19 -0
  67. package/src/topology/index.ts +17 -0
  68. package/src/topology/slots.ts +43 -0
  69. package/src/topology/types.ts +25 -0
  70. package/src/transfers/index.ts +13 -0
  71. package/src/transfers/planner.ts +52 -0
  72. package/src/transfers/ranges.ts +57 -0
  73. package/src/transfers/results.ts +45 -0
  74. package/src/transfers/types.ts +43 -0
  75. package/src/types.ts +43 -55
@@ -0,0 +1,113 @@
1
+ import { MAX_TRACKED_PEERS } from "../const";
2
+ import { normalizeIpv4, normalizePeer, parsePeer } from "../peer_address";
3
+ import { rankPeerCandidatesByLastSeen } from "../peer_discovery";
4
+ import type { PeerState } from "../types";
5
+
6
+ function normalizePeerTimestamp(value: unknown): number {
7
+ const ts = Number(value);
8
+ if (!Number.isFinite(ts)) return 0;
9
+ return Math.max(0, Math.floor(ts));
10
+ }
11
+
12
+ export function normalizePeerState(value: unknown): PeerState {
13
+ if (!value || typeof value !== "object" || Array.isArray(value))
14
+ return {};
15
+
16
+ const out = new Map<string, number>();
17
+ for (const [peerSpec, rawTimestamp] of Object.entries(
18
+ value as Record<string, unknown>,
19
+ )) {
20
+ const addr = parsePeer(peerSpec);
21
+ if (!addr) continue;
22
+ const peer = normalizePeer(addr.host, addr.port);
23
+ const timestamp = normalizePeerTimestamp(rawTimestamp);
24
+ const current = out.get(peer) ?? 0;
25
+ if (!out.has(peer) || timestamp > current) out.set(peer, timestamp);
26
+ }
27
+
28
+ return Object.fromEntries(out);
29
+ }
30
+
31
+ export function sortPeerStateEntries(
32
+ peers: PeerState,
33
+ ): Array<[peer: string, lastSeen: number]> {
34
+ return rankPeerCandidatesByLastSeen(
35
+ Object.entries(normalizePeerState(peers)).map(([peer, lastSeen]) => ({
36
+ peer,
37
+ lastSeen,
38
+ })),
39
+ ).map(({ peer, lastSeen }) => [peer, lastSeen]);
40
+ }
41
+
42
+ export function trimPeerState(
43
+ peers: PeerState,
44
+ limit = MAX_TRACKED_PEERS,
45
+ ): PeerState {
46
+ if (limit <= 0) return {};
47
+ return Object.fromEntries(sortPeerStateEntries(peers).slice(0, limit));
48
+ }
49
+
50
+ export function filterBlockedPeerState(
51
+ peers: PeerState,
52
+ blockedIps: readonly string[],
53
+ ): PeerState {
54
+ const blocked = new Set(
55
+ blockedIps
56
+ .map((entry) => normalizeIpv4(entry))
57
+ .filter((entry): entry is string => !!entry),
58
+ );
59
+ if (!blocked.size) return trimPeerState(peers);
60
+ return Object.fromEntries(
61
+ sortPeerStateEntries(peers).filter(([peer]) => {
62
+ const addr = parsePeer(peer);
63
+ const host = normalizeIpv4(addr?.host);
64
+ return !host || !blocked.has(host);
65
+ }),
66
+ );
67
+ }
68
+
69
+ export function rememberPeerInState(
70
+ peers: PeerState,
71
+ peerSpec: string,
72
+ timestamp = 0,
73
+ ): PeerState {
74
+ const addr = parsePeer(peerSpec);
75
+ if (!addr) return trimPeerState(peers);
76
+
77
+ const peer = normalizePeer(addr.host, addr.port);
78
+ const current = normalizePeerState(peers);
79
+ const existing = current[peer];
80
+ const nextTimestamp = Math.max(
81
+ existing ?? 0,
82
+ normalizePeerTimestamp(timestamp),
83
+ );
84
+ const shouldPromote =
85
+ existing == null ||
86
+ nextTimestamp > existing ||
87
+ (existing === 0 && nextTimestamp === 0);
88
+
89
+ if (!shouldPromote) return trimPeerState(current);
90
+
91
+ return trimPeerState(
92
+ Object.fromEntries([
93
+ [peer, nextTimestamp],
94
+ ...Object.entries(current).filter(
95
+ ([candidate]) => candidate !== peer,
96
+ ),
97
+ ]),
98
+ );
99
+ }
100
+
101
+ export function peerStateTargets(peers: PeerState): string[] {
102
+ return sortPeerStateEntries(peers).map(([peer]) => peer);
103
+ }
104
+
105
+ export function peerStateEquals(a: PeerState, b: PeerState): boolean {
106
+ const aEntries = sortPeerStateEntries(a);
107
+ const bEntries = sortPeerStateEntries(b);
108
+ if (aEntries.length !== bEntries.length) return false;
109
+ return aEntries.every(
110
+ ([peer, lastSeen], index) =>
111
+ bEntries[index]?.[0] === peer && bEntries[index]?.[1] === lastSeen,
112
+ );
113
+ }
@@ -0,0 +1,28 @@
1
+ export type PersistedConfig = {
2
+ listen_ip?: unknown;
3
+ listen_host?: unknown;
4
+ listen_port?: unknown;
5
+ advertised_ip?: unknown;
6
+ advertised_host?: unknown;
7
+ advertised_port?: unknown;
8
+ blocked_ips?: unknown;
9
+ gwebcache_urls?: unknown;
10
+ ultrapeer?: unknown;
11
+ max_connections?: unknown;
12
+ max_ultrapeer_connections?: unknown;
13
+ max_leaf_connections?: unknown;
14
+ max_ttl?: unknown;
15
+ enable_tls?: unknown;
16
+ log_ignore?: unknown;
17
+ data_dir?: unknown;
18
+ };
19
+
20
+ export type PersistedState = {
21
+ servent_id_hex?: unknown;
22
+ peers?: unknown;
23
+ };
24
+
25
+ export type PersistedDoc = {
26
+ config?: PersistedConfig;
27
+ state?: PersistedState;
28
+ };
@@ -1,5 +1,6 @@
1
1
  import { HEADER_LEN } from "../const";
2
2
  import { bytesToIpBE, ipToBytesBE } from "../shared";
3
+ export { parseByteRange } from "../transfers/ranges";
3
4
  import type {
4
5
  QueryDescriptor,
5
6
  QueryHitDescriptor,
@@ -18,11 +19,11 @@ import {
18
19
  bitprintUrnFromGgepHash,
19
20
  firstSha1Urn,
20
21
  normalizeUrnList,
22
+ sha1ToUrn,
21
23
  sha1BufferFromUrn,
22
24
  textUrnFromGgepUrn,
23
25
  } from "./content_urn";
24
26
  import { splitQuerySearch } from "./query_search";
25
- import { sha1ToUrn } from "./qrp";
26
27
 
27
28
  const MODERN_QUERY_FLAG_BITS = [
28
29
  ["requesterFirewalled", 14],
@@ -32,6 +33,26 @@ const MODERN_QUERY_FLAG_BITS = [
32
33
  ["outOfBand", 10],
33
34
  ] as const;
34
35
 
36
+ type QueryExtensionBlocks = {
37
+ textBlocks: Buffer[];
38
+ ggepItems: GgepItem[];
39
+ };
40
+
41
+ type QueryHitDescriptorBlockOptions = {
42
+ vendorCode?: string;
43
+ push: boolean;
44
+ busy?: boolean;
45
+ haveUploaded?: boolean;
46
+ measuredSpeed?: boolean;
47
+ ggep?: boolean;
48
+ privateArea?: Buffer;
49
+ };
50
+
51
+ type HttpDownloadHeader = {
52
+ remaining: number;
53
+ finalStart: number;
54
+ };
55
+
35
56
  function normalizedModernQueryMaxHits(
36
57
  maxHits: number | undefined,
37
58
  ): number {
@@ -61,10 +82,9 @@ function splitFsBlocks(buf: Buffer): Buffer[] {
61
82
  return blocks.filter((x) => x.length > 0);
62
83
  }
63
84
 
64
- function splitTextAndGgepExtensions(rawExtensions: Buffer): {
65
- textBlocks: Buffer[];
66
- ggepItems: GgepItem[];
67
- } {
85
+ function splitTextAndGgepExtensions(
86
+ rawExtensions: Buffer,
87
+ ): QueryExtensionBlocks {
68
88
  const ggepStart = rawExtensions.indexOf(0xc3);
69
89
  if (ggepStart === -1) {
70
90
  return {
@@ -151,15 +171,7 @@ function qhdFlagMeaningful(
151
171
  return !!(enabler & (1 << bit));
152
172
  }
153
173
 
154
- function buildQhdBlock(options: {
155
- vendorCode?: string;
156
- push: boolean;
157
- busy?: boolean;
158
- haveUploaded?: boolean;
159
- measuredSpeed?: boolean;
160
- ggep?: boolean;
161
- privateArea?: Buffer;
162
- }): Buffer {
174
+ function buildQhdBlock(options: QueryHitDescriptorBlockOptions): Buffer {
163
175
  const vendor = Buffer.alloc(4, 0);
164
176
  Buffer.from(
165
177
  (options.vendorCode || DEFAULT_VENDOR_CODE).slice(0, 4).padEnd(4, " "),
@@ -339,58 +351,6 @@ function parseQueryHitResultAt(
339
351
  };
340
352
  }
341
353
 
342
- function parseByteRangeSuffix(
343
- endRaw: string,
344
- size: number,
345
- last: number,
346
- ): { start: number; end: number; partial: boolean } | null {
347
- const suffixLen = Number(endRaw);
348
- if (!Number.isInteger(suffixLen) || suffixLen <= 0) return null;
349
- const length = Math.min(suffixLen, size);
350
- return { start: size - length, end: last, partial: length < size };
351
- }
352
-
353
- function explicitByteRangeEnd(
354
- endRaw: string,
355
- start: number,
356
- size: number,
357
- last: number,
358
- ): number | undefined {
359
- const end = endRaw ? Number(endRaw) : last;
360
- if (!Number.isInteger(end) || end < start) return undefined;
361
- if (size === 0) return -1;
362
- return Math.min(end, last);
363
- }
364
-
365
- function parseByteRangeExplicit(
366
- startRaw: string,
367
- endRaw: string,
368
- size: number,
369
- last: number,
370
- ): { start: number; end: number; partial: boolean } | null {
371
- const start = Number(startRaw);
372
- if (!Number.isInteger(start) || start < 0) return null;
373
- if (size > 0 && start > last) return null;
374
- const end = explicitByteRangeEnd(endRaw, start, size, last);
375
- if (end == null) return null;
376
- return { start, end, partial: size > 0 && (start > 0 || end < last) };
377
- }
378
-
379
- export function parseByteRange(
380
- rangeHeader: string | undefined,
381
- size: number,
382
- ): { start: number; end: number; partial: boolean } | null {
383
- const last = size > 0 ? size - 1 : -1;
384
- if (!rangeHeader) return { start: 0, end: last, partial: false };
385
- const m = /^bytes=(\d*)-(\d*)$/i.exec(rangeHeader.trim());
386
- if (!m) return null;
387
- const startRaw = m[1];
388
- const endRaw = m[2];
389
- if (!startRaw && !endRaw) return null;
390
- if (!startRaw) return parseByteRangeSuffix(endRaw, size, last);
391
- return parseByteRangeExplicit(startRaw, endRaw, size, last);
392
- }
393
-
394
354
  export function buildHeader(
395
355
  descriptorId: Buffer,
396
356
  payloadType: number,
@@ -692,7 +652,7 @@ export function buildUriResRequest(
692
652
  export function parseHttpDownloadHeader(
693
653
  head: string,
694
654
  requestedStart: number,
695
- ): { remaining: number; finalStart: number } {
655
+ ): HttpDownloadHeader {
696
656
  const first = head.replace(/\r\n/g, "\n").split("\n", 1)[0];
697
657
  const match = /^HTTP\/(\d+\.\d+)\s+(\d+)/i.exec(first);
698
658
  if (!match) throw new Error("invalid HTTP response");
@@ -35,7 +35,7 @@ function base32Decode(text: string): Buffer | undefined {
35
35
  return Buffer.from(out);
36
36
  }
37
37
 
38
- export function base32Encode(data: Buffer): string {
38
+ function base32Encode(data: Buffer): string {
39
39
  let result = "";
40
40
  let bits = 0;
41
41
  let value = 0;
@@ -51,6 +51,10 @@ export function base32Encode(data: Buffer): string {
51
51
  return result;
52
52
  }
53
53
 
54
+ export function sha1ToUrn(sha1: Buffer): string {
55
+ return `${SHA1_URN_PREFIX}${base32Encode(sha1)}`;
56
+ }
57
+
54
58
  function normalizeSha1Urn(raw: string): string | undefined {
55
59
  const match = /^urn:sha1:([A-Z2-7]+)$/i.exec(raw.trim());
56
60
  if (!match) return undefined;
@@ -0,0 +1,12 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+
4
+ export async function sha1File(abs: string): Promise<Buffer> {
5
+ return await new Promise<Buffer>((resolve, reject) => {
6
+ const hash = crypto.createHash("sha1");
7
+ const rs = fs.createReadStream(abs);
8
+ rs.on("data", (chunk) => hash.update(chunk));
9
+ rs.on("error", reject);
10
+ rs.on("end", () => resolve(hash.digest()));
11
+ });
12
+ }
@@ -3,8 +3,8 @@ import fsp from "node:fs/promises";
3
3
  import net from "node:net";
4
4
 
5
5
  import { errMsg } from "../shared";
6
+ import { parseByteRange } from "../transfers";
6
7
  import type { ShareFile } from "../types";
7
- import { parseByteRange } from "./codec";
8
8
  import { hasToken, parseHttpHeaders, socketCanEnd } from "./handshake";
9
9
  import {
10
10
  handleBrowseHostGet,
@@ -15,6 +15,17 @@ export type GgepItem = {
15
15
  data: Buffer;
16
16
  };
17
17
 
18
+ type GgepLengthCursor = {
19
+ length: number;
20
+ nextOffset: number;
21
+ };
22
+
23
+ type ParsedGgepItem = {
24
+ item: GgepItem;
25
+ nextOffset: number;
26
+ last: boolean;
27
+ };
28
+
18
29
  function hasZeroByte(data: Buffer): boolean {
19
30
  return data.includes(0);
20
31
  }
@@ -73,10 +84,7 @@ function encodeLength(length: number): Buffer {
73
84
  return Buffer.from(bytes);
74
85
  }
75
86
 
76
- function decodeLength(
77
- raw: Buffer,
78
- start: number,
79
- ): { length: number; nextOffset: number } {
87
+ function decodeLength(raw: Buffer, start: number): GgepLengthCursor {
80
88
  let offset = start;
81
89
  let length = 0;
82
90
  for (let i = 0; i < 3; i++) {
@@ -99,10 +107,7 @@ function maybeDeflateDecode(data: Buffer, flags: number): Buffer {
99
107
  return zlib.inflateRawSync(data);
100
108
  }
101
109
 
102
- function parseGgepItem(
103
- raw: Buffer,
104
- start: number,
105
- ): { item: GgepItem; nextOffset: number; last: boolean } {
110
+ function parseGgepItem(raw: Buffer, start: number): ParsedGgepItem {
106
111
  let offset = start;
107
112
  const flags = raw[offset++];
108
113
  if (flags == null) throw new Error("truncated GGEP flags");
@@ -1,170 +1,27 @@
1
1
  import net from "node:net";
2
2
 
3
- import {
4
- CANONICAL_HEADER_NAMES,
5
- INTERESTING_HANDSHAKE_HEADERS,
6
- } from "../const";
7
- import {
8
- ipv4Subnet16,
9
- isRoutableIpv4,
10
- normalizeIpv4,
11
- parsePeer,
12
- } from "../shared";
13
- import type { PeerAddr } from "../types";
14
-
15
- export function hasToken(
16
- value: string | undefined,
17
- token: string,
18
- ): boolean {
19
- if (!value) return false;
20
- return value
21
- .split(",")
22
- .map((x) => x.trim().toLowerCase())
23
- .includes(token.toLowerCase());
24
- }
25
-
26
- export function parseBoolHeader(
27
- v: string | undefined,
28
- ): boolean | undefined {
29
- if (v == null) return undefined;
30
- const s = v.trim().toLowerCase();
31
- if (s === "true") return true;
32
- if (s === "false") return false;
33
- return undefined;
34
- }
35
-
36
- export function parsePositiveIntHeader(
37
- v: string | undefined,
38
- ): number | undefined {
39
- if (!v) return undefined;
40
- const n = Number(v.trim());
41
- return Number.isInteger(n) && n > 0 ? n : undefined;
42
- }
43
-
44
- export function parsePeerHeaderList(v: string | undefined): PeerAddr[] {
45
- if (!v) return [];
46
- return v
47
- .split(",")
48
- .map((x) => parsePeer(x.trim()))
49
- .filter((x): x is PeerAddr => !!x);
50
- }
51
-
52
- export function parseListenIpHeader(
53
- v: string | undefined,
54
- ): PeerAddr | undefined {
55
- if (!v) return undefined;
56
- return parsePeer(v.trim()) || undefined;
57
- }
58
-
59
- function parseRemoteIpHeader(v: string | undefined): string | undefined {
60
- if (!v) return undefined;
61
- return normalizeIpv4(v.split(",")[0]?.trim());
62
- }
63
-
64
- export function lowerCaseHeaders(
65
- headers: Record<string, string>,
66
- ): Record<string, string> {
67
- const out: Record<string, string> = {};
68
- for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;
69
- return out;
70
- }
71
-
72
- export function mergeHeaders(
73
- ...parts: Array<Record<string, string> | undefined>
74
- ): Record<string, string> {
75
- const out: Record<string, string> = {};
76
- for (const part of parts) {
77
- if (!part) continue;
78
- for (const [k, v] of Object.entries(part)) out[k.toLowerCase()] = v;
79
- }
80
- return out;
81
- }
82
-
83
- export function findHeaderEnd(raw: string): number {
84
- const crlf = raw.indexOf("\r\n\r\n");
85
- const lf = raw.indexOf("\n\n");
86
- if (crlf !== -1 && (lf === -1 || crlf < lf)) return crlf + 4;
87
- if (lf !== -1) return lf + 2;
88
- return -1;
89
- }
90
-
91
- function appendContinuationLine(
92
- headers: Record<string, string>,
93
- current: string,
94
- line: string,
95
- ): boolean {
96
- if (!/^[ \t]/.test(line) || !current) return false;
97
- headers[current] = `${headers[current]} ${line.trim()}`.trim();
98
- return true;
99
- }
100
-
101
- function recordHeaderLine(
102
- headers: Record<string, string>,
103
- line: string,
104
- ): string {
105
- const idx = line.indexOf(":");
106
- if (idx === -1) return "";
107
- const key = line.slice(0, idx).trim().toLowerCase();
108
- const value = line.slice(idx + 1).trim();
109
- headers[key] = headers[key] ? `${headers[key]},${value}` : value;
110
- return key;
111
- }
112
-
113
- export function parseHandshakeBlock(raw: string): {
114
- startLine: string;
115
- headers: Record<string, string>;
116
- } {
117
- const end = findHeaderEnd(raw);
118
- const text = end === -1 ? raw : raw.slice(0, end);
119
- const lines = text.replace(/\r\n/g, "\n").split("\n");
120
- const startLine = lines.shift()?.trim() || "";
121
- const headers: Record<string, string> = {};
122
- let current = "";
123
-
124
- for (const line of lines) {
125
- if (!line) continue;
126
- if (appendContinuationLine(headers, current, line)) continue;
127
- current = recordHeaderLine(headers, line);
128
- }
129
- return { startLine, headers };
130
- }
131
-
132
- function canonicalHeaderName(key: string): string {
133
- return CANONICAL_HEADER_NAMES[key.toLowerCase()] || key;
134
- }
135
-
136
- export function buildHandshakeBlock(
137
- startLine: string,
138
- headers: Record<string, string>,
139
- ): Buffer {
140
- const lines = [startLine];
141
- for (const [k, v] of Object.entries(headers)) {
142
- lines.push(`${canonicalHeaderName(k)}: ${v}`);
143
- }
144
- lines.push("", "");
145
- return Buffer.from(lines.join("\r\n"), "latin1");
146
- }
147
-
148
- function summarizeHandshakeHeaders(
149
- headers: Record<string, string>,
150
- ): string {
151
- const parts = INTERESTING_HANDSHAKE_HEADERS.filter(
152
- (key) => !!headers[key],
153
- ).map((key) => `${canonicalHeaderName(key)}=${headers[key]}`);
154
- return parts.length ? ` [${parts.join("; ")}]` : "";
155
- }
156
-
157
- export function describeHandshakeResponse(
158
- startLine: string,
159
- headers: Record<string, string>,
160
- ): string {
161
- return `${startLine}${summarizeHandshakeHeaders(headers)}`;
162
- }
3
+ import { ipv4Subnet16, isRoutableIpv4, normalizeIpv4 } from "../shared";
4
+ import { parseRemoteIpHeader } from "../handshake_policy";
5
+
6
+ type ObservedAdvertisedHost = {
7
+ observedHost: string;
8
+ subnet: string;
9
+ };
10
+
11
+ export {
12
+ buildHandshakeBlock,
13
+ describeHandshakeResponse,
14
+ findHeaderEnd,
15
+ hasToken,
16
+ mergeHeaders,
17
+ parseHandshakeBlock,
18
+ parsePeerHeaderList,
19
+ } from "../handshake_policy";
163
20
 
164
21
  export function observedAdvertisedHostCandidate(
165
22
  headers: Record<string, string>,
166
23
  reporterHost?: string,
167
- ): { observedHost: string; subnet: string } | undefined {
24
+ ): ObservedAdvertisedHost | undefined {
168
25
  const observedHost = parseRemoteIpHeader(
169
26
  headers["remote-ip"] || headers["x-remote-ip"],
170
27
  );
@@ -1,4 +1,8 @@
1
1
  import { errMsg } from "../shared";
2
+ import {
3
+ buildHttpDownloadResult,
4
+ httpDownloadEndDecision,
5
+ } from "../transfers";
2
6
  import type { HttpDownloadState } from "./node_types";
3
7
 
4
8
  type HttpDownloadSourceHandlers = {
@@ -65,8 +69,10 @@ export async function readHttpDownloadSource({
65
69
  if (state.headerDone && state.remaining === 0) finish();
66
70
  },
67
71
  onEnd: () => {
68
- if (!done && state.headerDone && state.remaining === 0) finish();
69
- else if (!done) fail(new Error(incompleteMessage));
72
+ if (done) return;
73
+ const decision = httpDownloadEndDecision(state, incompleteMessage);
74
+ if (decision.kind === "complete") finish();
75
+ else fail(new Error(decision.message));
70
76
  },
71
77
  onError: (error) => fail(error),
72
78
  });
@@ -97,11 +103,7 @@ export async function readHttpDownloadSource({
97
103
  if (done) return;
98
104
  done = true;
99
105
  cleanup();
100
- const meta = {
101
- destPath,
102
- bytes: state.finalStart + state.bodyBytes,
103
- label,
104
- };
106
+ const meta = buildHttpDownloadResult(state, destPath, label);
105
107
  if (!state.ws) {
106
108
  resolve(meta);
107
109
  return;
@@ -11,6 +11,19 @@ type ParsedMagnet = {
11
11
  alternateSources: string[];
12
12
  };
13
13
 
14
+ type ParsedMagnetParams = {
15
+ uri: string;
16
+ params: URLSearchParams;
17
+ };
18
+
19
+ type MagnetBuildInput = {
20
+ fileName?: string;
21
+ fileSize?: number;
22
+ search?: string;
23
+ urns?: string[];
24
+ sha1Urn?: string;
25
+ };
26
+
14
27
  type MagnetFields = {
15
28
  xt: string[];
16
29
  xs: string[];
@@ -58,12 +71,7 @@ function pickFirst(values: string[]): string | undefined {
58
71
  return values.find((value) => value.length > 0);
59
72
  }
60
73
 
61
- function parseMagnetParams(raw: string):
62
- | {
63
- uri: string;
64
- params: URLSearchParams;
65
- }
66
- | undefined {
74
+ function parseMagnetParams(raw: string): ParsedMagnetParams | undefined {
67
75
  const uri = raw.trim();
68
76
  if (!/^magnet:\?/i.test(uri)) return undefined;
69
77
  return {
@@ -155,13 +163,7 @@ export function parseMagnetUri(raw: string): ParsedMagnet | undefined {
155
163
  };
156
164
  }
157
165
 
158
- export function buildMagnetUri(input: {
159
- fileName?: string;
160
- fileSize?: number;
161
- search?: string;
162
- urns?: string[];
163
- sha1Urn?: string;
164
- }): string {
166
+ export function buildMagnetUri(input: MagnetBuildInput): string {
165
167
  const params = new URLSearchParams();
166
168
  const urns = normalizeUrnList([
167
169
  ...(input.urns || []),
@@ -7,6 +7,7 @@ import {
7
7
  reportSelfToGWebCaches,
8
8
  type GWebCacheBootstrapState,
9
9
  } from "../gwebcache_client";
10
+ import { QrpTable } from "../query_routing/qrp";
10
11
  import { sleep } from "../shared";
11
12
  import type {
12
13
  ConfigDoc,
@@ -39,7 +40,6 @@ import * as topology from "./node_topology";
39
40
  import * as transfer from "./node_transfer";
40
41
  import type { ShareIndexEntry } from "./share_index";
41
42
  import type { Peer } from "./node_types";
42
- import { QrpTable } from "./qrp";
43
43
 
44
44
  type BoundMethods<T extends Record<string, unknown>> = {
45
45
  [K in keyof T as T[K] extends (...args: infer AllArgs) => unknown