feat(core): Gnutella joining after Java, measured against Java
GnutellaRun lets Java's PeerToPeerAnwendung join peers on terminal-net. The core's gnutella.ts does the same: a ping in Java's text format to port 6346, a pong back, the ping sent on to the other peers and their pongs back the way it came, at most the program's number of peers. Departure from Java: a ping that comes a second time gets no pong and its connection stays (Java's worker throws on the missing pong). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
// The Gnutella program (apps/gnutella.ts) beyond what Java's runs show
|
||||
// (gnutellaJava.test.ts): its packets as Java writes and reads them, a ping
|
||||
// that comes twice, the files it offers.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { GNUTELLA_FOLDER, gnutellaPacket, gnutellaText, pongPayloadLength, VirtualFileSystem, withSnapshot, type NetworkRuntimeEngine } from '../src/index.js';
|
||||
import { watchedEngine } from './javaRun.js';
|
||||
import { snapshotOf, type Scenario } from './scenario.js';
|
||||
|
||||
const scenario = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'scenarios', 'terminal-net.scenario.json'), 'utf8')) as Scenario;
|
||||
const CLIENT = '00000000-0000-0000-0000-000000000401';
|
||||
const ECHO = '00000000-0000-0000-0000-000000000404';
|
||||
|
||||
describe('Gnutella packets as Java writes and reads them', () => {
|
||||
it('writes a ping and a pong with their fields joined by "//"', () => {
|
||||
expect(gnutellaText({ kind: 'ping', guid: 42, hops: 1, ttl: 7, payloadLength: 0, ipAddress: '192.168.0.30' })).toBe('42//0x00//1//7//0//192.168.0.30');
|
||||
expect(gnutellaText({ kind: 'pong', guid: 42, hops: 0, ttl: 8, payloadLength: 77, ipAddress: '10.0.0.2', port: 6346, files: 2, bytes: 15 })).toBe(
|
||||
'42//0x01//0//8//77//10.0.0.2//6346//2//15',
|
||||
);
|
||||
});
|
||||
|
||||
it('reads them back, skipping empty fields as a StringTokenizer on "/" does', () => {
|
||||
expect(gnutellaPacket('42///0x00//1//7//0//192.168.0.30/')).toEqual({ kind: 'ping', guid: 42, hops: 1, ttl: 7, payloadLength: 0, ipAddress: '192.168.0.30' });
|
||||
expect(gnutellaPacket('42//0x01//0//8//77//10.0.0.2//6346//2//15')).toEqual({
|
||||
kind: 'pong',
|
||||
guid: 42,
|
||||
hops: 0,
|
||||
ttl: 8,
|
||||
payloadLength: 77,
|
||||
ipAddress: '10.0.0.2',
|
||||
port: 6346,
|
||||
files: 2,
|
||||
bytes: 15,
|
||||
});
|
||||
});
|
||||
|
||||
it('reads no packet from a text that is none, or of another kind', () => {
|
||||
expect(['', 'GET /x HTTP/1.1', '42//0x00//1//7//0', 'x//0x00//1//7//0//a', '42//0x80//1//7//0//1//txt', '42//0x01//0//8//77//10.0.0.2//6346//2'].map(gnutellaPacket)).toEqual(
|
||||
Array(6).fill(undefined),
|
||||
);
|
||||
});
|
||||
|
||||
it("counts a pong's payload in Java's bits: the address's characters, then the least power of two for each number", () => {
|
||||
expect(pongPayloadLength('10.0.0.2', 6346, 0, 0)).toBe(77);
|
||||
expect(pongPayloadLength('192.168.0.31', 6346, 0, 0)).toBe(109);
|
||||
expect(pongPayloadLength('1.2.3.4', 6346, 1, 5)).toBe(7 * 8 + 13 + 0 + 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the Gnutella program in the engine', () => {
|
||||
const started = (files?: VirtualFileSystem) =>
|
||||
watchedEngine(
|
||||
scenario,
|
||||
withSnapshot(snapshotOf(scenario), {
|
||||
gnutellaByNodeId: new Map([[ECHO, { maximumKnownPeers: 3 }]]),
|
||||
...(files === undefined ? {} : { fileSystemsByNodeId: new Map([[ECHO, files]]) }),
|
||||
}),
|
||||
).engine;
|
||||
|
||||
/** A connection of the client's to .31's port 6346, a ping sent on it; what came back. */
|
||||
const ping = (engine: NetworkRuntimeEngine, text: string) => {
|
||||
const socketId = engine.openTcpClientSocket(CLIENT, '192.168.0.31', 6346)!;
|
||||
expect(engine.connectTcp(socketId)).toBe(true);
|
||||
expect(engine.sendTcp(socketId, new TextEncoder().encode(text))).toBe(true);
|
||||
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 3_000 });
|
||||
const answers: string[] = [];
|
||||
for (let answer = engine.receiveTcp(socketId); answer !== undefined; answer = engine.receiveTcp(socketId)) answers.push(new TextDecoder().decode(answer));
|
||||
return { socketId, answers };
|
||||
};
|
||||
|
||||
it('makes its folder of files to share', () => {
|
||||
const engine = started();
|
||||
expect(engine.state.topologySnapshot.fileSystemsByNodeId.get(ECHO)!.entry(GNUTELLA_FOLDER).content.kind).toBe('directory');
|
||||
});
|
||||
|
||||
it('answers a ping once, and keeps the connection of the second (Java closes it)', () => {
|
||||
const engine = started();
|
||||
const first = ping(engine, '7//0x00//0//8//0//192.168.0.20');
|
||||
expect(first.answers).toEqual(['7//0x01//0//8//109//192.168.0.31//6346//0//0']);
|
||||
const second = ping(engine, '7//0x00//0//8//0//192.168.0.20');
|
||||
expect(second.answers).toEqual([]);
|
||||
expect(engine.state.socketsById.get(second.socketId)?.tcpState).toBe('ESTABLISHED');
|
||||
expect(engine.gnutella(ECHO)!.peers).toEqual(['192.168.0.20']);
|
||||
});
|
||||
|
||||
it("counts the files of its folder and their size in a pong (a text's in characters)", () => {
|
||||
const files = new VirtualFileSystem();
|
||||
files.createDirectory(GNUTELLA_FOLDER);
|
||||
files.writeTextFile(`${GNUTELLA_FOLDER}/lied.txt`, 'Hallo Welt');
|
||||
files.writeBinaryFile(`${GNUTELLA_FOLDER}/daten.bin`, new Uint8Array(5));
|
||||
files.createDirectory(`${GNUTELLA_FOLDER}/ordner`);
|
||||
files.writeTextFile('/anderswo.txt', 'nicht geteilt');
|
||||
const engine = started(files);
|
||||
expect(ping(engine, '9//0x00//0//8//0//192.168.0.20').answers).toEqual([`9//0x01//0//8//${pongPayloadLength('192.168.0.31', 6346, 2, 15)}//192.168.0.31//6346//2//15`]);
|
||||
});
|
||||
|
||||
it('joins no one at its own address, and forgets its peers when reset', () => {
|
||||
const engine = started();
|
||||
engine.gnutella(ECHO)!.join('192.168.0.31');
|
||||
engine.handle({ type: 'advance', toMilliseconds: 3_000 });
|
||||
expect(engine.capture.frames(scenario.nodes.find((node) => node.id === ECHO)!.ports[0]!.id)).toEqual([]);
|
||||
ping(engine, '5//0x00//0//8//0//192.168.0.20');
|
||||
engine.gnutella(ECHO)!.reset();
|
||||
expect(engine.gnutella(ECHO)!.peers).toEqual([]);
|
||||
expect(engine.gnutella(ECHO)!.log.all.map((line) => line.peers)).toEqual([['192.168.0.20'], []]);
|
||||
});
|
||||
|
||||
it('closes a connection whose text has no packet head, and leaves a packet of another kind (Java: its worker throws on the one)', () => {
|
||||
const engine = started();
|
||||
const garbage = ping(engine, 'hallo');
|
||||
expect(garbage.answers).toEqual([]);
|
||||
expect(engine.state.socketsById.get(garbage.socketId)?.tcpState).not.toBe('ESTABLISHED');
|
||||
const other = ping(engine, '3//0x42//0//8//0');
|
||||
expect(other.answers).toEqual([]);
|
||||
expect(engine.state.socketsById.get(other.socketId)?.tcpState).toBe('ESTABLISHED');
|
||||
});
|
||||
|
||||
it('sends a ping on once, and not when its time to live runs out', () => {
|
||||
const WEB = '00000000-0000-0000-0000-000000000403';
|
||||
const engine = watchedEngine(
|
||||
scenario,
|
||||
withSnapshot(snapshotOf(scenario), {
|
||||
gnutellaByNodeId: new Map([
|
||||
[ECHO, { maximumKnownPeers: 3 }],
|
||||
[WEB, { maximumKnownPeers: 3 }],
|
||||
]),
|
||||
}),
|
||||
).engine;
|
||||
engine.gnutella(WEB)!.join('192.168.0.31');
|
||||
engine.handle({ type: 'advance', toMilliseconds: 3_000 });
|
||||
const webCard = scenario.nodes.find((node) => node.id === WEB)!.ports[0]!.id;
|
||||
const forwarded = () =>
|
||||
engine.capture.frames(webCard).filter(({ frame }) => {
|
||||
const packet = frame.payload.kind === 'ipv4' ? frame.payload.packet : undefined;
|
||||
return packet?.payload.kind === 'tcp' && packet.senderIPAddress === '192.168.0.31' && new TextDecoder().decode(packet.payload.segment.payload).includes('//0x00//');
|
||||
}).length;
|
||||
expect(forwarded()).toBe(0);
|
||||
ping(engine, '11//0x00//7//1//0//192.168.0.20');
|
||||
expect(forwarded()).toBe(0);
|
||||
ping(engine, '12//0x00//0//8//0//192.168.0.20');
|
||||
ping(engine, '12//0x00//0//8//0//192.168.0.20');
|
||||
expect(forwarded()).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user