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,745 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
GWEBCACHE_REPORT_DELAY_SEC,
|
|
5
|
+
LOCAL_ROUTE,
|
|
6
|
+
MAX_PEER_AGE_SEC,
|
|
7
|
+
} from "../const";
|
|
8
|
+
import {
|
|
9
|
+
ensureDir,
|
|
10
|
+
errMsg,
|
|
11
|
+
fileExists,
|
|
12
|
+
isRoutableIpv4,
|
|
13
|
+
isUnspecifiedIpv4,
|
|
14
|
+
normalizeIpv4,
|
|
15
|
+
normalizePeer,
|
|
16
|
+
parsePeer,
|
|
17
|
+
safeFileName,
|
|
18
|
+
ts,
|
|
19
|
+
unique,
|
|
20
|
+
} from "../shared";
|
|
21
|
+
import type {
|
|
22
|
+
BlockIpResult,
|
|
23
|
+
ConnectPeerResult,
|
|
24
|
+
DownloadRecord,
|
|
25
|
+
GnutellaEvent,
|
|
26
|
+
GnutellaEventListener,
|
|
27
|
+
NodeStatus,
|
|
28
|
+
PeerInfo,
|
|
29
|
+
PeerState,
|
|
30
|
+
Route,
|
|
31
|
+
RuntimeConfig,
|
|
32
|
+
SearchHit,
|
|
33
|
+
ShareFile,
|
|
34
|
+
UnblockIpResult,
|
|
35
|
+
} from "../types";
|
|
36
|
+
import { observedAdvertisedHostCandidate } from "./handshake";
|
|
37
|
+
import {
|
|
38
|
+
appendPathSuffix,
|
|
39
|
+
configDocForRuntime,
|
|
40
|
+
detectLocalAdvertisedIpv4,
|
|
41
|
+
filterBlockedPeerState,
|
|
42
|
+
peerStateEquals,
|
|
43
|
+
peerStateTargets,
|
|
44
|
+
rememberPeerInState,
|
|
45
|
+
sortPeerStateEntries,
|
|
46
|
+
trimPeerState,
|
|
47
|
+
writeDoc,
|
|
48
|
+
} from "./peer_state";
|
|
49
|
+
import { persistShareIndex } from "./share_library";
|
|
50
|
+
import { seenKey } from "./core_utils";
|
|
51
|
+
import type { GnutellaServent } from "./node";
|
|
52
|
+
import type { Peer } from "./node_types";
|
|
53
|
+
|
|
54
|
+
export type MaintenanceOperation =
|
|
55
|
+
| "SHARE_RESCAN"
|
|
56
|
+
| "RECONNECT"
|
|
57
|
+
| "SAVE"
|
|
58
|
+
| "GWEBCACHE_UPDATE";
|
|
59
|
+
|
|
60
|
+
export function subscribe(
|
|
61
|
+
node: GnutellaServent,
|
|
62
|
+
listener: GnutellaEventListener,
|
|
63
|
+
): () => void {
|
|
64
|
+
node.listeners.add(listener);
|
|
65
|
+
return () => node.listeners.delete(listener);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function emitEvent(
|
|
69
|
+
node: GnutellaServent,
|
|
70
|
+
event: GnutellaEvent,
|
|
71
|
+
): void {
|
|
72
|
+
for (const listener of node.listeners) listener(event);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function emitMaintenanceError(
|
|
76
|
+
node: GnutellaServent,
|
|
77
|
+
operation: MaintenanceOperation,
|
|
78
|
+
e: unknown,
|
|
79
|
+
): void {
|
|
80
|
+
emitEvent(node, {
|
|
81
|
+
type: "MAINTENANCE_ERROR",
|
|
82
|
+
at: ts(),
|
|
83
|
+
operation,
|
|
84
|
+
message: errMsg(e),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function schedule(
|
|
89
|
+
node: GnutellaServent,
|
|
90
|
+
ms: number,
|
|
91
|
+
fn: () => void,
|
|
92
|
+
): void {
|
|
93
|
+
node.timers.push(node.collaborators.scheduler.setInterval(fn, ms));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function scheduleOnce(
|
|
97
|
+
node: GnutellaServent,
|
|
98
|
+
ms: number,
|
|
99
|
+
fn: () => void,
|
|
100
|
+
): NodeJS.Timeout {
|
|
101
|
+
const timer = node.collaborators.scheduler.setTimeout(() => {
|
|
102
|
+
node.timeouts = node.timeouts.filter(
|
|
103
|
+
(candidate) => candidate !== timer,
|
|
104
|
+
);
|
|
105
|
+
fn();
|
|
106
|
+
}, ms);
|
|
107
|
+
node.timeouts.push(timer);
|
|
108
|
+
return timer;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function cancelTimeout(
|
|
112
|
+
node: GnutellaServent,
|
|
113
|
+
timer: NodeJS.Timeout | undefined,
|
|
114
|
+
): void {
|
|
115
|
+
if (!timer) return;
|
|
116
|
+
node.collaborators.scheduler.clearTimeout(timer);
|
|
117
|
+
node.timeouts = node.timeouts.filter((candidate) => candidate !== timer);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function peerInfo(node: GnutellaServent, peer: Peer): PeerInfo {
|
|
121
|
+
const info: PeerInfo = {
|
|
122
|
+
key: peer.key,
|
|
123
|
+
remoteLabel: peer.remoteLabel,
|
|
124
|
+
browseTarget: peerBrowseTarget(node, peer),
|
|
125
|
+
role: peer.role,
|
|
126
|
+
outbound: peer.outbound,
|
|
127
|
+
dialTarget: peer.dialTarget,
|
|
128
|
+
compression:
|
|
129
|
+
!!peer.capabilities.compressIn || !!peer.capabilities.compressOut,
|
|
130
|
+
tls: node.socketUsesTls(peer.socket),
|
|
131
|
+
};
|
|
132
|
+
if (peer.capabilities.userAgent)
|
|
133
|
+
info.userAgent = peer.capabilities.userAgent;
|
|
134
|
+
return info;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function peerBrowseTarget(
|
|
138
|
+
node: GnutellaServent,
|
|
139
|
+
peer: Peer,
|
|
140
|
+
): string | undefined {
|
|
141
|
+
const fromListenIp = peer.capabilities.listenIp;
|
|
142
|
+
if (
|
|
143
|
+
fromListenIp &&
|
|
144
|
+
!node.isSelfPeer(fromListenIp.host, fromListenIp.port)
|
|
145
|
+
) {
|
|
146
|
+
return `${fromListenIp.host}:${fromListenIp.port}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const candidates = [
|
|
150
|
+
peer.dialTarget,
|
|
151
|
+
peer.outbound ? peer.remoteLabel : undefined,
|
|
152
|
+
peer.remoteLabel,
|
|
153
|
+
];
|
|
154
|
+
for (const candidate of candidates) {
|
|
155
|
+
const parsed = parsePeer(candidate || "");
|
|
156
|
+
if (!parsed || node.isSelfPeer(parsed.host, parsed.port)) continue;
|
|
157
|
+
return `${parsed.host}:${parsed.port}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function peerCount(node: GnutellaServent): number {
|
|
164
|
+
return node.peers.size;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function config(node: GnutellaServent): RuntimeConfig {
|
|
168
|
+
return node.runtimeConfig;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function getBlockedIps(node: GnutellaServent): string[] {
|
|
172
|
+
return [...node.config().blockedIps];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function isBlockedHost(
|
|
176
|
+
node: GnutellaServent,
|
|
177
|
+
host: string | undefined,
|
|
178
|
+
): boolean {
|
|
179
|
+
const normalized = normalizeIpv4(host);
|
|
180
|
+
return !!normalized && node.config().blockedIps.includes(normalized);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function peerHosts(peer: Peer): Set<string> {
|
|
184
|
+
const out = new Set<string>();
|
|
185
|
+
const push = (host: string | undefined) => {
|
|
186
|
+
const normalized = normalizeIpv4(host);
|
|
187
|
+
if (normalized) out.add(normalized);
|
|
188
|
+
};
|
|
189
|
+
push(peer.socket.remoteAddress);
|
|
190
|
+
const remote = parsePeer(peer.remoteLabel);
|
|
191
|
+
if (remote) push(remote.host);
|
|
192
|
+
const dialTarget = parsePeer(peer.dialTarget || "");
|
|
193
|
+
if (dialTarget) push(dialTarget.host);
|
|
194
|
+
if (peer.capabilities.listenIp) push(peer.capabilities.listenIp.host);
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function dropPeersMatchingIp(node: GnutellaServent, ip: string): number {
|
|
199
|
+
let droppedPeers = 0;
|
|
200
|
+
for (const peer of [...node.peers.values()]) {
|
|
201
|
+
if (!peerHosts(peer).has(ip)) continue;
|
|
202
|
+
droppedPeers++;
|
|
203
|
+
peer.socket.destroy(new Error(`blocked IP ${ip}`));
|
|
204
|
+
}
|
|
205
|
+
return droppedPeers;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function pruneBlockedKnownPeers(node: GnutellaServent): number {
|
|
209
|
+
const current = trimPeerState(node.persistedState.peers);
|
|
210
|
+
const filtered = filterBlockedPeerState(
|
|
211
|
+
current,
|
|
212
|
+
node.config().blockedIps,
|
|
213
|
+
);
|
|
214
|
+
const removedKnownPeers =
|
|
215
|
+
peerStateTargets(current).length - peerStateTargets(filtered).length;
|
|
216
|
+
if (peerStateEquals(current, filtered)) return 0;
|
|
217
|
+
node.persistedState.peers = filtered;
|
|
218
|
+
node.gwebCacheBootstrapState.lastExhaustedPeerSet = undefined;
|
|
219
|
+
return removedKnownPeers;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function blockIp(
|
|
223
|
+
node: GnutellaServent,
|
|
224
|
+
host: string,
|
|
225
|
+
): BlockIpResult {
|
|
226
|
+
const ip = normalizeIpv4(host);
|
|
227
|
+
if (!ip) throw new Error("expected IPv4 address");
|
|
228
|
+
if (node.config().blockedIps.includes(ip)) {
|
|
229
|
+
return {
|
|
230
|
+
ip,
|
|
231
|
+
status: "already-blocked",
|
|
232
|
+
droppedPeers: 0,
|
|
233
|
+
removedKnownPeers: 0,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
node.updateRuntimeConfig({
|
|
237
|
+
blockedIps: unique([...node.config().blockedIps, ip]),
|
|
238
|
+
});
|
|
239
|
+
const removedKnownPeers = node.pruneBlockedKnownPeers();
|
|
240
|
+
const droppedPeers = dropPeersMatchingIp(node, ip);
|
|
241
|
+
return {
|
|
242
|
+
ip,
|
|
243
|
+
status: "blocked",
|
|
244
|
+
droppedPeers,
|
|
245
|
+
removedKnownPeers,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function unblockIp(
|
|
250
|
+
node: GnutellaServent,
|
|
251
|
+
host: string,
|
|
252
|
+
): UnblockIpResult {
|
|
253
|
+
const ip = normalizeIpv4(host);
|
|
254
|
+
if (!ip) throw new Error("expected IPv4 address");
|
|
255
|
+
if (!node.config().blockedIps.includes(ip)) {
|
|
256
|
+
return { ip, status: "not-blocked" };
|
|
257
|
+
}
|
|
258
|
+
node.updateRuntimeConfig({
|
|
259
|
+
blockedIps: node
|
|
260
|
+
.config()
|
|
261
|
+
.blockedIps.filter((candidate) => candidate !== ip),
|
|
262
|
+
});
|
|
263
|
+
return { ip, status: "unblocked" };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function configuredAdvertisedHost(
|
|
267
|
+
node: GnutellaServent,
|
|
268
|
+
): string | undefined {
|
|
269
|
+
const raw = node.config().advertisedHost;
|
|
270
|
+
if (typeof raw !== "string") return undefined;
|
|
271
|
+
const host = raw.trim();
|
|
272
|
+
return host || undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function currentAdvertisedPort(node: GnutellaServent): number {
|
|
276
|
+
const configured = node.config().advertisedPort;
|
|
277
|
+
return Number.isInteger(configured) && (configured || 0) > 0
|
|
278
|
+
? (configured as number)
|
|
279
|
+
: node.config().listenPort;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function currentAdvertisedHost(node: GnutellaServent): string {
|
|
283
|
+
return (
|
|
284
|
+
node.configuredAdvertisedHost() ||
|
|
285
|
+
node.learnedAdvertisedHost ||
|
|
286
|
+
detectLocalAdvertisedIpv4(node.config().listenHost)
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function selfHosts(node: GnutellaServent): Set<string> {
|
|
291
|
+
const out = new Set<string>();
|
|
292
|
+
const push = (host: string | undefined) => {
|
|
293
|
+
const normalized = normalizeIpv4(host);
|
|
294
|
+
if (normalized && !isUnspecifiedIpv4(normalized)) out.add(normalized);
|
|
295
|
+
};
|
|
296
|
+
push(node.configuredAdvertisedHost());
|
|
297
|
+
push(node.learnedAdvertisedHost);
|
|
298
|
+
push(node.config().listenHost);
|
|
299
|
+
push(detectLocalAdvertisedIpv4(node.config().listenHost));
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function isSelfPeer(
|
|
304
|
+
node: GnutellaServent,
|
|
305
|
+
host: string,
|
|
306
|
+
port: number,
|
|
307
|
+
): boolean {
|
|
308
|
+
const normalizedHost = normalizeIpv4(host);
|
|
309
|
+
if (!normalizedHost || !port) return false;
|
|
310
|
+
const ports = new Set([
|
|
311
|
+
node.config().listenPort,
|
|
312
|
+
node.currentAdvertisedPort(),
|
|
313
|
+
]);
|
|
314
|
+
return ports.has(port) && node.selfHosts().has(normalizedHost);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function maybeObserveAdvertisedHost(
|
|
318
|
+
node: GnutellaServent,
|
|
319
|
+
headers: Record<string, string>,
|
|
320
|
+
reporterHost?: string,
|
|
321
|
+
): void {
|
|
322
|
+
if (node.configuredAdvertisedHost()) return;
|
|
323
|
+
const observed = observedAdvertisedHostCandidate(headers, reporterHost);
|
|
324
|
+
if (!observed) return;
|
|
325
|
+
const { observedHost, subnet } = observed;
|
|
326
|
+
if (observedHost === node.learnedAdvertisedHost) return;
|
|
327
|
+
node.trackPendingAdvertisedHost(observedHost, subnet);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function trackPendingAdvertisedHost(
|
|
331
|
+
node: GnutellaServent,
|
|
332
|
+
observedHost: string,
|
|
333
|
+
subnet: string,
|
|
334
|
+
): void {
|
|
335
|
+
if (node.pendingAdvertisedHost !== observedHost) {
|
|
336
|
+
node.pendingAdvertisedHost = observedHost;
|
|
337
|
+
node.pendingAdvertisedSubnets.clear();
|
|
338
|
+
}
|
|
339
|
+
node.pendingAdvertisedSubnets.add(subnet);
|
|
340
|
+
if (node.pendingAdvertisedSubnets.size < 3) return;
|
|
341
|
+
|
|
342
|
+
node.learnedAdvertisedHost = observedHost;
|
|
343
|
+
node.pendingAdvertisedHost = undefined;
|
|
344
|
+
node.pendingAdvertisedSubnets.clear();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function absorbHandshakeHeaders(
|
|
348
|
+
node: GnutellaServent,
|
|
349
|
+
headers: Record<string, string>,
|
|
350
|
+
reporterHost?: string,
|
|
351
|
+
): void {
|
|
352
|
+
node.maybeAbsorbTryHeaders(headers);
|
|
353
|
+
node.maybeObserveAdvertisedHost(headers, reporterHost);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function save(node: GnutellaServent): Promise<void> {
|
|
357
|
+
const c = node.config();
|
|
358
|
+
for (const peer of node.peers.values()) node.markPeerSeenIfStable(peer);
|
|
359
|
+
node.pruneExpiredKnownPeers();
|
|
360
|
+
node.pruneBlockedKnownPeers();
|
|
361
|
+
node.persistedState.peers = trimPeerState(node.persistedState.peers);
|
|
362
|
+
node.persistedState.serventIdHex = node.serventId.toString("hex");
|
|
363
|
+
node.doc.config = configDocForRuntime(node.runtimeConfig);
|
|
364
|
+
node.doc.state = node.persistedState;
|
|
365
|
+
await ensureDir(path.dirname(node.configPath));
|
|
366
|
+
await ensureDir(c.downloadsDir);
|
|
367
|
+
await writeDoc(node.configPath, node.doc);
|
|
368
|
+
await persistShareIndex(node);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function rememberKnownPeer(
|
|
372
|
+
node: GnutellaServent,
|
|
373
|
+
host: string,
|
|
374
|
+
port: number,
|
|
375
|
+
timestamp: number,
|
|
376
|
+
): void {
|
|
377
|
+
if (!host || !port || node.isSelfPeer(host, port)) return;
|
|
378
|
+
if (node.isBlockedHost(host)) return;
|
|
379
|
+
node.persistedState.peers = rememberPeerInState(
|
|
380
|
+
node.persistedState.peers,
|
|
381
|
+
normalizePeer(host, port),
|
|
382
|
+
timestamp,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function addKnownPeer(
|
|
387
|
+
node: GnutellaServent,
|
|
388
|
+
host: string,
|
|
389
|
+
port: number,
|
|
390
|
+
): void {
|
|
391
|
+
rememberKnownPeer(node, host, port, 0);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function updateKnownPeerLastSeen(
|
|
395
|
+
node: GnutellaServent,
|
|
396
|
+
host: string,
|
|
397
|
+
port: number,
|
|
398
|
+
timestamp?: number,
|
|
399
|
+
): void {
|
|
400
|
+
rememberKnownPeer(
|
|
401
|
+
node,
|
|
402
|
+
host,
|
|
403
|
+
port,
|
|
404
|
+
timestamp ?? node.peerSeenTimestamp(),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function peerSeenTimestamp(
|
|
409
|
+
node: GnutellaServent,
|
|
410
|
+
nowMs = node.now(),
|
|
411
|
+
): number {
|
|
412
|
+
return Math.max(0, Math.floor(nowMs / 1000));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function pruneExpiredKnownPeers(
|
|
416
|
+
node: GnutellaServent,
|
|
417
|
+
nowSec?: number,
|
|
418
|
+
): boolean {
|
|
419
|
+
const timestamp = nowSec ?? node.peerSeenTimestamp();
|
|
420
|
+
const current = trimPeerState(node.persistedState.peers);
|
|
421
|
+
const filtered = Object.fromEntries(
|
|
422
|
+
sortPeerStateEntries(
|
|
423
|
+
filterBlockedPeerState(current, node.config().blockedIps),
|
|
424
|
+
).filter(
|
|
425
|
+
([, lastSeen]) =>
|
|
426
|
+
lastSeen === 0 || timestamp - lastSeen <= MAX_PEER_AGE_SEC,
|
|
427
|
+
),
|
|
428
|
+
) as PeerState;
|
|
429
|
+
if (peerStateEquals(current, filtered)) return false;
|
|
430
|
+
node.persistedState.peers = filtered;
|
|
431
|
+
node.gwebCacheBootstrapState.lastExhaustedPeerSet = undefined;
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function shouldBootstrapFreshPeers(node: GnutellaServent): boolean {
|
|
436
|
+
const peers = filterBlockedPeerState(
|
|
437
|
+
trimPeerState(node.persistedState.peers),
|
|
438
|
+
node.config().blockedIps,
|
|
439
|
+
);
|
|
440
|
+
const timestamps = Object.values(peers);
|
|
441
|
+
return timestamps.length > 0 && timestamps.every((value) => value === 0);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function rememberPeerAddresses(
|
|
445
|
+
node: GnutellaServent,
|
|
446
|
+
peer: Peer,
|
|
447
|
+
timestamp = 0,
|
|
448
|
+
): void {
|
|
449
|
+
const remembered = new Set<string>();
|
|
450
|
+
const push = (host: string, port: number) => {
|
|
451
|
+
const target = normalizePeer(host, port);
|
|
452
|
+
if (remembered.has(target)) return;
|
|
453
|
+
remembered.add(target);
|
|
454
|
+
if (timestamp > 0) node.updateKnownPeerLastSeen(host, port, timestamp);
|
|
455
|
+
else node.addKnownPeer(host, port);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
if (peer.dialTarget) {
|
|
459
|
+
const addr = parsePeer(peer.dialTarget);
|
|
460
|
+
if (addr) push(addr.host, addr.port);
|
|
461
|
+
}
|
|
462
|
+
if (peer.capabilities.listenIp) {
|
|
463
|
+
push(peer.capabilities.listenIp.host, peer.capabilities.listenIp.port);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function markPeerSeenIfStable(
|
|
468
|
+
node: GnutellaServent,
|
|
469
|
+
peer: Peer,
|
|
470
|
+
nowMs = node.now(),
|
|
471
|
+
): void {
|
|
472
|
+
if (nowMs - peer.connectedAt < node.config().peerSeenThresholdSec * 1000)
|
|
473
|
+
return;
|
|
474
|
+
node.rememberPeerAddresses(peer, node.peerSeenTimestamp(nowMs));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function scheduleGWebCacheReport(node: GnutellaServent): void {
|
|
478
|
+
if (
|
|
479
|
+
node.gwebCacheReportAttempted ||
|
|
480
|
+
node.gwebCacheReported ||
|
|
481
|
+
node.gwebCacheReportTimer ||
|
|
482
|
+
node.peerCount() === 0
|
|
483
|
+
)
|
|
484
|
+
return;
|
|
485
|
+
|
|
486
|
+
node.gwebCacheReportTimer = node.scheduleOnce(
|
|
487
|
+
GWEBCACHE_REPORT_DELAY_SEC * 1000,
|
|
488
|
+
() => {
|
|
489
|
+
node.gwebCacheReportTimer = undefined;
|
|
490
|
+
if (node.stopped || node.gwebCacheReported || node.peerCount() === 0)
|
|
491
|
+
return;
|
|
492
|
+
node.gwebCacheReportAttempted = true;
|
|
493
|
+
void node
|
|
494
|
+
.announceSelfToGWebCaches()
|
|
495
|
+
.catch((e) => node.emitMaintenanceError("GWEBCACHE_UPDATE", e));
|
|
496
|
+
},
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function refreshGWebCacheReport(node: GnutellaServent): void {
|
|
501
|
+
if (node.peerCount() > 0) {
|
|
502
|
+
node.scheduleGWebCacheReport();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
node.cancelTimeout(node.gwebCacheReportTimer);
|
|
506
|
+
node.gwebCacheReportTimer = undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export async function announceSelfToGWebCaches(
|
|
510
|
+
node: GnutellaServent,
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
const host = normalizeIpv4(node.currentAdvertisedHost());
|
|
513
|
+
const port = node.currentAdvertisedPort();
|
|
514
|
+
if (!host || !isRoutableIpv4(host) || !port) return;
|
|
515
|
+
|
|
516
|
+
const result = await node.reportSelfToGWebCaches({
|
|
517
|
+
caches: node.config().gwebCacheUrls,
|
|
518
|
+
client: node.config().vendorCode,
|
|
519
|
+
version: node.config().userAgent,
|
|
520
|
+
ip: normalizePeer(host, port),
|
|
521
|
+
uptimeSec: Math.max(
|
|
522
|
+
0,
|
|
523
|
+
Math.floor((node.now() - node.startedAtMs) / 1000),
|
|
524
|
+
),
|
|
525
|
+
leafCount:
|
|
526
|
+
node.nodeMode() === "ultrapeer"
|
|
527
|
+
? node.connectedLeafCount()
|
|
528
|
+
: undefined,
|
|
529
|
+
maxLeaves:
|
|
530
|
+
node.nodeMode() === "ultrapeer"
|
|
531
|
+
? node.config().maxLeafConnections
|
|
532
|
+
: undefined,
|
|
533
|
+
state: node.gwebCacheBootstrapState,
|
|
534
|
+
});
|
|
535
|
+
if (result.reportedCaches.length > 0) node.gwebCacheReported = true;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function peerDialState(
|
|
539
|
+
node: GnutellaServent,
|
|
540
|
+
host: string,
|
|
541
|
+
port: number,
|
|
542
|
+
): "connected" | "dialing" | "none" {
|
|
543
|
+
const target = normalizePeer(host, port);
|
|
544
|
+
if (node.dialing.has(target)) return "dialing";
|
|
545
|
+
for (const peer of node.peers.values()) {
|
|
546
|
+
if (peer.dialTarget === target) return "connected";
|
|
547
|
+
if (
|
|
548
|
+
peer.capabilities.listenIp &&
|
|
549
|
+
normalizePeer(
|
|
550
|
+
peer.capabilities.listenIp.host,
|
|
551
|
+
peer.capabilities.listenIp.port,
|
|
552
|
+
) === target
|
|
553
|
+
)
|
|
554
|
+
return "connected";
|
|
555
|
+
}
|
|
556
|
+
return "none";
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export async function connectToPeer(
|
|
560
|
+
node: GnutellaServent,
|
|
561
|
+
peerSpec: string,
|
|
562
|
+
): Promise<ConnectPeerResult> {
|
|
563
|
+
const addr = parsePeer(peerSpec);
|
|
564
|
+
if (!addr) throw new Error("expected ip:port");
|
|
565
|
+
const peer = normalizePeer(addr.host, addr.port);
|
|
566
|
+
if (node.isSelfPeer(addr.host, addr.port))
|
|
567
|
+
throw new Error("cannot add self as peer");
|
|
568
|
+
if (node.isBlockedHost(addr.host))
|
|
569
|
+
return { peer, status: "blocked", message: `blocked IP ${addr.host}` };
|
|
570
|
+
|
|
571
|
+
node.addKnownPeer(addr.host, addr.port);
|
|
572
|
+
|
|
573
|
+
const state = node.peerDialState(addr.host, addr.port);
|
|
574
|
+
if (state === "connected") return { peer, status: "already-connected" };
|
|
575
|
+
if (state === "dialing") return { peer, status: "dialing" };
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
await node.connectPeer(addr.host, addr.port);
|
|
579
|
+
return { peer, status: "connected" };
|
|
580
|
+
} catch (e) {
|
|
581
|
+
return { peer, status: "saved", message: errMsg(e) };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function markSeen(
|
|
586
|
+
node: GnutellaServent,
|
|
587
|
+
payloadType: number,
|
|
588
|
+
descriptorIdHex: string,
|
|
589
|
+
payload?: Buffer,
|
|
590
|
+
): void {
|
|
591
|
+
node.seen.set(
|
|
592
|
+
seenKey(payloadType, descriptorIdHex, payload),
|
|
593
|
+
node.now(),
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function hasSeen(
|
|
598
|
+
node: GnutellaServent,
|
|
599
|
+
payloadType: number,
|
|
600
|
+
descriptorIdHex: string,
|
|
601
|
+
payload?: Buffer,
|
|
602
|
+
): boolean {
|
|
603
|
+
return node.seen.has(seenKey(payloadType, descriptorIdHex, payload));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function pruneSeenEntries(
|
|
607
|
+
node: GnutellaServent,
|
|
608
|
+
now: number,
|
|
609
|
+
maxAgeMs: number,
|
|
610
|
+
): void {
|
|
611
|
+
for (const [key, at] of node.seen) {
|
|
612
|
+
if (now - at > maxAgeMs) node.seen.delete(key);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function pruneRouteEntries(
|
|
617
|
+
_node: GnutellaServent,
|
|
618
|
+
routes: Map<string, Route | typeof LOCAL_ROUTE>,
|
|
619
|
+
now: number,
|
|
620
|
+
maxAgeMs: number,
|
|
621
|
+
): void {
|
|
622
|
+
for (const [key, route] of routes) {
|
|
623
|
+
if (route !== LOCAL_ROUTE && now - route.ts > maxAgeMs)
|
|
624
|
+
routes.delete(key);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export function prunePushRoutes(
|
|
629
|
+
node: GnutellaServent,
|
|
630
|
+
now: number,
|
|
631
|
+
maxAgeMs: number,
|
|
632
|
+
): void {
|
|
633
|
+
for (const [key, route] of node.pushRoutes) {
|
|
634
|
+
if (now - route.ts > maxAgeMs) node.pushRoutes.delete(key);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function prunePendingPushQueues(
|
|
639
|
+
node: GnutellaServent,
|
|
640
|
+
now: number,
|
|
641
|
+
waitMs: number,
|
|
642
|
+
): void {
|
|
643
|
+
for (const [key, queue] of node.pendingPushes) {
|
|
644
|
+
const keep = queue.filter((pending) => {
|
|
645
|
+
if (now - pending.createdAt <= waitMs) return true;
|
|
646
|
+
pending.reject(new Error("push timed out"));
|
|
647
|
+
return false;
|
|
648
|
+
});
|
|
649
|
+
if (keep.length) node.pendingPushes.set(key, keep);
|
|
650
|
+
else node.pendingPushes.delete(key);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export function prunePongCache(
|
|
655
|
+
node: GnutellaServent,
|
|
656
|
+
now: number,
|
|
657
|
+
maxAgeMs: number,
|
|
658
|
+
): void {
|
|
659
|
+
for (const [key, entry] of node.pongCache) {
|
|
660
|
+
if (now - entry.at > maxAgeMs) node.pongCache.delete(key);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export function pruneMaps(node: GnutellaServent): void {
|
|
665
|
+
const now = node.now();
|
|
666
|
+
const seenAge = node.config().seenTtlSec * 1000;
|
|
667
|
+
const routeAge = node.config().routeTtlSec * 1000;
|
|
668
|
+
node.pruneSeenEntries(now, seenAge);
|
|
669
|
+
node.pruneRouteEntries(node.pingRoutes, now, routeAge);
|
|
670
|
+
node.pruneRouteEntries(node.queryRoutes, now, routeAge);
|
|
671
|
+
node.prunePushRoutes(now, routeAge);
|
|
672
|
+
node.prunePendingPushQueues(now, node.config().pushWaitMs);
|
|
673
|
+
node.prunePongCache(now, routeAge);
|
|
674
|
+
if (node.lastResults.length > 1000)
|
|
675
|
+
node.lastResults = node.lastResults.slice(-1000);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export function getPeers(node: GnutellaServent): PeerInfo[] {
|
|
679
|
+
return [...node.peers.values()].map((peer) => node.peerInfo(peer));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function getShares(node: GnutellaServent): ShareFile[] {
|
|
683
|
+
return [...node.shares];
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export function getResults(node: GnutellaServent): SearchHit[] {
|
|
687
|
+
return [...node.lastResults];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function clearResults(node: GnutellaServent): void {
|
|
691
|
+
node.lastResults = [];
|
|
692
|
+
node.resultSeq = 1;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export function getKnownPeers(node: GnutellaServent): string[] {
|
|
696
|
+
return peerStateTargets(
|
|
697
|
+
filterBlockedPeerState(
|
|
698
|
+
node.persistedState.peers,
|
|
699
|
+
node.config().blockedIps,
|
|
700
|
+
),
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function getDownloads(node: GnutellaServent): DownloadRecord[] {
|
|
705
|
+
return [...node.downloads];
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export function getServentIdHex(node: GnutellaServent): string {
|
|
709
|
+
return node.serventId.toString("hex");
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export function getStatus(node: GnutellaServent): NodeStatus {
|
|
713
|
+
return {
|
|
714
|
+
peers: node.peers.size,
|
|
715
|
+
shares: node.shares.length,
|
|
716
|
+
results: node.lastResults.length,
|
|
717
|
+
knownPeers: node.getKnownPeers().length,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export function reserveAutoDownloadPath(
|
|
722
|
+
node: GnutellaServent,
|
|
723
|
+
fileName: string,
|
|
724
|
+
): Promise<string> {
|
|
725
|
+
const basePath = path.resolve(
|
|
726
|
+
path.join(node.config().downloadsDir, safeFileName(fileName)),
|
|
727
|
+
);
|
|
728
|
+
let suffixNo = 1;
|
|
729
|
+
return (async () => {
|
|
730
|
+
for (;;) {
|
|
731
|
+
const candidate =
|
|
732
|
+
suffixNo === 1 ? basePath : appendPathSuffix(basePath, suffixNo);
|
|
733
|
+
if (node.activeAutoDownloadPaths.has(candidate)) {
|
|
734
|
+
suffixNo++;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (await fileExists(candidate)) {
|
|
738
|
+
suffixNo++;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
node.activeAutoDownloadPaths.add(candidate);
|
|
742
|
+
return candidate;
|
|
743
|
+
}
|
|
744
|
+
})();
|
|
745
|
+
}
|