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
package/CLI.md
CHANGED
|
@@ -61,6 +61,7 @@ GnutellaBun keeps both settings and remembered state in the same JSON file.
|
|
|
61
61
|
| `config.ultrapeer` | Set `true` if you want GnutellaBun to behave like a larger relay-style node. Leave `false` for a lighter client. |
|
|
62
62
|
| `config.max_ultrapeer_connections` | Cap for ultrapeer-to-ultrapeer links. |
|
|
63
63
|
| `config.max_leaf_connections` | Cap for leaf links. |
|
|
64
|
+
| `config.max_ttl` | Maximum descriptor TTL to advertise and relay. Defaults to `4`. |
|
|
64
65
|
| `config.log_ignore` | Event categories to hide when `monitor` is enabled. |
|
|
65
66
|
|
|
66
67
|
### Saved State
|
package/gnutella.json.example
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gnutella",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "GnutellaBun is a small Gnutella client for Bun, usable as a CLI or TypeScript library.",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./src/protocol.ts",
|
|
13
13
|
"./types": "./src/types.ts",
|
|
14
|
-
"./gwebcache": "./src/gwebcache_client.ts"
|
|
14
|
+
"./gwebcache": "./src/gwebcache_client.ts",
|
|
15
|
+
"./query-routing": "./src/query_routing/index.ts"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"bin",
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
"test:integration": "bun test tests/integration/protocol.test.ts",
|
|
66
67
|
"test:unit": "bun test tests/unit",
|
|
67
68
|
"typecheck": "tsc --noEmit",
|
|
68
|
-
"unused-exports": "ts-unused-exports tsconfig.json --excludePathsFromReport=src/protocol.ts --exitWithCount",
|
|
69
|
+
"unused-exports": "ts-unused-exports tsconfig.json \"--excludePathsFromReport=src/protocol.ts;src/query_routing/index.ts\" --exitWithCount",
|
|
69
70
|
"fix": "./scripts/fix-all.sh",
|
|
70
71
|
"verify": "./scripts/verify-all.sh"
|
|
71
72
|
}
|
package/src/cli_shared.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { errMsg } from "./shared";
|
|
2
|
-
import { RESULT_NAME_WIDTH_MAX } from "./const";
|
|
3
2
|
import type { CliNode, ParsedCli } from "./types";
|
|
4
3
|
import { buildMagnetUri } from "./protocol/magnet";
|
|
5
4
|
|
|
6
5
|
const SIZE_FORMAT = new Intl.NumberFormat("en-US", {
|
|
6
|
+
minimumFractionDigits: 1,
|
|
7
7
|
maximumFractionDigits: 1,
|
|
8
|
+
useGrouping: false,
|
|
8
9
|
});
|
|
9
|
-
const SIZE_UNITS = ["
|
|
10
|
+
const SIZE_UNITS = ["b", "kb", "mb", "gb", "tb", "pb"];
|
|
11
|
+
const SIZE_VALUE_WIDTH = 5;
|
|
12
|
+
const SIZE_UNIT_WIDTH = 2;
|
|
10
13
|
const RESULT_COUNT_DISPLAY_MAX = 999;
|
|
11
14
|
const PEER_TABLE_WIDTH_MAX = 80;
|
|
12
15
|
|
|
13
16
|
type ResultInfo = ReturnType<CliNode["getResults"]>[number];
|
|
17
|
+
type FormattedSize = { value: string; unit: string };
|
|
14
18
|
|
|
15
|
-
function
|
|
19
|
+
function formattedSize(bytes: number): FormattedSize {
|
|
16
20
|
const safeBytes =
|
|
17
21
|
Number.isFinite(bytes) && bytes > 0 ? Math.floor(bytes) : 0;
|
|
18
22
|
let value = safeBytes;
|
|
@@ -21,7 +25,20 @@ function formatSize(bytes: number): string {
|
|
|
21
25
|
value /= 1024;
|
|
22
26
|
unitIndex++;
|
|
23
27
|
}
|
|
24
|
-
return
|
|
28
|
+
return {
|
|
29
|
+
value: unitIndex === 0 ? String(value) : SIZE_FORMAT.format(value),
|
|
30
|
+
unit: SIZE_UNITS[unitIndex],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatSize(bytes: number): string {
|
|
35
|
+
const size = formattedSize(bytes);
|
|
36
|
+
return `${size.value} ${size.unit}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatResultSize(bytes: number): string {
|
|
40
|
+
const size = formattedSize(bytes);
|
|
41
|
+
return `${size.value.padStart(SIZE_VALUE_WIDTH, " ")} ${size.unit.padEnd(SIZE_UNIT_WIDTH, " ")}`;
|
|
25
42
|
}
|
|
26
43
|
|
|
27
44
|
export function displayResultCount(count: number): number {
|
|
@@ -234,19 +251,12 @@ export function printResults(
|
|
|
234
251
|
const rows = [...results]
|
|
235
252
|
.sort(
|
|
236
253
|
(a, b) =>
|
|
237
|
-
a.resultNo - b.resultNo ||
|
|
238
|
-
a.fileName.localeCompare(b.fileName) ||
|
|
239
|
-
a.fileSize - b.fileSize ||
|
|
240
|
-
a.remoteHost.localeCompare(b.remoteHost) ||
|
|
241
|
-
a.remotePort - b.remotePort,
|
|
254
|
+
a.resultNo - b.resultNo || a.fileName.localeCompare(b.fileName),
|
|
242
255
|
)
|
|
243
256
|
.map((result) => ({
|
|
244
257
|
resultNo: String(result.resultNo),
|
|
258
|
+
fileSize: formatResultSize(result.fileSize),
|
|
245
259
|
fileName: sanitizeTableCell(result.fileName),
|
|
246
|
-
fileSize: formatSize(result.fileSize),
|
|
247
|
-
remote: sanitizeTableCell(
|
|
248
|
-
`${result.remoteHost}:${result.remotePort}`,
|
|
249
|
-
),
|
|
250
260
|
}));
|
|
251
261
|
|
|
252
262
|
const widths = {
|
|
@@ -254,49 +264,28 @@ export function printResults(
|
|
|
254
264
|
"No".length,
|
|
255
265
|
...rows.map((row) => row.resultNo.length),
|
|
256
266
|
),
|
|
257
|
-
fileName: RESULT_NAME_WIDTH_MAX,
|
|
258
267
|
fileSize: Math.max(
|
|
259
268
|
"Size".length,
|
|
260
269
|
...rows.map((row) => row.fileSize.length),
|
|
261
270
|
),
|
|
262
|
-
|
|
271
|
+
fileName: Math.max(
|
|
272
|
+
"File".length,
|
|
273
|
+
...rows.map((row) => row.fileName.length),
|
|
274
|
+
),
|
|
263
275
|
};
|
|
264
276
|
|
|
265
|
-
const line = (
|
|
266
|
-
resultNo
|
|
267
|
-
fileName: string,
|
|
268
|
-
fileSize: string,
|
|
269
|
-
remote: string,
|
|
270
|
-
) =>
|
|
271
|
-
`${resultNo.padStart(widths.resultNo, " ")} ${fileName} ${fileSize.padStart(widths.fileSize, " ")} ${remote}`.trimEnd();
|
|
272
|
-
|
|
273
|
-
const fitName = (fileName: string): string => {
|
|
274
|
-
if (fileName.length <= widths.fileName - 2)
|
|
275
|
-
return fileName.padEnd(widths.fileName, " ");
|
|
276
|
-
if (widths.fileName <= 2) return fileName.slice(0, widths.fileName);
|
|
277
|
-
const kept = widths.fileName - 2;
|
|
278
|
-
const head = Math.floor(kept / 2);
|
|
279
|
-
const tail = kept - head;
|
|
280
|
-
return `${fileName.slice(0, head)}..${fileName.slice(-tail)}`;
|
|
281
|
-
};
|
|
277
|
+
const line = (resultNo: string, fileSize: string, fileName: string) =>
|
|
278
|
+
`${resultNo.padStart(widths.resultNo, " ")} ${fileSize.padStart(widths.fileSize, " ")} ${fileName.padEnd(widths.fileName, " ")}`.trimEnd();
|
|
282
279
|
|
|
283
280
|
log(
|
|
284
281
|
[
|
|
285
|
-
line("No", "
|
|
282
|
+
line("No", "Size", "File"),
|
|
286
283
|
line(
|
|
287
284
|
"-".repeat(widths.resultNo),
|
|
288
|
-
"-".repeat(widths.fileName),
|
|
289
285
|
"-".repeat(widths.fileSize),
|
|
290
|
-
"-".repeat(widths.
|
|
291
|
-
),
|
|
292
|
-
...rows.map((row) =>
|
|
293
|
-
line(
|
|
294
|
-
row.resultNo,
|
|
295
|
-
fitName(row.fileName),
|
|
296
|
-
row.fileSize,
|
|
297
|
-
row.remote,
|
|
298
|
-
),
|
|
286
|
+
"-".repeat(widths.fileName),
|
|
299
287
|
),
|
|
288
|
+
...rows.map((row) => line(row.resultNo, row.fileSize, row.fileName)),
|
|
300
289
|
].join("\n"),
|
|
301
290
|
);
|
|
302
291
|
}
|
package/src/const.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
export const HEADER_LEN = 23;
|
|
2
2
|
export const LOCAL_ROUTE = "__local__";
|
|
3
|
-
export const DEFAULT_QRP_TABLE_SIZE = 65536;
|
|
4
|
-
export const DEFAULT_QRP_INFINITY = 7;
|
|
5
|
-
export const DEFAULT_QRP_ENTRY_BITS = 1;
|
|
6
3
|
export const DEFAULT_LISTEN_HOST = "0.0.0.0";
|
|
7
4
|
export const DEFAULT_LISTEN_PORT_MIN = 20000;
|
|
8
5
|
export const DEFAULT_LISTEN_PORT_MAX = 29999;
|
|
@@ -18,7 +15,7 @@ export const RESCAN_SHARES_SEC = 30;
|
|
|
18
15
|
export const ROUTE_TTL_SEC = 600;
|
|
19
16
|
export const SEEN_TTL_SEC = 600;
|
|
20
17
|
export const MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
21
|
-
export const MAX_TTL =
|
|
18
|
+
export const MAX_TTL = 4;
|
|
22
19
|
export const DEFAULT_PING_TTL = 1;
|
|
23
20
|
export const DEFAULT_QUERY_TTL = 4;
|
|
24
21
|
export const ADVERTISED_SPEED_KBPS = 512;
|
|
@@ -37,8 +34,6 @@ export const ENABLE_BYE = true;
|
|
|
37
34
|
export const ENABLE_PONG_CACHING = true;
|
|
38
35
|
export const ENABLE_GGEP = true;
|
|
39
36
|
export const SERVE_URI_RES = true;
|
|
40
|
-
export const QRP_COMPRESSOR_NONE = 0;
|
|
41
|
-
export const QRP_COMPRESSOR_DEFLATE = 1;
|
|
42
37
|
export const MAX_XTRY = 10;
|
|
43
38
|
export const BYE_DEFAULT_CODE = 200;
|
|
44
39
|
export const BOOTSTRAP_CONNECT_CONCURRENCY = 8;
|
|
@@ -104,7 +99,6 @@ export const INTERESTING_HANDSHAKE_HEADERS = [
|
|
|
104
99
|
"remote-ip",
|
|
105
100
|
] as const;
|
|
106
101
|
|
|
107
|
-
export const QRP_HASH_MULTIPLIER = 0x4f1bbcdc;
|
|
108
102
|
export const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
109
103
|
|
|
110
104
|
export const PROMPT_THROBBER_FRAMES = ["*", "o", ".", " "] as const;
|
|
@@ -134,5 +128,3 @@ export const CLI_HELP_LINES = [
|
|
|
134
128
|
"quit",
|
|
135
129
|
"sleep",
|
|
136
130
|
] as const;
|
|
137
|
-
|
|
138
|
-
export const RESULT_NAME_WIDTH_MAX = 48;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
overflowPongCacheKeys,
|
|
3
|
+
pongCacheKey,
|
|
4
|
+
selectCachedPongPayloads,
|
|
5
|
+
} from "./pong_cache";
|
|
6
|
+
export { responseRouteDecision } from "./response_routes";
|
|
7
|
+
export {
|
|
8
|
+
shouldMarkDescriptorSeen,
|
|
9
|
+
shouldSuppressDescriptor,
|
|
10
|
+
} from "./seen";
|
|
11
|
+
export {
|
|
12
|
+
forwardedDescriptorLifetime,
|
|
13
|
+
normalizeQueryLifetime,
|
|
14
|
+
pongReplyTtl,
|
|
15
|
+
queryHitReplyTtl,
|
|
16
|
+
shouldRelayPing,
|
|
17
|
+
} from "./ttl";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { PongCacheEntry } from "./types";
|
|
4
|
+
|
|
5
|
+
export function pongCacheKey(payload: Buffer): string {
|
|
6
|
+
return crypto.createHash("sha1").update(payload).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function overflowPongCacheKeys(
|
|
10
|
+
entries: Iterable<[string, Pick<PongCacheEntry, "at">]>,
|
|
11
|
+
maxSize: number,
|
|
12
|
+
): string[] {
|
|
13
|
+
const all = [...entries];
|
|
14
|
+
if (all.length <= maxSize) return [];
|
|
15
|
+
return all
|
|
16
|
+
.sort((a, b) => a[1].at - b[1].at)
|
|
17
|
+
.slice(0, all.length - maxSize)
|
|
18
|
+
.map(([key]) => key);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function selectCachedPongPayloads(
|
|
22
|
+
entries: Iterable<PongCacheEntry>,
|
|
23
|
+
alreadySent: number,
|
|
24
|
+
maxSent: number,
|
|
25
|
+
): Buffer[] {
|
|
26
|
+
const available = Math.max(0, maxSent - alreadySent);
|
|
27
|
+
if (available === 0) return [];
|
|
28
|
+
return [...entries]
|
|
29
|
+
.sort((a, b) => b.at - a.at)
|
|
30
|
+
.slice(0, available)
|
|
31
|
+
.map((entry) => entry.payload);
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LOCAL_ROUTE } from "../const";
|
|
2
|
+
import type { Route } from "../types";
|
|
3
|
+
import type { ResponseRouteDecision, ResponseRouteOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
export function responseRouteDecision(
|
|
6
|
+
route: Route | typeof LOCAL_ROUTE | undefined,
|
|
7
|
+
options: ResponseRouteOptions = {},
|
|
8
|
+
): ResponseRouteDecision {
|
|
9
|
+
if (!route) return { kind: "drop" };
|
|
10
|
+
if (route === LOCAL_ROUTE) return { kind: "local" };
|
|
11
|
+
if (options.nodeMode === "leaf" && !options.forwardInLeaf) {
|
|
12
|
+
return { kind: "drop" };
|
|
13
|
+
}
|
|
14
|
+
return { kind: "forward", route };
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TYPE } from "../const";
|
|
2
|
+
import type { DescriptorSuppressionInput } from "./types";
|
|
3
|
+
|
|
4
|
+
export function shouldSuppressDescriptor(
|
|
5
|
+
input: DescriptorSuppressionInput,
|
|
6
|
+
): boolean {
|
|
7
|
+
if (
|
|
8
|
+
input.closingAfterBye &&
|
|
9
|
+
input.payloadType !== TYPE.QUERY_HIT &&
|
|
10
|
+
input.payloadType !== TYPE.PUSH
|
|
11
|
+
) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (input.payloadType === TYPE.ROUTE_TABLE_UPDATE) return false;
|
|
15
|
+
return input.alreadySeen;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function shouldMarkDescriptorSeen(payloadType: number): boolean {
|
|
19
|
+
return payloadType !== TYPE.ROUTE_TABLE_UPDATE;
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DescriptorLifetime } from "./types";
|
|
2
|
+
|
|
3
|
+
export function normalizeQueryLifetime(
|
|
4
|
+
ttl: number,
|
|
5
|
+
hops: number,
|
|
6
|
+
maxTtl: number,
|
|
7
|
+
): DescriptorLifetime | null {
|
|
8
|
+
if (ttl > 15) return null;
|
|
9
|
+
const maxLife = Math.max(1, maxTtl);
|
|
10
|
+
if (hops > maxLife) return null;
|
|
11
|
+
return { ttl: Math.max(0, Math.min(ttl, maxLife - hops)), hops };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function forwardedDescriptorLifetime(
|
|
15
|
+
ttl: number,
|
|
16
|
+
hops: number,
|
|
17
|
+
): DescriptorLifetime | undefined {
|
|
18
|
+
if (ttl <= 0) return undefined;
|
|
19
|
+
return { ttl: Math.max(0, ttl - 1), hops: hops + 1 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function pongReplyTtl(hops: number): number {
|
|
23
|
+
return Math.max(1, hops);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function queryHitReplyTtl(hops: number, maxTtl: number): number {
|
|
27
|
+
return Math.min(maxTtl, Math.max(1, hops + 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function shouldRelayPing(
|
|
31
|
+
ttl: number,
|
|
32
|
+
now: number,
|
|
33
|
+
lastPingAt: number,
|
|
34
|
+
minIntervalMs: number,
|
|
35
|
+
): boolean {
|
|
36
|
+
return ttl > 1 && now - lastPingAt >= minIntervalMs;
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Route, RuntimeConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
export type DescriptorLifetime = {
|
|
4
|
+
ttl: number;
|
|
5
|
+
hops: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type DescriptorSuppressionInput = {
|
|
9
|
+
closingAfterBye: boolean;
|
|
10
|
+
payloadType: number;
|
|
11
|
+
alreadySeen: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ResponseRouteOptions = {
|
|
15
|
+
nodeMode?: RuntimeConfig["nodeMode"];
|
|
16
|
+
forwardInLeaf?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ResponseRouteDecision =
|
|
20
|
+
| { kind: "drop" }
|
|
21
|
+
| { kind: "local" }
|
|
22
|
+
| { kind: "forward"; route: Route };
|
|
23
|
+
|
|
24
|
+
export type PongCacheEntry = {
|
|
25
|
+
payload: Buffer;
|
|
26
|
+
at: number;
|
|
27
|
+
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
addPeerCandidatesToKnownSet,
|
|
3
|
+
normalizePeerCandidates,
|
|
4
|
+
peerCandidateSetKey,
|
|
5
|
+
shouldFetchFreshPeerCandidates,
|
|
6
|
+
} from "../peer_discovery";
|
|
2
7
|
import {
|
|
3
8
|
DEFAULT_MAX_BOOTSTRAP_CACHES,
|
|
4
9
|
DEFAULT_MAX_BOOTSTRAP_PEERS,
|
|
@@ -16,7 +21,6 @@ import {
|
|
|
16
21
|
} from "./response";
|
|
17
22
|
import type {
|
|
18
23
|
BootstrapOptions,
|
|
19
|
-
BootstrapPeer,
|
|
20
24
|
BootstrapResult,
|
|
21
25
|
ConnectBootstrapOptions,
|
|
22
26
|
ConnectBootstrapResult,
|
|
@@ -27,38 +31,6 @@ import type {
|
|
|
27
31
|
ReportSelfResult,
|
|
28
32
|
} from "./types";
|
|
29
33
|
|
|
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
34
|
function buildReportReferenceUrl(
|
|
63
35
|
cache: string,
|
|
64
36
|
knownAliveCaches: readonly string[],
|
|
@@ -117,25 +89,18 @@ function emptyConnectBootstrapResult(
|
|
|
117
89
|
};
|
|
118
90
|
}
|
|
119
91
|
|
|
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
92
|
function shouldSkipCacheBootstrap(
|
|
131
|
-
candidates:
|
|
93
|
+
candidates: ReturnType<typeof normalizePeerCandidates>,
|
|
132
94
|
initialAttempt: Awaited<ReturnType<typeof connectBootstrapPeerSet>>,
|
|
133
95
|
candidateKey: string,
|
|
134
96
|
state: GWebCacheBootstrapState | undefined,
|
|
135
97
|
): boolean {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
98
|
+
return !shouldFetchFreshPeerCandidates({
|
|
99
|
+
candidates,
|
|
100
|
+
initialAttempt,
|
|
101
|
+
candidateKey,
|
|
102
|
+
state,
|
|
103
|
+
});
|
|
139
104
|
}
|
|
140
105
|
|
|
141
106
|
function buildBootstrapFetchOptions(
|
|
@@ -156,15 +121,13 @@ function buildBootstrapFetchOptions(
|
|
|
156
121
|
}
|
|
157
122
|
|
|
158
123
|
function addDiscoveredBootstrapPeers(
|
|
159
|
-
peers:
|
|
124
|
+
peers: ReturnType<typeof normalizePeerCandidates>,
|
|
160
125
|
knownPeers: Set<string>,
|
|
161
126
|
addPeer: ConnectBootstrapOptions["addPeer"],
|
|
162
127
|
): string[] {
|
|
163
|
-
const addedPeers
|
|
164
|
-
for (const peer of
|
|
165
|
-
|
|
166
|
-
addedPeers.push(peer.peer);
|
|
167
|
-
addPeer?.(peer.peer);
|
|
128
|
+
const addedPeers = addPeerCandidatesToKnownSet(peers, knownPeers);
|
|
129
|
+
for (const peer of addedPeers) {
|
|
130
|
+
addPeer?.(peer);
|
|
168
131
|
}
|
|
169
132
|
return addedPeers;
|
|
170
133
|
}
|
|
@@ -182,7 +145,7 @@ async function fetchAndRetryBootstrapPeers(
|
|
|
182
145
|
buildBootstrapFetchOptions(options),
|
|
183
146
|
);
|
|
184
147
|
rememberAliveCaches(options.state, bootstrap.successfulCaches);
|
|
185
|
-
const discovered =
|
|
148
|
+
const discovered = normalizePeerCandidates(
|
|
186
149
|
bootstrap.peers,
|
|
187
150
|
options.isSelfPeer,
|
|
188
151
|
).filter((peer) => !knownPeers.has(peer.peer));
|
|
@@ -200,7 +163,7 @@ async function fetchAndRetryBootstrapPeers(
|
|
|
200
163
|
}
|
|
201
164
|
|
|
202
165
|
async function connectBootstrapPeerSet(
|
|
203
|
-
peers:
|
|
166
|
+
peers: ReturnType<typeof normalizePeerCandidates>,
|
|
204
167
|
options: Pick<
|
|
205
168
|
ConnectBootstrapOptions,
|
|
206
169
|
| "availableSlots"
|
|
@@ -433,11 +396,11 @@ function finishCacheBootstrap(
|
|
|
433
396
|
export async function connectBootstrapPeers(
|
|
434
397
|
options: ConnectBootstrapOptions,
|
|
435
398
|
): Promise<ConnectBootstrapResult> {
|
|
436
|
-
const candidates =
|
|
399
|
+
const candidates = normalizePeerCandidates(
|
|
437
400
|
options.peers,
|
|
438
401
|
options.isSelfPeer,
|
|
439
402
|
);
|
|
440
|
-
const candidateKey =
|
|
403
|
+
const candidateKey = peerCandidateSetKey(candidates);
|
|
441
404
|
const knownPeers = new Set(candidates.map((peer) => peer.peer));
|
|
442
405
|
const initialAttempt = await connectBootstrapPeerSet(
|
|
443
406
|
candidates,
|
package/src/gwebcache/types.ts
CHANGED
|
@@ -92,21 +92,17 @@ export type BootstrapOptions = {
|
|
|
92
92
|
fetchImpl?: FetchLike;
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
+
type BootstrapCacheError = {
|
|
96
|
+
cache: string;
|
|
97
|
+
message: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
95
100
|
export type BootstrapResult = {
|
|
96
101
|
peers: string[];
|
|
97
102
|
caches: string[];
|
|
98
103
|
queriedCaches: string[];
|
|
99
104
|
successfulCaches: string[];
|
|
100
|
-
errors:
|
|
101
|
-
cache: string;
|
|
102
|
-
message: string;
|
|
103
|
-
}>;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
export type BootstrapPeer = {
|
|
107
|
-
host: string;
|
|
108
|
-
port: number;
|
|
109
|
-
peer: string;
|
|
105
|
+
errors: BootstrapCacheError[];
|
|
110
106
|
};
|
|
111
107
|
|
|
112
108
|
export type GWebCacheBootstrapState = {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { lowerCaseHeaders, parseRemoteIpHeader } from "./headers";
|
|
2
|
+
import type { RejectHandshakePolicy } from "./types";
|
|
3
|
+
|
|
4
|
+
export function buildRejectHeaders(
|
|
5
|
+
policy: RejectHandshakePolicy,
|
|
6
|
+
): Record<string, string> {
|
|
7
|
+
const headers = lowerCaseHeaders(policy.extraHeaders || {});
|
|
8
|
+
const observedRemote = parseRemoteIpHeader(policy.remoteIp);
|
|
9
|
+
const tryPeers = policy.tryPeers || [];
|
|
10
|
+
if (observedRemote) headers["remote-ip"] = observedRemote;
|
|
11
|
+
if (tryPeers.length) {
|
|
12
|
+
const value = tryPeers.join(",");
|
|
13
|
+
headers["x-try"] = value;
|
|
14
|
+
headers["x-try-ultrapeers"] = value;
|
|
15
|
+
}
|
|
16
|
+
return headers;
|
|
17
|
+
}
|