*Initial version

This commit is contained in:
Derrick Hammer 2022-06-27 13:53:00 -04:00
parent 8a3e04fe01
commit c8d422f1c9
9 changed files with 312 additions and 1 deletions

View File

@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "@lumeweb/relay",
"type": "module",
"version": "0.1.0",
"description": "",
"main": "build/index.js",
"dependencies": {
"@hyperswarm/dht": "^6.0.1",
"@hyperswarm/dht-relay": "^0.3.0",
"@types/node": "^18.0.0",
"@types/ws": "^8.5.3",
"async-mutex": "^0.3.2",
"jayson": "^3.6.6",
"json-stable-stringify": "^1.0.1",
"libskynet": "^0.0.48",
"libskynetnode": "^0.1.3",
"msgpackr": "^1.6.1",
"node-cache": "^5.1.2",
"random-access-memory": "^4.1.0"
},
"devDependencies": {
"hyper-typings": "^1.0.0",
"prettier": "^2.7.1"
}
}

46
src/dht.ts Normal file
View File

@ -0,0 +1,46 @@
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const DHT = require("@hyperswarm/dht");
import { errorExit } from "./util.js";
import {
deriveMyskyRootKeypair,
ed25519Keypair,
seedPhraseToSeed,
validSeedPhrase,
} from "libskynet";
let server: {
listen: (arg0: ed25519Keypair) => void;
ready: () => any;
};
async function start() {
const RELAY_SEED = process.env.RELAY_SEED ?? null;
if (!RELAY_SEED) {
errorExit("RELAY_SEED missing. Aborting.");
}
let [, err] = validSeedPhrase(RELAY_SEED as string);
if (err !== null) {
errorExit("RELAY_SEED is invalid. Aborting.");
}
const keyPair = deriveMyskyRootKeypair(
seedPhraseToSeed(RELAY_SEED as string)[0]
);
const node = new DHT({ keyPair });
await node.ready();
return (server = node);
}
export async function get() {
if (!server) {
return start();
}
return server;
}

11
src/index.ts Normal file
View File

@ -0,0 +1,11 @@
import { start as startRpc } from "./rpc.js";
import { start as startRelay } from "./relay.js";
startRelay();
startRpc();
process.on("uncaughtException", function (err) {
console.log("Caught exception: " + err);
});
export {};

57
src/relay.ts Normal file
View File

@ -0,0 +1,57 @@
import WS from "ws";
// @ts-ignore
import DHT from "@hyperswarm/dht";
// @ts-ignore
import { relay } from "@hyperswarm/dht-relay";
// @ts-ignore
import Stream from "@hyperswarm/dht-relay/ws";
import { get as getDHT } from "./dht.js";
import { overwriteRegistryEntry } from "libskynetnode/dist";
import { Buffer } from "buffer";
import { blake2b } from "libskynet/dist";
export async function start() {
const RELAY_PORT = process.env.RELAY_PORT ?? (8080 as unknown as string);
const server = new WS.Server({
port: RELAY_PORT as unknown as number,
});
const dht = await getDHT();
await overwriteRegistryEntry(
dht.defaultKeyPair,
hashDataKey("lume-dht-relay"),
stringToUint8ArrayUtf8(`${dht.localAddress()}:${RELAY_PORT}`)
);
server.on("connection", (socket) => {
relay(dht, new Stream(false, socket));
});
}
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;
}

149
src/rpc.ts Normal file
View File

@ -0,0 +1,149 @@
import crypto from "crypto";
import jayson from "jayson/promise/index.js";
import { pack, unpack } from "msgpackr";
import { Mutex } from "async-mutex";
import { createRequire } from "module";
import NodeCache from "node-cache";
import { get as getDHT } from "./dht.js";
const require = createRequire(import.meta.url);
const stringify = require("json-stable-stringify");
const clients: { [chain: string]: any } = {};
const pendingRequests = new NodeCache();
const processedRequests = new NodeCache({
stdTTL: 60 * 60 * 12,
});
interface RPCRequest {
force: boolean;
chain: string;
query: string;
data: string;
}
interface RPCResponse {
updated: number;
data:
| any
| {
error: string | boolean;
};
}
function hash(data: string): string {
return crypto.createHash("sha256").update(data).digest("hex");
}
function getClient(chain: string): Function {
chain = chain.replace(/[^a-z0-9\-]/g, "");
if (!(chain in clients)) {
clients[chain] = jayson.Client.http({
host: process.env.RPC_PROXY_HOST,
port: parseInt(process.env.RPC_PROXY_PORT as string),
path: "/",
headers: {
"X-Chain": chain,
},
});
}
return clients[chain];
}
function getRequestId(request: RPCRequest) {
const clonedRequest = Object.assign({}, request);
// @ts-ignore
delete clonedRequest.force;
return hash(stringify(clonedRequest));
}
function maybeProcessRequest(item: any) {
let request: RPCRequest = unpack(item) as RPCRequest;
if (!request.chain) {
throw new Error("RPC chain missing");
}
if (!request.data) {
throw new Error("RPC data missing");
}
return processRequest(request);
}
async function processRequest(request: RPCRequest): Promise<RPCResponse> {
const reqId = getRequestId(request);
let lock: Mutex = pendingRequests.get(reqId) as Mutex;
const lockExists = !!lock;
if (!lockExists) {
lock = new Mutex();
pendingRequests.set(reqId, lock);
}
if (lock.isLocked()) {
await lock.waitForUnlock();
return processedRequests.get(reqId) as RPCResponse;
}
await lock.acquire();
if (!request.force && processedRequests.get(reqId)) {
return processedRequests.get(reqId) as RPCResponse;
}
let rpcResp;
let error;
try {
// @ts-ignore
rpcResp = await getClient(request.chain).request(
request.query,
request.data
);
} catch (e) {
error = (e as Error).message;
}
let dbData: RPCResponse = {
updated: Date.now(),
data: "",
};
if (rpcResp) {
if (false === rpcResp.result) {
error = true;
}
if (rpcResp.error) {
error = rpcResp.error.message;
}
}
dbData.data = error ? { error } : rpcResp.result;
if (!processedRequests.get(reqId) || request.force) {
processedRequests.set(reqId, dbData);
}
await lock.release();
return dbData;
}
export async function start() {
(await getDHT()).on("connection", (socket: any) => {
socket.on("data", async (data: any) => {
try {
socket.write(pack(await maybeProcessRequest(data)));
} catch (error) {
console.trace(error);
socket.write(pack({ error }));
}
socket.end();
});
});
}

4
src/util.ts Normal file
View File

@ -0,0 +1,4 @@
export function errorExit(msg: string): void {
console.log(msg);
process.exit(1);
}

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"rootDir": "src",
"outDir": "build",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

6
tslint.json Normal file
View File

@ -0,0 +1,6 @@
{
"defaultSeverity": "error",
"extends": ["tslint:latest", "tslint-config-prettier"],
"jsRules": {},
"rulesDirectory": []
}