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,491 @@
1
+ import { normalizePeer, parsePeer } from "../shared";
2
+ import {
3
+ DEFAULT_MAX_BOOTSTRAP_CACHES,
4
+ DEFAULT_MAX_BOOTSTRAP_PEERS,
5
+ DEFAULT_MAX_CACHES,
6
+ DEFAULT_MAX_PEERS,
7
+ aliveCachesForState,
8
+ normalizeGWebCachePeer,
9
+ rememberAliveCaches,
10
+ seedCacheList,
11
+ } from "./shared";
12
+ import {
13
+ describeHttpError,
14
+ describeUpdateError,
15
+ requestGWebCache,
16
+ } from "./response";
17
+ import type {
18
+ BootstrapOptions,
19
+ BootstrapPeer,
20
+ BootstrapResult,
21
+ ConnectBootstrapOptions,
22
+ ConnectBootstrapResult,
23
+ GWebCacheBootstrapState,
24
+ GWebCacheHttpResponse,
25
+ GWebCacheRequestOptions,
26
+ ReportSelfOptions,
27
+ ReportSelfResult,
28
+ } from "./types";
29
+
30
+ function normalizeBootstrapPeers(
31
+ peers: readonly string[],
32
+ isSelfPeer?: (host: string, port: number) => boolean,
33
+ ): BootstrapPeer[] {
34
+ const out: BootstrapPeer[] = [];
35
+ const seen = new Set<string>();
36
+
37
+ for (const peer of peers) {
38
+ const parsed = parsePeer(peer);
39
+ if (!parsed) continue;
40
+ if (isSelfPeer?.(parsed.host, parsed.port)) continue;
41
+
42
+ const normalized = normalizePeer(parsed.host, parsed.port);
43
+ if (seen.has(normalized)) continue;
44
+ seen.add(normalized);
45
+ out.push({
46
+ host: parsed.host,
47
+ port: parsed.port,
48
+ peer: normalized,
49
+ });
50
+ }
51
+
52
+ return out;
53
+ }
54
+
55
+ function bootstrapPeerSetKey(peers: BootstrapPeer[]): string {
56
+ return peers
57
+ .map((peer) => peer.peer)
58
+ .sort((a, b) => a.localeCompare(b))
59
+ .join(",");
60
+ }
61
+
62
+ function buildReportReferenceUrl(
63
+ cache: string,
64
+ knownAliveCaches: readonly string[],
65
+ seedCaches: readonly string[],
66
+ fallbackCache: string,
67
+ ): string {
68
+ return (
69
+ knownAliveCaches.find((candidate) => candidate !== cache) ||
70
+ seedCaches.find((candidate) => candidate !== cache) ||
71
+ fallbackCache
72
+ );
73
+ }
74
+
75
+ async function reportSelfToCache(
76
+ cache: string,
77
+ peer: string,
78
+ referenceUrl: string,
79
+ options: ReportSelfOptions,
80
+ ): Promise<{ reported: boolean; message?: string }> {
81
+ try {
82
+ const result = await requestGWebCache(cache, {
83
+ mode: "update",
84
+ client: options.client,
85
+ version: options.version,
86
+ spec: 2,
87
+ ip: peer,
88
+ url: referenceUrl,
89
+ cluster: options.cluster,
90
+ leafCount: options.leafCount,
91
+ maxLeaves: options.maxLeaves,
92
+ uptimeSec: options.uptimeSec,
93
+ timeoutMs: options.timeoutMs,
94
+ signal: options.signal,
95
+ fetchImpl: options.fetchImpl,
96
+ });
97
+ if (result.ok && result.spec && result.update?.ok)
98
+ return { reported: true };
99
+ return { reported: false, message: describeUpdateError(result) };
100
+ } catch (error) {
101
+ return {
102
+ reported: false,
103
+ message: error instanceof Error ? error.message : String(error),
104
+ };
105
+ }
106
+ }
107
+
108
+ function emptyConnectBootstrapResult(
109
+ attemptedPeers: string[],
110
+ ): ConnectBootstrapResult {
111
+ return {
112
+ attemptedPeers,
113
+ fetchedFromCaches: false,
114
+ addedPeers: [],
115
+ queriedCaches: [],
116
+ errors: [],
117
+ };
118
+ }
119
+
120
+ function exhaustedBootstrapPeerSet(
121
+ candidates: BootstrapPeer[],
122
+ initialAttempt: Awaited<ReturnType<typeof connectBootstrapPeerSet>>,
123
+ ): boolean {
124
+ return (
125
+ candidates.length === 0 ||
126
+ initialAttempt.attemptedPeers.length >= candidates.length
127
+ );
128
+ }
129
+
130
+ function shouldSkipCacheBootstrap(
131
+ candidates: BootstrapPeer[],
132
+ initialAttempt: Awaited<ReturnType<typeof connectBootstrapPeerSet>>,
133
+ candidateKey: string,
134
+ state: GWebCacheBootstrapState | undefined,
135
+ ): boolean {
136
+ if (!exhaustedBootstrapPeerSet(candidates, initialAttempt)) return true;
137
+ if (state?.active) return true;
138
+ return state?.lastExhaustedPeerSet === candidateKey;
139
+ }
140
+
141
+ function buildBootstrapFetchOptions(
142
+ options: ConnectBootstrapOptions,
143
+ ): BootstrapOptions {
144
+ return {
145
+ caches: options.caches,
146
+ client: options.client,
147
+ version: options.version,
148
+ network: options.network,
149
+ timeoutMs: options.timeoutMs,
150
+ maxPeers: options.maxBootstrapPeers || DEFAULT_MAX_BOOTSTRAP_PEERS,
151
+ maxCaches: options.maxBootstrapCaches || DEFAULT_MAX_BOOTSTRAP_CACHES,
152
+ queryAll: true,
153
+ signal: options.signal,
154
+ fetchImpl: options.fetchImpl,
155
+ };
156
+ }
157
+
158
+ function addDiscoveredBootstrapPeers(
159
+ peers: BootstrapPeer[],
160
+ knownPeers: Set<string>,
161
+ addPeer: ConnectBootstrapOptions["addPeer"],
162
+ ): string[] {
163
+ const addedPeers: string[] = [];
164
+ for (const peer of peers) {
165
+ knownPeers.add(peer.peer);
166
+ addedPeers.push(peer.peer);
167
+ addPeer?.(peer.peer);
168
+ }
169
+ return addedPeers;
170
+ }
171
+
172
+ async function fetchAndRetryBootstrapPeers(
173
+ knownPeers: Set<string>,
174
+ options: ConnectBootstrapOptions,
175
+ ): Promise<{
176
+ retryAttempt: Awaited<ReturnType<typeof connectBootstrapPeerSet>>;
177
+ addedPeers: string[];
178
+ queriedCaches: string[];
179
+ errors: BootstrapResult["errors"];
180
+ }> {
181
+ const bootstrap = await fetchBootstrapData(
182
+ buildBootstrapFetchOptions(options),
183
+ );
184
+ rememberAliveCaches(options.state, bootstrap.successfulCaches);
185
+ const discovered = normalizeBootstrapPeers(
186
+ bootstrap.peers,
187
+ options.isSelfPeer,
188
+ ).filter((peer) => !knownPeers.has(peer.peer));
189
+ const addedPeers = addDiscoveredBootstrapPeers(
190
+ discovered,
191
+ knownPeers,
192
+ options.addPeer,
193
+ );
194
+ return {
195
+ retryAttempt: await connectBootstrapPeerSet(discovered, options),
196
+ addedPeers,
197
+ queriedCaches: bootstrap.queriedCaches,
198
+ errors: bootstrap.errors,
199
+ };
200
+ }
201
+
202
+ async function connectBootstrapPeerSet(
203
+ peers: BootstrapPeer[],
204
+ options: Pick<
205
+ ConnectBootstrapOptions,
206
+ | "availableSlots"
207
+ | "connectConcurrency"
208
+ | "connectPeer"
209
+ | "connectTimeoutMs"
210
+ >,
211
+ ): Promise<{
212
+ attemptedPeers: string[];
213
+ successCount: number;
214
+ }> {
215
+ const availableSlots = Math.max(0, options.availableSlots());
216
+ const workerCount = Math.min(
217
+ Math.max(1, options.connectConcurrency),
218
+ availableSlots,
219
+ peers.length,
220
+ );
221
+ if (!workerCount) return { attemptedPeers: [], successCount: 0 };
222
+
223
+ const attemptedPeers: string[] = [];
224
+ let successCount = 0;
225
+ let next = 0;
226
+
227
+ const dialNext = async (): Promise<void> => {
228
+ while (next < peers.length) {
229
+ if (options.availableSlots() <= 0) return;
230
+ const peer = peers[next++];
231
+ attemptedPeers.push(peer.peer);
232
+ try {
233
+ await options.connectPeer(
234
+ peer.host,
235
+ peer.port,
236
+ options.connectTimeoutMs,
237
+ );
238
+ successCount += 1;
239
+ } catch {
240
+ // Keep walking until the bootstrap list is exhausted.
241
+ }
242
+ }
243
+ };
244
+
245
+ await Promise.all(Array.from({ length: workerCount }, () => dialNext()));
246
+ return { attemptedPeers, successCount };
247
+ }
248
+
249
+ function mergeBootstrapResponse(
250
+ result: BootstrapResult | GWebCacheHttpResponse,
251
+ peers: Set<string>,
252
+ caches: Set<string>,
253
+ maxPeers: number,
254
+ maxCaches: number,
255
+ ): void {
256
+ for (const peer of result.peers) {
257
+ if (peers.size >= maxPeers) break;
258
+ peers.add(peer);
259
+ }
260
+ for (const cache of result.caches) {
261
+ if (caches.size >= maxCaches) break;
262
+ caches.add(cache);
263
+ }
264
+ }
265
+
266
+ function buildBootstrapRequestOptions(
267
+ options: BootstrapOptions,
268
+ ): GWebCacheRequestOptions {
269
+ return {
270
+ mode: "get",
271
+ network: options.network || "gnutella",
272
+ client: options.client,
273
+ version: options.version,
274
+ timeoutMs: options.timeoutMs,
275
+ signal: options.signal,
276
+ fetchImpl: options.fetchImpl,
277
+ };
278
+ }
279
+
280
+ function bootstrapResponseError(
281
+ result: GWebCacheHttpResponse,
282
+ ): string | undefined {
283
+ if (!result.ok) return describeHttpError(result);
284
+ if (!result.spec) return "unexpected non-spec2 gwebcache response";
285
+ return undefined;
286
+ }
287
+
288
+ async function queryBootstrapCache(
289
+ cache: string,
290
+ options: BootstrapOptions,
291
+ ): Promise<{ result?: GWebCacheHttpResponse; error?: string }> {
292
+ try {
293
+ return {
294
+ result: await requestGWebCache(
295
+ cache,
296
+ buildBootstrapRequestOptions(options),
297
+ ),
298
+ };
299
+ } catch (error) {
300
+ return {
301
+ error: error instanceof Error ? error.message : String(error),
302
+ };
303
+ }
304
+ }
305
+
306
+ export async function fetchBootstrapData(
307
+ options: BootstrapOptions = {},
308
+ ): Promise<BootstrapResult> {
309
+ const seedCaches = seedCacheList(options.caches);
310
+ const maxPeers = Math.max(1, options.maxPeers ?? DEFAULT_MAX_PEERS);
311
+ const maxCaches = Math.max(1, options.maxCaches ?? DEFAULT_MAX_CACHES);
312
+ const peers = new Set<string>();
313
+ const caches = new Set<string>();
314
+ const successfulCaches = new Set<string>();
315
+ const queriedCaches: string[] = [];
316
+ const errors: BootstrapResult["errors"] = [];
317
+
318
+ for (const cache of seedCaches) {
319
+ if (!options.queryAll && peers.size >= maxPeers) break;
320
+ queriedCaches.push(cache);
321
+ const outcome = await queryBootstrapCache(cache, options);
322
+ if (outcome.error) {
323
+ errors.push({ cache, message: outcome.error });
324
+ continue;
325
+ }
326
+ const result = outcome.result!;
327
+ mergeBootstrapResponse(result, peers, caches, maxPeers, maxCaches);
328
+ const message = bootstrapResponseError(result);
329
+ if (message) {
330
+ errors.push({ cache, message });
331
+ continue;
332
+ }
333
+ successfulCaches.add(cache);
334
+ }
335
+
336
+ return {
337
+ peers: [...peers],
338
+ caches: [...caches],
339
+ queriedCaches,
340
+ successfulCaches: [...successfulCaches],
341
+ errors,
342
+ };
343
+ }
344
+
345
+ export async function getMorePeers(
346
+ options: BootstrapOptions = {},
347
+ ): Promise<string[]> {
348
+ const result = await fetchBootstrapData(options);
349
+ return result.peers;
350
+ }
351
+
352
+ export async function reportSelfToGWebCaches(
353
+ options: ReportSelfOptions,
354
+ ): Promise<ReportSelfResult> {
355
+ const peer = normalizeGWebCachePeer(options.ip);
356
+ if (!peer)
357
+ throw new Error(`invalid gwebcache peer update: ${options.ip}`);
358
+
359
+ const seedCaches = seedCacheList(options.caches);
360
+ const errors: BootstrapResult["errors"] = [];
361
+ const reportedCaches: string[] = [];
362
+ const attemptedCaches: string[] = [];
363
+
364
+ let knownAliveCaches = aliveCachesForState(options.state);
365
+ const referenceCache = knownAliveCaches[0] || seedCaches[0];
366
+ if (!referenceCache) {
367
+ return {
368
+ referenceCache: undefined,
369
+ attemptedCaches,
370
+ reportedCaches,
371
+ errors,
372
+ };
373
+ }
374
+
375
+ for (const cache of seedCaches) {
376
+ attemptedCaches.push(cache);
377
+ const result = await reportSelfToCache(
378
+ cache,
379
+ peer,
380
+ buildReportReferenceUrl(
381
+ cache,
382
+ knownAliveCaches,
383
+ seedCaches,
384
+ referenceCache,
385
+ ),
386
+ options,
387
+ );
388
+ if (result.reported) {
389
+ reportedCaches.push(cache);
390
+ rememberAliveCaches(options.state, [cache]);
391
+ knownAliveCaches = aliveCachesForState(options.state);
392
+ continue;
393
+ }
394
+ if (result.message) errors.push({ cache, message: result.message });
395
+ }
396
+
397
+ return {
398
+ referenceCache,
399
+ attemptedCaches,
400
+ reportedCaches,
401
+ errors,
402
+ };
403
+ }
404
+
405
+ function bootstrapSatisfied(
406
+ successCount: number,
407
+ connectedCount: number,
408
+ ): boolean {
409
+ return successCount > 0 || connectedCount > 0;
410
+ }
411
+
412
+ function clearExhaustedPeerSet(
413
+ state: GWebCacheBootstrapState | undefined,
414
+ ): void {
415
+ if (state) state.lastExhaustedPeerSet = undefined;
416
+ }
417
+
418
+ function startCacheBootstrap(
419
+ state: GWebCacheBootstrapState | undefined,
420
+ candidateKey: string,
421
+ ): void {
422
+ if (!state) return;
423
+ state.active = true;
424
+ state.lastExhaustedPeerSet = candidateKey;
425
+ }
426
+
427
+ function finishCacheBootstrap(
428
+ state: GWebCacheBootstrapState | undefined,
429
+ ): void {
430
+ if (state) state.active = false;
431
+ }
432
+
433
+ export async function connectBootstrapPeers(
434
+ options: ConnectBootstrapOptions,
435
+ ): Promise<ConnectBootstrapResult> {
436
+ const candidates = normalizeBootstrapPeers(
437
+ options.peers,
438
+ options.isSelfPeer,
439
+ );
440
+ const candidateKey = bootstrapPeerSetKey(candidates);
441
+ const knownPeers = new Set(candidates.map((peer) => peer.peer));
442
+ const initialAttempt = await connectBootstrapPeerSet(
443
+ candidates,
444
+ options,
445
+ );
446
+
447
+ if (
448
+ bootstrapSatisfied(
449
+ initialAttempt.successCount,
450
+ options.connectedCount(),
451
+ )
452
+ ) {
453
+ clearExhaustedPeerSet(options.state);
454
+ return emptyConnectBootstrapResult(initialAttempt.attemptedPeers);
455
+ }
456
+ if (
457
+ shouldSkipCacheBootstrap(
458
+ candidates,
459
+ initialAttempt,
460
+ candidateKey,
461
+ options.state,
462
+ )
463
+ ) {
464
+ return emptyConnectBootstrapResult(initialAttempt.attemptedPeers);
465
+ }
466
+
467
+ startCacheBootstrap(options.state, candidateKey);
468
+ try {
469
+ const retry = await fetchAndRetryBootstrapPeers(knownPeers, options);
470
+ if (
471
+ bootstrapSatisfied(
472
+ retry.retryAttempt.successCount,
473
+ options.connectedCount(),
474
+ )
475
+ ) {
476
+ clearExhaustedPeerSet(options.state);
477
+ }
478
+ return {
479
+ attemptedPeers: [
480
+ ...initialAttempt.attemptedPeers,
481
+ ...retry.retryAttempt.attemptedPeers,
482
+ ],
483
+ fetchedFromCaches: true,
484
+ addedPeers: retry.addedPeers,
485
+ queriedCaches: retry.queriedCaches,
486
+ errors: retry.errors,
487
+ };
488
+ } finally {
489
+ finishCacheBootstrap(options.state);
490
+ }
491
+ }