diff --git a/LICENSE b/LICENSE index 13a5b6e..8995c8b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Lume Web +Copyright (c) 2022 Hammer Technologies LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package.json b/package.json new file mode 100644 index 0000000..c1e239e --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "@lumeweb/dht-web", + "type": "module", + "version": "0.1.0", + "devDependencies": { + "@types/ws": "^8.5.3", + "prettier": "^2.7.1" + }, + "dependencies": { + "@derhuerst/round-robin-scheduler": "^1.0.4", + "@hyperswarm/dht-relay": "^0.3.0", + "libkernel": "^0.1.41", + "libkmodule": "^0.2.12", + "libskynet": "^0.0.48", + "websocket-pool": "^1.3.1" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7638170 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,145 @@ +// @ts-ignore +import { DhtNode } from "@hyperswarm/dht-relay"; +// @ts-ignore +import Stream from "@hyperswarm/dht-relay/ws"; +// @ts-ignore +import createPool from "websocket-pool"; +// @ts-ignore +import createRoundRobin from "@derhuerst/round-robin-scheduler"; +import { Buffer } from "buffer"; +import { blake2b } from "libskynet"; +import { registryRead } from "libkernel"; +import { errTuple } from "libskynet"; + +const REGISTRY_DHT_KEY = "lumeweb-dht-relay"; +const IP_REGEX = + /^(?:(?:2[0-4]\d|25[0-5]|1\d{2}|[1-9]?\d)\.){3}(?:2[0-4]\d|25[0-5]|1\d{2}|[1-9]?\d)$/; + +export default class DHT { + private _dht: DhtNode; + private _wsPool: DhtNode; + + constructor() { + this._wsPool = createPool(WebSocket, createRoundRobin); + this._dht = new DhtNode(new Stream(true, this._wsPool)); + return this.setupProxy(); + } + + static get IS_WEB() { + return true; + } + + private _relays: { [pubkey: string]: () => void } = {}; + + get relays(): string[] { + return Object.keys(this._relays); + } + + public async addRelay(pubkey: string): Promise { + let entry: errTuple = await registryRead( + stringToUint8ArrayUtf8(pubkey), + hashDataKey(REGISTRY_DHT_KEY) + ); + + if (entry[1] || !entry[0]?.exists) { + return false; + } + + const host = Buffer.from(entry[0].entryData).toString("utf8") as string; + const [ip, port] = host.split("."); + if (!IP_REGEX.test(ip)) { + return false; + } + + if (isNaN(parseInt(port))) { + return false; + } + + const connection = `ws://${ip}:${port}/`; + + if (!(await this.isServerAvailable(connection))) { + return false; + } + + this._relays[pubkey] = this._wsPool.add(connection); + + return true; + } + + public removeRelay(pubkey: string): boolean { + if (!(pubkey in this._relays)) { + return false; + } + + this._relays[pubkey](); + delete this._relays[pubkey]; + + return true; + } + + public clearRelays(): void { + this._wsPool.close(); + this._relays = {}; + } + + private async isServerAvailable(connection: string): Promise { + return new Promise((resolve) => { + const ws = new WebSocket(connection); + ws.addEventListener("open", () => { + ws.close(); + resolve(true); + }); + ws.addEventListener("error", () => { + resolve(false); + }); + }); + } + + private setupProxy() { + return new Proxy(this, { + get(target, name: string) { + if (!target.hasOwnProperty(name)) { + if (!target._dht.hasOwnProperty(name)) { + throw new Error(`Cannot access the ${name} property`); + } + return target._dht[target]; + } else { + // @ts-ignore + return target[name]; + } + }, + has(target, name: string) { + if (!target.hasOwnProperty(name)) { + return target._dht.hasOwnProperty(name); + } + + return true; + }, + }); + } +} + +export function hashDataKey(dataKey: string): Uint8Array { + return blake2b(encodeUtf8String(dataKey)); +} + +function encodeUtf8String(str: string): Uint8Array { + const byteArray = stringToUint8ArrayUtf8(str); + const encoded = new Uint8Array(8 + byteArray.length); + encoded.set(encodeNumber(byteArray.length)); + encoded.set(byteArray, 8); + return encoded; +} + +function stringToUint8ArrayUtf8(str: string): Uint8Array { + return Uint8Array.from(Buffer.from(str, "utf-8")); +} + +function encodeNumber(num: number): Uint8Array { + const encoded = new Uint8Array(8); + for (let index = 0; index < encoded.length; index++) { + encoded[index] = num & 0xff; + num = num >> 8; + } + return encoded; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a4d5cd4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "declaration": true, + "strict": true, + "module": "esnext", + "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" + ] +}