*Initial version

This commit is contained in:
Derrick Hammer 2022-11-15 06:34:01 -05:00
parent 9c47e6e1ee
commit 76731d4354
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
7 changed files with 394 additions and 1 deletions

View File

@ -1,6 +1,8 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2022 Hammer Technologies LLC
Credits to https://github.com/RangerMauve/hyper-flood for original version
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

8
messages.proto Normal file
View File

@ -0,0 +1,8 @@
// type=0
message Packet {
required bytes originId = 1;
required uint32 messageNumber = 2;
required uint32 ttl = 3;
required bytes data = 4;
}

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "@lumeweb/dht-flood",
"type": "commonjs",
"version": "0.1.0",
"main": "dist/index.js",
"dependencies": {
"compact-encoding": "^2.11.0",
"lru": "^3.1.0",
"protocol-buffers-encodings": "^1.2.0",
"protomux-rpc": "^1.3.0"
},
"devDependencies": {
"@types/b4a": "^1.6.0",
"@types/debug": "^4.1.7",
"b4a": "^1.6.1",
"debug": "^4.3.4",
"hyperswarm": "^4.3.5",
"protoc": "^1.1.3",
"sodium-universal": "^3.1.0",
"tape": "^5.6.1",
"ts-proto": "^1.131.2"
}
}

126
src/index.ts Normal file
View File

@ -0,0 +1,126 @@
import EventEmitter from "events";
import crypto from "crypto";
// @ts-ignore
import LRU from "lru";
import debug0 from "debug";
// @ts-ignore
import Protomux from "protomux";
import {Packet} from "./messages.js";
// @ts-ignore
import c from "compact-encoding"
import b4a from "b4a"
const debug = debug0('dht-flood')
const LRU_SIZE = 255
const TTL = 255
const PROTOCOL = "lumeweb.flood"
const FLOOD_SYMBOL = Symbol.for(PROTOCOL)
export default class DHTFlood extends EventEmitter {
private id: Buffer;
private ttl: number;
private messageNumber: number;
private lru: LRU;
private swarm: any;
private mux: any;
constructor({
lruSize = LRU_SIZE,
ttl = TTL,
messageNumber = 0,
id = crypto.randomBytes(32),
swarm = null
} = {}) {
super()
this.id = id
this.ttl = ttl
this.messageNumber = messageNumber
this.lru = new LRU(lruSize)
if (!swarm) {
throw new Error('swarm is required');
}
this.swarm = swarm;
this.swarm.on("connection", (peer: any) => {
const mux = Protomux.from(peer);
mux.pair({protocol: PROTOCOL}, () => this.setupPeer(peer));
});
}
private handleMessage({originId, messageNumber, ttl, data}: Packet, messenger: any) {
const originIdBuf = b4a.from(originId) as Buffer;
// Ignore messages from ourselves
if (originIdBuf.equals(this.id)) return debug('Got message from self', originId, messageNumber)
// Ignore messages we've already seen
const key = originIdBuf.toString('hex') + messageNumber
if (this.lru.get(key)) return debug('Got message that was already seen', originId, messageNumber)
this.lru.set(key, true)
this.emit('message', data, originId, messageNumber)
if (ttl <= 0) return debug('Got message at end of TTL', originId, messageNumber, ttl)
messenger.send({
originId,
messageNumber,
data,
ttl: ttl - 1
});
}
private setupPeer(peer: any) {
const mux = Protomux.from(peer);
let chan: any;
if (!mux.opened({protocol: PROTOCOL})) {
chan = mux.createChannel({
protocol: PROTOCOL,
});
peer[FLOOD_SYMBOL] = chan;
}
chan = peer[FLOOD_SYMBOL];
if (!chan) {
throw new Error('could not find channel');
}
if (!chan.messages.length) {
chan.addMessage({
encoding: {
preencode: (state: any, m: any) => c.raw.preencode(state, Packet.encode(m).finish()),
encode: (state: any, m: any) => c.raw.encode(state, Packet.encode(m).finish()),
decode: (state: any) => Packet.decode(c.raw.decode(state)),
},
onmessage: (msg: any) => this.handleMessage(msg, chan.messages[0]),
})
}
if (!chan.opened) {
chan.open();
}
return chan.messages[0];
}
broadcast(data: any, ttl = this.ttl) {
this.messageNumber++
const {id, messageNumber} = this
for (const peer of this.swarm.connections.values()) {
const message = this.setupPeer(peer);
message.send({
originId: id,
messageNumber,
ttl,
data
})
}
}
}

149
src/messages.ts Normal file
View File

@ -0,0 +1,149 @@
/* eslint-disable */
import * as _m0 from "protobufjs/minimal";
export const protobufPackage = "";
/** type=0 */
export interface Packet {
originId: Uint8Array;
messageNumber: number;
ttl: number;
data: Uint8Array;
}
function createBasePacket(): Packet {
return { originId: new Uint8Array(), messageNumber: 0, ttl: 0, data: new Uint8Array() };
}
export const Packet = {
encode(message: Packet, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.originId.length !== 0) {
writer.uint32(10).bytes(message.originId);
}
if (message.messageNumber !== 0) {
writer.uint32(16).uint32(message.messageNumber);
}
if (message.ttl !== 0) {
writer.uint32(24).uint32(message.ttl);
}
if (message.data.length !== 0) {
writer.uint32(34).bytes(message.data);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): Packet {
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBasePacket();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.originId = reader.bytes();
break;
case 2:
message.messageNumber = reader.uint32();
break;
case 3:
message.ttl = reader.uint32();
break;
case 4:
message.data = reader.bytes();
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},
fromJSON(object: any): Packet {
return {
originId: isSet(object.originId) ? bytesFromBase64(object.originId) : new Uint8Array(),
messageNumber: isSet(object.messageNumber) ? Number(object.messageNumber) : 0,
ttl: isSet(object.ttl) ? Number(object.ttl) : 0,
data: isSet(object.data) ? bytesFromBase64(object.data) : new Uint8Array(),
};
},
toJSON(message: Packet): unknown {
const obj: any = {};
message.originId !== undefined &&
(obj.originId = base64FromBytes(message.originId !== undefined ? message.originId : new Uint8Array()));
message.messageNumber !== undefined && (obj.messageNumber = Math.round(message.messageNumber));
message.ttl !== undefined && (obj.ttl = Math.round(message.ttl));
message.data !== undefined &&
(obj.data = base64FromBytes(message.data !== undefined ? message.data : new Uint8Array()));
return obj;
},
fromPartial<I extends Exact<DeepPartial<Packet>, I>>(object: I): Packet {
const message = createBasePacket();
message.originId = object.originId ?? new Uint8Array();
message.messageNumber = object.messageNumber ?? 0;
message.ttl = object.ttl ?? 0;
message.data = object.data ?? new Uint8Array();
return message;
},
};
declare var self: any | undefined;
declare var window: any | undefined;
declare var global: any | undefined;
var globalThis: any = (() => {
if (typeof globalThis !== "undefined") {
return globalThis;
}
if (typeof self !== "undefined") {
return self;
}
if (typeof window !== "undefined") {
return window;
}
if (typeof global !== "undefined") {
return global;
}
throw "Unable to locate global object";
})();
function bytesFromBase64(b64: string): Uint8Array {
if (globalThis.Buffer) {
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"));
} else {
const bin = globalThis.atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
arr[i] = bin.charCodeAt(i);
}
return arr;
}
}
function base64FromBytes(arr: Uint8Array): string {
if (globalThis.Buffer) {
return globalThis.Buffer.from(arr).toString("base64");
} else {
const bin: string[] = [];
arr.forEach((byte) => {
bin.push(String.fromCharCode(byte));
});
return globalThis.btoa(bin.join(""));
}
}
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T
: T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}

62
test.js Normal file
View File

@ -0,0 +1,62 @@
const test = require('tape')
const Hyperswarm = require('hyperswarm')
const sodium = require('sodium-universal')
const b4a = require('b4a')
const { default: DHTFlood } = require('./')
const crypto = require('crypto')
const topicName = crypto.randomBytes(10)
test('Broadcast through several peers', (t) => {
const peer1 = createPeer()
const peer2 = createPeer()
const peer3 = createPeer()
t.plan(2)
Promise.all([peer1, peer2, peer3]).then((peers) => {
const peer1 = peers.shift()
const peer2 = peers.shift()
const peer3 = peers.shift()
const flood1 = new DHTFlood({ swarm: peer1 })
const flood2 = new DHTFlood({ swarm: peer2 })
const flood3 = new DHTFlood({ swarm: peer3 })
const data = Buffer.from('Hello World')
flood1.on('message', () => t.error('Got own message'))
flood2.on('message', (message) => {
t.deepEquals(message, data, 'Data got broadcast')
})
flood3.on('message', (message) => {
t.deepEquals(message, data, 'Data got broadcast')
})
function maybeFlood () {
if (peer1.peers.size === 2) {
flood1.broadcast(data)
}
}
peer1.on('connection', () => {
maybeFlood()
})
t.teardown(() => {
[peer1, peer2, peer3].forEach((item) => item.destroy())
})
})
})
async function createPeer () {
const swarm = new Hyperswarm()
await swarm.dht.ready()
await swarm.listen()
const topic = b4a.allocUnsafe(32)
sodium.crypto_generichash(topic, b4a.from(topicName))
swarm.join(topic)
return swarm
}

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"declaration": true,
"strict": true,
"module": "commonjs",
"target": "esnext",
"esModuleInterop": true,
"sourceMap": false,
"rootDir": "src",
"outDir": "dist",
"typeRoots": [
"node_modules/@types",
],
"moduleResolution": "node",
"declarationMap": true,
"declarationDir": "dist",
"emitDeclarationOnly": false,
"allowJs": true
},
"include": [
"src"
]
}