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.
- package/CLI.md +1 -0
- package/gnutella.json.example +1 -0
- package/package.json +4 -3
- package/src/cli_shared.ts +32 -43
- package/src/const.ts +1 -9
- package/src/descriptor_routing/index.ts +17 -0
- package/src/descriptor_routing/pong_cache.ts +32 -0
- package/src/descriptor_routing/response_routes.ts +15 -0
- package/src/descriptor_routing/seen.ts +20 -0
- package/src/descriptor_routing/ttl.ts +37 -0
- package/src/descriptor_routing/types.ts +27 -0
- package/src/gwebcache/bootstrap.ts +21 -58
- package/src/gwebcache/types.ts +6 -10
- package/src/handshake_policy/admission.ts +17 -0
- package/src/handshake_policy/capabilities.ts +167 -0
- package/src/handshake_policy/headers.ts +157 -0
- package/src/handshake_policy/index.ts +21 -0
- package/src/handshake_policy/types.ts +36 -0
- package/src/peer_address.ts +68 -0
- package/src/peer_discovery/candidate_policy.ts +80 -0
- package/src/peer_discovery/index.ts +8 -0
- package/src/peer_discovery/types.ts +26 -0
- package/src/persistence/config_doc.ts +61 -0
- package/src/persistence/index.ts +14 -0
- package/src/persistence/peer_state.ts +113 -0
- package/src/persistence/types.ts +28 -0
- package/src/protocol/codec.ts +27 -67
- package/src/protocol/content_urn.ts +5 -1
- package/src/protocol/file_hash.ts +12 -0
- package/src/protocol/file_server.ts +1 -1
- package/src/protocol/ggep.ts +13 -8
- package/src/protocol/handshake.ts +18 -161
- package/src/protocol/http_download_reader.ts +9 -7
- package/src/protocol/magnet.ts +15 -13
- package/src/protocol/node.ts +1 -1
- package/src/protocol/node_handshake.ts +55 -113
- package/src/protocol/node_protocol_runtime.ts +69 -60
- package/src/protocol/node_qrp_runtime.ts +7 -6
- package/src/protocol/node_query_routing.ts +43 -132
- package/src/protocol/node_state.ts +2 -3
- package/src/protocol/node_topology.ts +38 -82
- package/src/protocol/node_transfer.ts +52 -35
- package/src/protocol/peer_state.ts +36 -207
- package/src/protocol/qrp.ts +1 -549
- package/src/protocol/query_matching.ts +22 -0
- package/src/protocol/share_index.ts +8 -70
- package/src/protocol/share_library.ts +30 -73
- package/src/query_routing/dynamic_query.ts +117 -0
- package/src/query_routing/index.ts +27 -0
- package/src/query_routing/qrp/constants.ts +9 -0
- package/src/query_routing/qrp/hash.ts +27 -0
- package/src/query_routing/qrp/patch_values.ts +29 -0
- package/src/query_routing/qrp/remote_state.ts +98 -0
- package/src/query_routing/qrp/routing.ts +46 -0
- package/src/query_routing/qrp/table.ts +319 -0
- package/src/query_routing/qrp/terms.ts +62 -0
- package/src/query_routing/qrp/types.ts +31 -0
- package/src/query_routing/qrp.ts +13 -0
- package/src/share_catalog/catalog.ts +108 -0
- package/src/share_catalog/index.ts +16 -0
- package/src/share_catalog/keywords.ts +15 -0
- package/src/share_catalog/manifest.ts +81 -0
- package/src/share_catalog/types.ts +43 -0
- package/src/shared.ts +9 -68
- package/src/topology/admission.ts +51 -0
- package/src/topology/classify.ts +19 -0
- package/src/topology/index.ts +17 -0
- package/src/topology/slots.ts +43 -0
- package/src/topology/types.ts +25 -0
- package/src/transfers/index.ts +13 -0
- package/src/transfers/planner.ts +52 -0
- package/src/transfers/ranges.ts +57 -0
- package/src/transfers/results.ts +45 -0
- package/src/transfers/types.ts +43 -0
- 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
|
+
};
|
package/src/protocol/codec.ts
CHANGED
|
@@ -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(
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
):
|
|
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
|
-
|
|
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,
|
package/src/protocol/ggep.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
):
|
|
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 (
|
|
69
|
-
|
|
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;
|
package/src/protocol/magnet.ts
CHANGED
|
@@ -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 || []),
|
package/src/protocol/node.ts
CHANGED
|
@@ -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
|