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.
- package/CLI.md +189 -0
- package/DEVELOPER.md +193 -0
- package/LICENSE +674 -0
- package/QUICKSTART.md +133 -0
- package/README.md +74 -0
- package/bin/gnutella.ts +15 -0
- package/gnutella.json.example +18 -0
- package/package.json +72 -0
- package/src/cli.ts +692 -0
- package/src/cli_shared.ts +359 -0
- package/src/const.ts +138 -0
- package/src/gwebcache/bootstrap.ts +491 -0
- package/src/gwebcache/response.ts +391 -0
- package/src/gwebcache/shared.ts +116 -0
- package/src/gwebcache/types.ts +187 -0
- package/src/gwebcache_client.ts +13 -0
- package/src/protocol/browse_host.ts +552 -0
- package/src/protocol/client_blocking.ts +29 -0
- package/src/protocol/codec.ts +715 -0
- package/src/protocol/content_urn.ts +170 -0
- package/src/protocol/core_utils.ts +43 -0
- package/src/protocol/file_server.ts +245 -0
- package/src/protocol/ggep.ts +168 -0
- package/src/protocol/handshake.ts +199 -0
- package/src/protocol/http_download_reader.ts +112 -0
- package/src/protocol/magnet.ts +176 -0
- package/src/protocol/node.ts +416 -0
- package/src/protocol/node_handshake.ts +992 -0
- package/src/protocol/node_lifecycle.ts +210 -0
- package/src/protocol/node_protocol_runtime.ts +949 -0
- package/src/protocol/node_qrp_runtime.ts +97 -0
- package/src/protocol/node_query_routing.ts +208 -0
- package/src/protocol/node_state.ts +745 -0
- package/src/protocol/node_tls.ts +257 -0
- package/src/protocol/node_topology.ts +141 -0
- package/src/protocol/node_transfer.ts +455 -0
- package/src/protocol/node_types.ts +106 -0
- package/src/protocol/peer_state.ts +675 -0
- package/src/protocol/qrp.ts +549 -0
- package/src/protocol/query_search.ts +29 -0
- package/src/protocol/share_index.ts +131 -0
- package/src/protocol/share_library.ts +246 -0
- package/src/protocol.ts +36 -0
- package/src/shared.ts +236 -0
- package/src/types.ts +452 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ADVERTISED_SPEED_KBPS,
|
|
8
|
+
CONNECT_TIMEOUT_MS,
|
|
9
|
+
DATA_DOWNLOADS_DIRNAME,
|
|
10
|
+
DEFAULT_LISTEN_HOST,
|
|
11
|
+
DEFAULT_LISTEN_PORT_MAX,
|
|
12
|
+
DEFAULT_LISTEN_PORT_MIN,
|
|
13
|
+
DEFAULT_PING_TTL,
|
|
14
|
+
DEFAULT_QUERY_ROUTING_VERSION,
|
|
15
|
+
DEFAULT_QUERY_TTL,
|
|
16
|
+
DEFAULT_USER_AGENT,
|
|
17
|
+
DEFAULT_VENDOR_CODE,
|
|
18
|
+
DOWNLOAD_TIMEOUT_MS,
|
|
19
|
+
ENABLE_BYE,
|
|
20
|
+
ENABLE_COMPRESSION,
|
|
21
|
+
ENABLE_GGEP,
|
|
22
|
+
ENABLE_PONG_CACHING,
|
|
23
|
+
ENABLE_QRP,
|
|
24
|
+
ENABLE_TLS,
|
|
25
|
+
MAX_LEAF_CONNECTIONS,
|
|
26
|
+
MAX_PAYLOAD_BYTES,
|
|
27
|
+
MAX_RESULTS_PER_QUERY,
|
|
28
|
+
MAX_TRACKED_PEERS,
|
|
29
|
+
MAX_TTL,
|
|
30
|
+
MAX_ULTRAPEER_CONNECTIONS,
|
|
31
|
+
PEER_SEEN_THRESHOLD_SEC,
|
|
32
|
+
PING_INTERVAL_SEC,
|
|
33
|
+
PUSH_WAIT_MS,
|
|
34
|
+
RECONNECT_INTERVAL_SEC,
|
|
35
|
+
RESCAN_SHARES_SEC,
|
|
36
|
+
ROUTE_TTL_SEC,
|
|
37
|
+
SEEN_TTL_SEC,
|
|
38
|
+
SERVE_URI_RES,
|
|
39
|
+
} from "../const";
|
|
40
|
+
import {
|
|
41
|
+
ensureDir,
|
|
42
|
+
fileExists,
|
|
43
|
+
isRoutableIpv4,
|
|
44
|
+
isUnspecifiedIpv4,
|
|
45
|
+
normalizeIpv4,
|
|
46
|
+
normalizePeer,
|
|
47
|
+
parsePeer,
|
|
48
|
+
unique,
|
|
49
|
+
} from "../shared";
|
|
50
|
+
import type { ConfigDoc, PeerState, RuntimeConfig } from "../types";
|
|
51
|
+
import { normalizeCacheUrl } from "../gwebcache/shared";
|
|
52
|
+
|
|
53
|
+
type PersistedConfig = {
|
|
54
|
+
listen_ip?: unknown;
|
|
55
|
+
listen_host?: unknown;
|
|
56
|
+
listen_port?: unknown;
|
|
57
|
+
advertised_ip?: unknown;
|
|
58
|
+
advertised_host?: unknown;
|
|
59
|
+
advertised_port?: unknown;
|
|
60
|
+
blocked_ips?: unknown;
|
|
61
|
+
gwebcache_urls?: unknown;
|
|
62
|
+
ultrapeer?: unknown;
|
|
63
|
+
max_connections?: unknown;
|
|
64
|
+
max_ultrapeer_connections?: unknown;
|
|
65
|
+
max_leaf_connections?: unknown;
|
|
66
|
+
enable_tls?: unknown;
|
|
67
|
+
log_ignore?: unknown;
|
|
68
|
+
data_dir?: unknown;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type PersistedState = {
|
|
72
|
+
servent_id_hex?: unknown;
|
|
73
|
+
peers?: unknown;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type PersistedDoc = {
|
|
77
|
+
config?: PersistedConfig;
|
|
78
|
+
state?: PersistedState;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type LocalIpv4Candidates = {
|
|
82
|
+
routable?: string;
|
|
83
|
+
privateAddr?: string;
|
|
84
|
+
loopback: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function interfaceIpv4Host(addr: {
|
|
88
|
+
address: string;
|
|
89
|
+
family?: string | number;
|
|
90
|
+
}): string | undefined {
|
|
91
|
+
const family = String(addr.family || "");
|
|
92
|
+
if (family !== "IPv4" && family !== "4") return undefined;
|
|
93
|
+
const host = normalizeIpv4(addr.address);
|
|
94
|
+
if (!host || isUnspecifiedIpv4(host)) return undefined;
|
|
95
|
+
return host;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function recordLocalIpv4Candidate(
|
|
99
|
+
candidates: LocalIpv4Candidates,
|
|
100
|
+
host: string,
|
|
101
|
+
internal: boolean | undefined,
|
|
102
|
+
): void {
|
|
103
|
+
if (internal) {
|
|
104
|
+
if (!candidates.loopback) candidates.loopback = host;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (isRoutableIpv4(host)) {
|
|
108
|
+
candidates.routable = host;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!candidates.privateAddr) candidates.privateAddr = host;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function detectLocalAdvertisedIpv4(listenHost: string): string {
|
|
115
|
+
const listen = normalizeIpv4(listenHost);
|
|
116
|
+
if (listen && !isUnspecifiedIpv4(listen)) return listen;
|
|
117
|
+
|
|
118
|
+
const candidates: LocalIpv4Candidates = {
|
|
119
|
+
loopback: "127.0.0.1",
|
|
120
|
+
};
|
|
121
|
+
for (const iface of Object.values(os.networkInterfaces())) {
|
|
122
|
+
for (const addr of iface || []) {
|
|
123
|
+
const host = interfaceIpv4Host(addr);
|
|
124
|
+
if (!host) continue;
|
|
125
|
+
recordLocalIpv4Candidate(candidates, host, addr.internal);
|
|
126
|
+
if (candidates.routable) return candidates.routable;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return candidates.privateAddr || candidates.loopback;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizePeerTimestamp(value: unknown): number {
|
|
133
|
+
const ts = Number(value);
|
|
134
|
+
if (!Number.isFinite(ts)) return 0;
|
|
135
|
+
return Math.max(0, Math.floor(ts));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizePeerState(value: unknown): PeerState {
|
|
139
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
140
|
+
return {};
|
|
141
|
+
|
|
142
|
+
const out = new Map<string, number>();
|
|
143
|
+
for (const [peerSpec, rawTimestamp] of Object.entries(
|
|
144
|
+
value as Record<string, unknown>,
|
|
145
|
+
)) {
|
|
146
|
+
const addr = parsePeer(peerSpec);
|
|
147
|
+
if (!addr) continue;
|
|
148
|
+
const peer = normalizePeer(addr.host, addr.port);
|
|
149
|
+
const timestamp = normalizePeerTimestamp(rawTimestamp);
|
|
150
|
+
const current = out.get(peer) ?? 0;
|
|
151
|
+
if (!out.has(peer) || timestamp > current) out.set(peer, timestamp);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return Object.fromEntries(out);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function sortPeerStateEntries(
|
|
158
|
+
peers: PeerState,
|
|
159
|
+
): Array<[peer: string, lastSeen: number]> {
|
|
160
|
+
const entries = Object.entries(normalizePeerState(peers)) as Array<
|
|
161
|
+
[string, number]
|
|
162
|
+
>;
|
|
163
|
+
entries.sort((a, b) => b[1] - a[1]);
|
|
164
|
+
return entries;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function trimPeerState(
|
|
168
|
+
peers: PeerState,
|
|
169
|
+
limit = MAX_TRACKED_PEERS,
|
|
170
|
+
): PeerState {
|
|
171
|
+
if (limit <= 0) return {};
|
|
172
|
+
return Object.fromEntries(sortPeerStateEntries(peers).slice(0, limit));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function filterBlockedPeerState(
|
|
176
|
+
peers: PeerState,
|
|
177
|
+
blockedIps: readonly string[],
|
|
178
|
+
): PeerState {
|
|
179
|
+
const blocked = new Set(
|
|
180
|
+
blockedIps
|
|
181
|
+
.map((entry) => normalizeIpv4(entry))
|
|
182
|
+
.filter((entry): entry is string => !!entry),
|
|
183
|
+
);
|
|
184
|
+
if (!blocked.size) return trimPeerState(peers);
|
|
185
|
+
return Object.fromEntries(
|
|
186
|
+
sortPeerStateEntries(peers).filter(([peer]) => {
|
|
187
|
+
const addr = parsePeer(peer);
|
|
188
|
+
const host = normalizeIpv4(addr?.host);
|
|
189
|
+
return !host || !blocked.has(host);
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function rememberPeerInState(
|
|
195
|
+
peers: PeerState,
|
|
196
|
+
peerSpec: string,
|
|
197
|
+
timestamp = 0,
|
|
198
|
+
): PeerState {
|
|
199
|
+
const addr = parsePeer(peerSpec);
|
|
200
|
+
if (!addr) return trimPeerState(peers);
|
|
201
|
+
|
|
202
|
+
const peer = normalizePeer(addr.host, addr.port);
|
|
203
|
+
const current = normalizePeerState(peers);
|
|
204
|
+
const existing = current[peer];
|
|
205
|
+
const nextTimestamp = Math.max(
|
|
206
|
+
existing ?? 0,
|
|
207
|
+
normalizePeerTimestamp(timestamp),
|
|
208
|
+
);
|
|
209
|
+
const shouldPromote =
|
|
210
|
+
existing == null ||
|
|
211
|
+
nextTimestamp > existing ||
|
|
212
|
+
(existing === 0 && nextTimestamp === 0);
|
|
213
|
+
|
|
214
|
+
if (!shouldPromote) return trimPeerState(current);
|
|
215
|
+
|
|
216
|
+
return trimPeerState(
|
|
217
|
+
Object.fromEntries([
|
|
218
|
+
[peer, nextTimestamp],
|
|
219
|
+
...Object.entries(current).filter(
|
|
220
|
+
([candidate]) => candidate !== peer,
|
|
221
|
+
),
|
|
222
|
+
]),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function peerStateTargets(peers: PeerState): string[] {
|
|
227
|
+
return sortPeerStateEntries(peers).map(([peer]) => peer);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function peerStateEquals(a: PeerState, b: PeerState): boolean {
|
|
231
|
+
const aEntries = sortPeerStateEntries(a);
|
|
232
|
+
const bEntries = sortPeerStateEntries(b);
|
|
233
|
+
if (aEntries.length !== bEntries.length) return false;
|
|
234
|
+
return aEntries.every(
|
|
235
|
+
([peer, lastSeen], index) =>
|
|
236
|
+
bEntries[index]?.[0] === peer && bEntries[index]?.[1] === lastSeen,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function appendPathSuffix(
|
|
241
|
+
filePath: string,
|
|
242
|
+
suffixNo: number,
|
|
243
|
+
): string {
|
|
244
|
+
const dir = path.dirname(filePath);
|
|
245
|
+
const ext = path.extname(filePath);
|
|
246
|
+
const base = path.basename(filePath, ext);
|
|
247
|
+
return path.join(dir, `${base} (${suffixNo})${ext}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveConfiguredPath(
|
|
251
|
+
baseDir: string,
|
|
252
|
+
value: unknown,
|
|
253
|
+
): string | undefined {
|
|
254
|
+
if (typeof value !== "string") return undefined;
|
|
255
|
+
const trimmed = value.trim();
|
|
256
|
+
if (!trimmed) return undefined;
|
|
257
|
+
return path.isAbsolute(trimmed)
|
|
258
|
+
? path.normalize(trimmed)
|
|
259
|
+
: path.resolve(baseDir, trimmed);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function resolveDataDir(
|
|
263
|
+
configPath: string,
|
|
264
|
+
config: {
|
|
265
|
+
data_dir?: unknown;
|
|
266
|
+
},
|
|
267
|
+
): string {
|
|
268
|
+
const base = path.dirname(path.resolve(configPath));
|
|
269
|
+
const explicit = resolveConfiguredPath(base, config.data_dir);
|
|
270
|
+
return explicit || base;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function normalizedListenHost(value: unknown): string {
|
|
274
|
+
return (
|
|
275
|
+
normalizeIpv4(typeof value === "string" ? value : undefined) ||
|
|
276
|
+
DEFAULT_LISTEN_HOST
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function normalizedPositivePort(value: unknown): number | undefined {
|
|
281
|
+
return Number.isInteger(value) && (value as number) > 0
|
|
282
|
+
? (value as number)
|
|
283
|
+
: undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function defaultListenPortForServentId(serventIdHex: string): number {
|
|
287
|
+
const span = DEFAULT_LISTEN_PORT_MAX - DEFAULT_LISTEN_PORT_MIN + 1;
|
|
288
|
+
if (!/^[0-9a-f]{32}$/i.test(serventIdHex)) {
|
|
289
|
+
return DEFAULT_LISTEN_PORT_MIN;
|
|
290
|
+
}
|
|
291
|
+
const seed = Number.parseInt(serventIdHex.slice(0, 8), 16);
|
|
292
|
+
return DEFAULT_LISTEN_PORT_MIN + (seed % span);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function normalizedAdvertisedHost(value: unknown): string | undefined {
|
|
296
|
+
return normalizeIpv4(typeof value === "string" ? value : undefined);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function runtimeGWebCacheUrls(value: unknown): string[] {
|
|
300
|
+
return normalizedGWebCacheUrls(value) || [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function derivedMaxConnections(config: {
|
|
304
|
+
nodeMode: "leaf" | "ultrapeer";
|
|
305
|
+
maxUltrapeerConnections: number;
|
|
306
|
+
maxLeafConnections: number;
|
|
307
|
+
}): number {
|
|
308
|
+
return config.nodeMode === "ultrapeer"
|
|
309
|
+
? config.maxUltrapeerConnections + config.maxLeafConnections
|
|
310
|
+
: config.maxUltrapeerConnections;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function runtimeConfigFor(
|
|
314
|
+
configPath: string,
|
|
315
|
+
doc: Pick<ConfigDoc, "config" | "state">,
|
|
316
|
+
): RuntimeConfig {
|
|
317
|
+
const dataDir = resolveDataDir(configPath, {
|
|
318
|
+
data_dir: doc.config.dataDir,
|
|
319
|
+
});
|
|
320
|
+
const defaultListenPort = defaultListenPortForServentId(
|
|
321
|
+
doc.state.serventIdHex,
|
|
322
|
+
);
|
|
323
|
+
const monitorIgnoreEvents =
|
|
324
|
+
normalizedMonitorIgnoreEvents(doc.config.monitorIgnoreEvents) || [];
|
|
325
|
+
const nodeMode = doc.config.ultrapeer === true ? "ultrapeer" : "leaf";
|
|
326
|
+
const maxUltrapeerConnections =
|
|
327
|
+
positiveIntegerOrUndefined(doc.config.maxUltrapeerConnections) ||
|
|
328
|
+
MAX_ULTRAPEER_CONNECTIONS;
|
|
329
|
+
const maxLeafConnections =
|
|
330
|
+
positiveIntegerOrUndefined(doc.config.maxLeafConnections) ||
|
|
331
|
+
MAX_LEAF_CONNECTIONS;
|
|
332
|
+
return {
|
|
333
|
+
listenHost: normalizedListenHost(doc.config.listenHost),
|
|
334
|
+
listenPort:
|
|
335
|
+
normalizedPositivePort(doc.config.listenPort) || defaultListenPort,
|
|
336
|
+
advertisedHost: normalizedAdvertisedHost(doc.config.advertisedHost),
|
|
337
|
+
advertisedPort: normalizedPositivePort(doc.config.advertisedPort),
|
|
338
|
+
blockedIps: normalizedBlockedIps(doc.config.blockedIps) || [],
|
|
339
|
+
gwebCacheUrls: runtimeGWebCacheUrls(doc.config.gwebCacheUrls),
|
|
340
|
+
ultrapeer: doc.config.ultrapeer === true,
|
|
341
|
+
monitorIgnoreEvents,
|
|
342
|
+
nodeMode,
|
|
343
|
+
dataDir,
|
|
344
|
+
downloadsDir: path.join(dataDir, DATA_DOWNLOADS_DIRNAME),
|
|
345
|
+
peerSeenThresholdSec: PEER_SEEN_THRESHOLD_SEC,
|
|
346
|
+
maxConnections: derivedMaxConnections({
|
|
347
|
+
nodeMode,
|
|
348
|
+
maxUltrapeerConnections,
|
|
349
|
+
maxLeafConnections,
|
|
350
|
+
}),
|
|
351
|
+
maxUltrapeerConnections,
|
|
352
|
+
maxLeafConnections,
|
|
353
|
+
connectTimeoutMs: CONNECT_TIMEOUT_MS,
|
|
354
|
+
pingIntervalSec: PING_INTERVAL_SEC,
|
|
355
|
+
reconnectIntervalSec: RECONNECT_INTERVAL_SEC,
|
|
356
|
+
rescanSharesSec: RESCAN_SHARES_SEC,
|
|
357
|
+
routeTtlSec: ROUTE_TTL_SEC,
|
|
358
|
+
seenTtlSec: SEEN_TTL_SEC,
|
|
359
|
+
maxPayloadBytes: MAX_PAYLOAD_BYTES,
|
|
360
|
+
maxTtl: MAX_TTL,
|
|
361
|
+
defaultPingTtl: DEFAULT_PING_TTL,
|
|
362
|
+
defaultQueryTtl: DEFAULT_QUERY_TTL,
|
|
363
|
+
advertisedSpeedKBps: ADVERTISED_SPEED_KBPS,
|
|
364
|
+
downloadTimeoutMs: DOWNLOAD_TIMEOUT_MS,
|
|
365
|
+
pushWaitMs: PUSH_WAIT_MS,
|
|
366
|
+
maxResultsPerQuery: MAX_RESULTS_PER_QUERY,
|
|
367
|
+
userAgent: DEFAULT_USER_AGENT,
|
|
368
|
+
queryRoutingVersion: DEFAULT_QUERY_ROUTING_VERSION,
|
|
369
|
+
enableCompression: ENABLE_COMPRESSION,
|
|
370
|
+
enableQrp: ENABLE_QRP,
|
|
371
|
+
enableBye: ENABLE_BYE,
|
|
372
|
+
enablePongCaching: ENABLE_PONG_CACHING,
|
|
373
|
+
enableGgep: ENABLE_GGEP,
|
|
374
|
+
enableTls:
|
|
375
|
+
typeof doc.config.enableTls === "boolean"
|
|
376
|
+
? doc.config.enableTls
|
|
377
|
+
: ENABLE_TLS,
|
|
378
|
+
serveUriRes: SERVE_URI_RES,
|
|
379
|
+
vendorCode: DEFAULT_VENDOR_CODE,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function applyRuntimeConfigPatch(
|
|
384
|
+
config: RuntimeConfig,
|
|
385
|
+
patch: Partial<RuntimeConfig>,
|
|
386
|
+
): RuntimeConfig {
|
|
387
|
+
const next = {
|
|
388
|
+
...config,
|
|
389
|
+
...patch,
|
|
390
|
+
};
|
|
391
|
+
if (
|
|
392
|
+
patch.maxConnections != null &&
|
|
393
|
+
patch.maxUltrapeerConnections == null
|
|
394
|
+
) {
|
|
395
|
+
next.maxUltrapeerConnections = patch.maxConnections;
|
|
396
|
+
}
|
|
397
|
+
if (patch.nodeMode && patch.ultrapeer == null) {
|
|
398
|
+
next.ultrapeer = patch.nodeMode === "ultrapeer";
|
|
399
|
+
} else if (patch.ultrapeer != null && patch.nodeMode == null) {
|
|
400
|
+
next.nodeMode = patch.ultrapeer ? "ultrapeer" : "leaf";
|
|
401
|
+
}
|
|
402
|
+
next.maxConnections = derivedMaxConnections(next);
|
|
403
|
+
return next;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function configDocForRuntime(
|
|
407
|
+
config: RuntimeConfig,
|
|
408
|
+
): ConfigDoc["config"] {
|
|
409
|
+
return {
|
|
410
|
+
listenHost: config.listenHost,
|
|
411
|
+
listenPort: config.listenPort,
|
|
412
|
+
advertisedHost: config.advertisedHost,
|
|
413
|
+
advertisedPort: config.advertisedPort,
|
|
414
|
+
blockedIps: config.blockedIps.length
|
|
415
|
+
? [...config.blockedIps]
|
|
416
|
+
: undefined,
|
|
417
|
+
gwebCacheUrls: config.gwebCacheUrls.length
|
|
418
|
+
? [...config.gwebCacheUrls]
|
|
419
|
+
: undefined,
|
|
420
|
+
ultrapeer: config.ultrapeer,
|
|
421
|
+
maxUltrapeerConnections: config.maxUltrapeerConnections,
|
|
422
|
+
maxLeafConnections: config.maxLeafConnections,
|
|
423
|
+
enableTls: config.enableTls,
|
|
424
|
+
monitorIgnoreEvents: config.monitorIgnoreEvents.length
|
|
425
|
+
? [...config.monitorIgnoreEvents]
|
|
426
|
+
: undefined,
|
|
427
|
+
dataDir: config.dataDir,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function randomDocServentId(): string {
|
|
432
|
+
const id = crypto.randomBytes(16);
|
|
433
|
+
id[8] = 0xff;
|
|
434
|
+
id[15] = 0x00;
|
|
435
|
+
return id.toString("hex");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function optionalNonEmptyString(value: unknown): string | undefined {
|
|
439
|
+
if (typeof value !== "string") return undefined;
|
|
440
|
+
const trimmed = value.trim();
|
|
441
|
+
return trimmed || undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function positiveIntegerOrUndefined(value: unknown): number | undefined {
|
|
445
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0
|
|
446
|
+
? value
|
|
447
|
+
: undefined;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function normalizedStringArray(
|
|
451
|
+
value: unknown,
|
|
452
|
+
normalize: (value: string) => string | undefined,
|
|
453
|
+
): string[] | undefined {
|
|
454
|
+
if (!Array.isArray(value)) return undefined;
|
|
455
|
+
const normalized = unique(
|
|
456
|
+
value
|
|
457
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
458
|
+
.map((entry) => normalize(entry))
|
|
459
|
+
.filter((entry): entry is string => !!entry),
|
|
460
|
+
);
|
|
461
|
+
return normalized.length ? normalized : undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function normalizedMonitorIgnoreEvents(
|
|
465
|
+
value: unknown,
|
|
466
|
+
): string[] | undefined {
|
|
467
|
+
return normalizedStringArray(value, (entry) => {
|
|
468
|
+
const normalized = entry.trim().toUpperCase();
|
|
469
|
+
return normalized || undefined;
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function normalizedBlockedIps(value: unknown): string[] | undefined {
|
|
474
|
+
return normalizedStringArray(value, (entry) => normalizeIpv4(entry));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function normalizedGWebCacheUrls(value: unknown): string[] | undefined {
|
|
478
|
+
return normalizedStringArray(value, (entry) => normalizeCacheUrl(entry));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function normalizedServentIdHex(value: unknown): string | undefined {
|
|
482
|
+
if (typeof value !== "string" || !/^[0-9a-f]{32}$/i.test(value))
|
|
483
|
+
return undefined;
|
|
484
|
+
return value.toLowerCase();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function defaultDoc(configPath: string): ConfigDoc {
|
|
488
|
+
const base = path.dirname(path.resolve(configPath));
|
|
489
|
+
const serventIdHex = randomDocServentId();
|
|
490
|
+
return {
|
|
491
|
+
config: {
|
|
492
|
+
listenHost: DEFAULT_LISTEN_HOST,
|
|
493
|
+
listenPort: defaultListenPortForServentId(serventIdHex),
|
|
494
|
+
blockedIps: [],
|
|
495
|
+
ultrapeer: false,
|
|
496
|
+
maxUltrapeerConnections: MAX_ULTRAPEER_CONNECTIONS,
|
|
497
|
+
maxLeafConnections: MAX_LEAF_CONNECTIONS,
|
|
498
|
+
dataDir: base,
|
|
499
|
+
},
|
|
500
|
+
state: {
|
|
501
|
+
serventIdHex,
|
|
502
|
+
peers: {},
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function ensureDocRuntimeDirs(
|
|
508
|
+
configPath: string,
|
|
509
|
+
doc: ConfigDoc,
|
|
510
|
+
ensureConfigDir = false,
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
const runtime = runtimeConfigFor(configPath, doc);
|
|
513
|
+
if (ensureConfigDir) await ensureDir(path.dirname(configPath));
|
|
514
|
+
await ensureDir(runtime.downloadsDir);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function applyLoadedPeerLimits(
|
|
518
|
+
doc: ConfigDoc,
|
|
519
|
+
config: PersistedConfig,
|
|
520
|
+
): void {
|
|
521
|
+
const maxUltrapeerConnections =
|
|
522
|
+
positiveIntegerOrUndefined(config.max_ultrapeer_connections) ||
|
|
523
|
+
positiveIntegerOrUndefined(config.max_connections);
|
|
524
|
+
if (maxUltrapeerConnections)
|
|
525
|
+
doc.config.maxUltrapeerConnections = maxUltrapeerConnections;
|
|
526
|
+
const maxLeafConnections = positiveIntegerOrUndefined(
|
|
527
|
+
config.max_leaf_connections,
|
|
528
|
+
);
|
|
529
|
+
if (maxLeafConnections)
|
|
530
|
+
doc.config.maxLeafConnections = maxLeafConnections;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function applyLoadedMonitorConfig(
|
|
534
|
+
doc: ConfigDoc,
|
|
535
|
+
config: PersistedConfig,
|
|
536
|
+
): void {
|
|
537
|
+
const monitorIgnoreEvents = normalizedMonitorIgnoreEvents(
|
|
538
|
+
config.log_ignore,
|
|
539
|
+
);
|
|
540
|
+
if (monitorIgnoreEvents)
|
|
541
|
+
doc.config.monitorIgnoreEvents = monitorIgnoreEvents;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function applyOptionalLoadedConfig(
|
|
545
|
+
doc: ConfigDoc,
|
|
546
|
+
config: PersistedConfig,
|
|
547
|
+
): void {
|
|
548
|
+
const advertisedHost =
|
|
549
|
+
optionalNonEmptyString(config.advertised_ip) ||
|
|
550
|
+
optionalNonEmptyString(config.advertised_host);
|
|
551
|
+
if (advertisedHost) doc.config.advertisedHost = advertisedHost;
|
|
552
|
+
const advertisedPort = positiveIntegerOrUndefined(
|
|
553
|
+
config.advertised_port,
|
|
554
|
+
);
|
|
555
|
+
if (advertisedPort) doc.config.advertisedPort = advertisedPort;
|
|
556
|
+
const blockedIps = normalizedBlockedIps(config.blocked_ips);
|
|
557
|
+
if (blockedIps) doc.config.blockedIps = blockedIps;
|
|
558
|
+
const gwebCacheUrls = normalizedGWebCacheUrls(config.gwebcache_urls);
|
|
559
|
+
if (gwebCacheUrls) doc.config.gwebCacheUrls = gwebCacheUrls;
|
|
560
|
+
if (typeof config.enable_tls === "boolean")
|
|
561
|
+
doc.config.enableTls = config.enable_tls;
|
|
562
|
+
doc.config.ultrapeer = config.ultrapeer === true;
|
|
563
|
+
applyLoadedPeerLimits(doc, config);
|
|
564
|
+
applyLoadedMonitorConfig(doc, config);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function buildLoadedDoc(
|
|
568
|
+
configPath: string,
|
|
569
|
+
parsed: PersistedDoc,
|
|
570
|
+
): ConfigDoc {
|
|
571
|
+
const defaults = defaultDoc(configPath);
|
|
572
|
+
const config = parsed.config || {};
|
|
573
|
+
const state = parsed.state || {};
|
|
574
|
+
const serventIdHex =
|
|
575
|
+
normalizedServentIdHex(state.servent_id_hex) ||
|
|
576
|
+
defaults.state.serventIdHex;
|
|
577
|
+
const doc: ConfigDoc = {
|
|
578
|
+
config: {
|
|
579
|
+
listenHost:
|
|
580
|
+
optionalNonEmptyString(config.listen_ip) ||
|
|
581
|
+
optionalNonEmptyString(config.listen_host) ||
|
|
582
|
+
defaults.config.listenHost,
|
|
583
|
+
listenPort:
|
|
584
|
+
positiveIntegerOrUndefined(config.listen_port) ||
|
|
585
|
+
defaultListenPortForServentId(serventIdHex),
|
|
586
|
+
blockedIps: [],
|
|
587
|
+
ultrapeer: config.ultrapeer === true,
|
|
588
|
+
maxUltrapeerConnections: defaults.config.maxUltrapeerConnections,
|
|
589
|
+
maxLeafConnections: defaults.config.maxLeafConnections,
|
|
590
|
+
dataDir: resolveDataDir(configPath, config),
|
|
591
|
+
},
|
|
592
|
+
state: {
|
|
593
|
+
serventIdHex,
|
|
594
|
+
peers: normalizePeerState(state.peers),
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
applyOptionalLoadedConfig(doc, config);
|
|
598
|
+
doc.state.peers = filterBlockedPeerState(
|
|
599
|
+
doc.state.peers,
|
|
600
|
+
doc.config.blockedIps || [],
|
|
601
|
+
);
|
|
602
|
+
return doc;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function createDefaultDocOnDisk(
|
|
606
|
+
configPath: string,
|
|
607
|
+
): Promise<ConfigDoc> {
|
|
608
|
+
const doc = defaultDoc(configPath);
|
|
609
|
+
await ensureDocRuntimeDirs(configPath, doc, true);
|
|
610
|
+
await writeDoc(configPath, doc);
|
|
611
|
+
return doc;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export async function loadDoc(configPath: string): Promise<ConfigDoc> {
|
|
615
|
+
const full = path.resolve(configPath);
|
|
616
|
+
if (!(await fileExists(full))) return await createDefaultDocOnDisk(full);
|
|
617
|
+
const raw = await fsp.readFile(full, "utf8");
|
|
618
|
+
const doc = buildLoadedDoc(full, JSON.parse(raw) as PersistedDoc);
|
|
619
|
+
await ensureDocRuntimeDirs(full, doc);
|
|
620
|
+
return doc;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function persistedConfigForRuntime(
|
|
624
|
+
runtime: RuntimeConfig,
|
|
625
|
+
): PersistedConfig {
|
|
626
|
+
const cleanConfig: PersistedConfig = {
|
|
627
|
+
listen_ip: runtime.listenHost,
|
|
628
|
+
listen_port: runtime.listenPort,
|
|
629
|
+
gwebcache_urls: [...runtime.gwebCacheUrls],
|
|
630
|
+
ultrapeer: runtime.ultrapeer,
|
|
631
|
+
max_ultrapeer_connections: runtime.maxUltrapeerConnections,
|
|
632
|
+
max_leaf_connections: runtime.maxLeafConnections,
|
|
633
|
+
enable_tls: runtime.enableTls,
|
|
634
|
+
data_dir: runtime.dataDir,
|
|
635
|
+
};
|
|
636
|
+
if (runtime.advertisedHost)
|
|
637
|
+
cleanConfig.advertised_ip = runtime.advertisedHost;
|
|
638
|
+
if (
|
|
639
|
+
runtime.advertisedPort != null &&
|
|
640
|
+
runtime.advertisedPort !== runtime.listenPort
|
|
641
|
+
) {
|
|
642
|
+
cleanConfig.advertised_port = runtime.advertisedPort;
|
|
643
|
+
}
|
|
644
|
+
if (runtime.blockedIps.length)
|
|
645
|
+
cleanConfig.blocked_ips = runtime.blockedIps;
|
|
646
|
+
if (runtime.monitorIgnoreEvents.length)
|
|
647
|
+
cleanConfig.log_ignore = runtime.monitorIgnoreEvents;
|
|
648
|
+
return cleanConfig;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function persistedStateForDoc(doc: ConfigDoc): PersistedState {
|
|
652
|
+
return {
|
|
653
|
+
servent_id_hex:
|
|
654
|
+
typeof doc.state.serventIdHex === "string" &&
|
|
655
|
+
/^[0-9a-f]{32}$/i.test(doc.state.serventIdHex)
|
|
656
|
+
? doc.state.serventIdHex.toLowerCase()
|
|
657
|
+
: randomDocServentId(),
|
|
658
|
+
peers: trimPeerState(doc.state.peers),
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export async function writeDoc(
|
|
663
|
+
configPath: string,
|
|
664
|
+
doc: ConfigDoc,
|
|
665
|
+
): Promise<void> {
|
|
666
|
+
const full = path.resolve(configPath);
|
|
667
|
+
const tmp = `${full}.tmp`;
|
|
668
|
+
const runtime = runtimeConfigFor(full, doc);
|
|
669
|
+
const clean: PersistedDoc = {
|
|
670
|
+
config: persistedConfigForRuntime(runtime),
|
|
671
|
+
state: persistedStateForDoc(doc),
|
|
672
|
+
};
|
|
673
|
+
await fsp.writeFile(tmp, `${JSON.stringify(clean, null, 2)}\n`, "utf8");
|
|
674
|
+
await fsp.rename(tmp, full);
|
|
675
|
+
}
|