diff --git a/package.json b/package.json index 91cd61b..6ff15e5 100644 --- a/package.json +++ b/package.json @@ -12,20 +12,21 @@ "@hyperswarm/dht": "^6.0.1", "@hyperswarm/dht-relay": "^0.3.0", "@pokt-network/pocket-js": "^0.8.0-rc", - "@root/greenlock": "^4.0.5", "@solana/web3.js": "^1.47.3", + "@types/acme-client": "^3.3.0", "@types/node": "^18.0.0", "@types/node-cron": "^3.0.2", "@types/ws": "^8.5.3", "@types/xml2js": "^0.4.11", + "acme-client": "^4.2.5", "algosdk": "^1.18.1", "async-mutex": "^0.3.2", "bcfg": "^0.1.7", + "date-fns": "^2.28.0", "dotenv": "^16.0.1", "ethers": "^5.6.9", "express": "^4.18.1", - "greenlock-express": "^4.0.3", - "hsd": "^4.0.1", + "hsd": "https://github.com/LumeWeb/hsd.git#spv-namestate", "jayson": "^3.6.6", "json-stable-stringify": "^1.0.1", "libskynet": "^0.0.48", @@ -44,6 +45,7 @@ "@types/express": "^4.17.13", "@types/minimatch": "^3.0.5", "@types/sprintf-js": "^1.1.2", + "esbuild": "^0.14.49", "hyper-typings": "^1.0.0", "prettier": "^2.7.1" } diff --git a/src/config.ts b/src/config.ts index 6b7f67b..81c894a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,35 +1,36 @@ // @ts-ignore import BConfig from "bcfg"; -import { errorExit } from "./util.js"; +import {errorExit} from "./util.js"; const config = new BConfig("lumeweb-relay"); config.inject({ - relayPort: 8080, + relayPort: 8080, }); config.load({ - env: true, - argv: true, + env: true, + argv: true, }); try { - config.open("config.conf"); -} catch (e) {} + config.open("config.conf"); +} catch (e) { +} for (const setting of ["relay-domain", "afraid-username", "relay-seed"]) { - if (!config.get(setting)) { - errorExit(`Required config option ${setting} not set`); - } + if (!config.get(setting)) { + errorExit(`Required config option ${setting} not set`); + } } let usingPocketGateway = true; export function usePocketGateway() { - return usingPocketGateway; + return usingPocketGateway; } export function updateUsePocketGateway(state: boolean): void { - usingPocketGateway = state; + usingPocketGateway = state; } export default config; diff --git a/src/dht.ts b/src/dht.ts index 973d410..216b811 100644 --- a/src/dht.ts +++ b/src/dht.ts @@ -1,41 +1,57 @@ -import { createRequire } from "module"; +import {createRequire} from "module"; + const require = createRequire(import.meta.url); const DHT = require("@hyperswarm/dht"); -import { errorExit } from "./util.js"; +import {errorExit} from "./util.js"; import { - deriveMyskyRootKeypair, - ed25519Keypair, - seedPhraseToSeed, - validSeedPhrase, + deriveMyskyRootKeypair, + ed25519Keypair, + seedPhraseToSeed, + validSeedPhrase, } from "libskynet"; import config from "./config.js"; +let node: { + ready: () => any; + createServer: () => any; + defaultKeyPair: any; + on: any; +}; let server: { - listen: (arg0: ed25519Keypair) => void; - ready: () => any; + listen: (arg0: ed25519Keypair) => any; + on: any; }; async function start() { - const seed = config.str("relay-seed"); + const seed = config.str("relay-seed"); - let [, err] = validSeedPhrase(seed); - if (err !== null) { - errorExit("RELAY_SEED is invalid. Aborting."); - } + let [, err] = validSeedPhrase(seed); + if (err !== null) { + errorExit("RELAY_SEED is invalid. Aborting."); + } - const keyPair = deriveMyskyRootKeypair(seedPhraseToSeed(seed)[0]); + const keyPair = deriveMyskyRootKeypair(seedPhraseToSeed(seed)[0]); - const node = new DHT({ keyPair }); + node = new DHT({keyPair}); - await node.ready(); + await node.ready(); - return (server = node); + server = node.createServer(); + await server.listen(keyPair); + + return node; } -export async function get() { - if (!server) { - return start(); - } +export async function get( + ret: "server" | "dht" = "dht" +): Promise { + if (!node) { + await start(); + } - return server; + if (ret == "server") { + return server; + } + + return node; } diff --git a/src/dns.ts b/src/dns.ts index 1c75a2d..bac13c0 100644 --- a/src/dns.ts +++ b/src/dns.ts @@ -1,122 +1,96 @@ import cron from "node-cron"; import fetch from "node-fetch"; -import { get as getDHT } from "./dht.js"; -import { overwriteRegistryEntry } from "libskynetnode"; -import { Buffer } from "buffer"; -import { blake2b } from "libskynet"; -import { Parser } from "xml2js"; -import { URL } from "url"; -import { errorExit } from "./util.js"; -import { pack } from "msgpackr"; +import {get as getDHT} from "./dht.js"; +import {overwriteRegistryEntry} from "libskynetnode"; +import {Buffer} from "buffer"; +import {Parser} from "xml2js"; +import {URL} from "url"; +import {errorExit, hashDataKey} from "./util.js"; +import {pack} from "msgpackr"; import config from "./config.js"; -const { createHash } = await import("crypto"); +const {createHash} = await import("crypto"); let activeIp: string; -const REGISTRY_DHT_KEY = "lumeweb-dht-relay"; +const REGISTRY_NODE_KEY = "lumeweb-dht-node"; async function ipUpdate() { - let currentIp = await getCurrentIp(); + let currentIp = await getCurrentIp(); - if (activeIp && currentIp === activeIp) { - return; - } + if (activeIp && currentIp === activeIp) { + return; + } - let domain = await getDomainInfo(); + let domain = await getDomainInfo(); - await fetch(domain.url[0].toString()); + await fetch(domain.url[0].toString()); - activeIp = domain.address[0]; + activeIp = domain.address[0]; } export async function start() { - const dht = await getDHT(); + const dht = (await getDHT()) as any; - await ipUpdate(); + await ipUpdate(); - await overwriteRegistryEntry( - dht.defaultKeyPair, - hashDataKey(REGISTRY_DHT_KEY), - pack(`${config.str("relay-domain")}:${config.uint("relay-port")}`) - ); + await overwriteRegistryEntry( + dht.defaultKeyPair, + hashDataKey(REGISTRY_NODE_KEY), + pack(`${config.str("relay-domain")}:${config.uint("relay-port")}`) + ); - cron.schedule("0 * * * *", ipUpdate); -} + console.log( + "node pubkey:", + Buffer.from(dht.defaultKeyPair.publicKey).toString("hex") + ); -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; + cron.schedule("0 * * * *", ipUpdate); } async function getDomainInfo() { - const relayDomain = config.str("relay-domain"); - const parser = new Parser(); + const relayDomain = config.str("relay-domain"); + const parser = new Parser(); - const url = new URL("https://freedns.afraid.org/api/"); + const url = new URL("https://freedns.afraid.org/api/"); - const params = url.searchParams; + const params = url.searchParams; - params.append("action", "getdyndns"); - params.append("v", "2"); - params.append("style", "xml"); + params.append("action", "getdyndns"); + params.append("v", "2"); + params.append("style", "xml"); - const hash = createHash("sha1"); - hash.update( - `${config.str("afraid-username")}|${config.str("afraid-password")}` - ); + const hash = createHash("sha1"); + hash.update( + `${config.str("afraid-username")}|${config.str("afraid-password")}` + ); - params.append("sha", hash.digest().toString("hex")); + params.append("sha", hash.digest().toString("hex")); - const response = await (await fetch(url.toString())).text(); + const response = await (await fetch(url.toString())).text(); - if (/could not authenticate/i.test(response)) { - errorExit("Failed to authenticate to afraid.org"); - } - - const json = await parser.parseStringPromise(response); - - let domain = null; - - for (const item of json.xml.item) { - if (item.host[0] === relayDomain) { - domain = item; - break; + if (/could not authenticate/i.test(response)) { + errorExit("Failed to authenticate to afraid.org"); } - } - if (!domain) { - errorExit(`Domain ${relayDomain} not found in afraid.org account`); - } + const json = await parser.parseStringPromise(response); - return domain; + let domain = null; + + for (const item of json.xml.item) { + if (item.host[0] === relayDomain) { + domain = item; + break; + } + } + + if (!domain) { + errorExit(`Domain ${relayDomain} not found in afraid.org account`); + } + + return domain; } async function getCurrentIp(): Promise { - const response = await (await fetch("http://checkip.dyndns.org")).text(); - const parser = new Parser(); - - const html = await parser.parseStringPromise(response.trim()); - - return html.html.body[0].split(":").pop(); + return await (await fetch("http://ip1.dynupdate.no-ip.com/")).text(); } diff --git a/src/index.ts b/src/index.ts index fa7bf76..753c259 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,13 @@ -import { start as startDns } from "./dns.js"; -import { start as startRpc } from "./rpc.js"; -import { start as startRelay } from "./relay.js"; +import {start as startDns} from "./dns.js"; +import {start as startRpc} from "./rpc.js"; +import {start as startRelay} from "./relay.js"; await startDns(); await startRpc(); await startRelay(); process.on("uncaughtException", function (err) { - console.log("Caught exception: " + err); + console.log("Caught exception: " + err); }); export {}; diff --git a/src/relay.ts b/src/relay.ts index b49ac4f..186c766 100644 --- a/src/relay.ts +++ b/src/relay.ts @@ -1,69 +1,118 @@ -import WS from "ws"; - // @ts-ignore import DHT from "@hyperswarm/dht"; // @ts-ignore -import { relay } from "@hyperswarm/dht-relay"; +import {relay} from "@hyperswarm/dht-relay"; // @ts-ignore import Stream from "@hyperswarm/dht-relay/ws"; -import { get as getDHT } from "./dht.js"; -// @ts-ignore -import GLE from "greenlock-express"; -// @ts-ignore -import Greenlock from "@root/greenlock"; +import express, {Express} from "express"; import path from "path"; -import { fileURLToPath } from "url"; +import {fileURLToPath} from "url"; import config from "./config.js"; +import * as http from "http"; +import * as https from "https"; +import * as tls from "tls"; +import * as acme from "acme-client"; +import {Buffer} from "buffer"; +import {intervalToDuration} from "date-fns"; +import cron from "node-cron"; +import {get as getDHT} from "./dht.js"; +import WS from "ws"; +// @ts-ignore +import DHT from "@hyperswarm/dht"; +import {pack} from "msgpackr"; +import {overwriteRegistryEntry} from "libskynetnode"; +import {hashDataKey} from "./util.js"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +let sslCtx: tls.SecureContext = tls.createSecureContext(); +const sslParams: tls.SecureContextOptions = {}; -const sslConfig = { - packageRoot: path.dirname(__dirname), - maintainerEmail: "contact@lumeweb.com", - configDir: path.resolve(config.prefix, "ssl"), - cluster: false, - agreeToTerms: true, - staging: true, -}; +const sslPrivateKey = await acme.forge.createPrivateKey(); +const acmeClient = new acme.Client({ + accountKey: sslPrivateKey, + directoryUrl: acme.directory.letsencrypt.production, +}); + +let app: Express; +let router = express.Router(); export async function start() { - const relayDomain = config.str("relay-domain"); - const relayPort = config.str("relay-port"); - const greenlock = Greenlock.create(sslConfig); - await greenlock.add({ - subject: relayDomain, - altnames: [relayDomain], - }); - // @ts-ignore - config.greenlock = greenlock; - GLE.init(sslConfig).ready(async (GLEServer: any) => { - let httpsServer = GLEServer.httpsServer(); - var httpServer = GLEServer.httpServer(); - - await new Promise((resolve) => { - httpServer.listen(80, "0.0.0.0", function () { - console.info("HTTP Listening on ", httpServer.address()); - resolve(null); - }); + const relayPort = config.str("relay-port"); + app = express(); + app.use(function (req, res, next) { + router(req, res, next); }); + let httpsServer = https.createServer({ + SNICallback(servername, cb) { + cb(null, sslCtx); + }, + }); + + let httpServer = http.createServer(app); + + cron.schedule("0 * * * *", createOrRenewSSl); + + await new Promise((resolve) => { + httpServer.listen(80, "0.0.0.0", function () { + console.info("HTTP Listening on ", httpServer.address()); + resolve(null); + }); + }); const dht = await getDHT(); - let wsServer = new WS.Server({ server: httpServer }); + let wsServer = new WS.Server({server: httpsServer}); wsServer.on("connection", (socket: any) => { - relay(dht, new Stream(false, socket)); - }); - await new Promise((resolve) => { - httpsServer.listen(relayPort, "0.0.0.0", function () { - console.info("Relay started on ", httpsServer.address()); - resolve(null); - }); + relay(dht, new Stream(false, socket)); }); - await greenlock.get({ - servername: relayDomain, + await new Promise((resolve) => { + httpsServer.listen(relayPort, "0.0.0.0", function () { + console.info("Relay started on ", httpsServer.address()); + resolve(null); + }); }); - }); + + await createOrRenewSSl(); +} + +async function createOrRenewSSl() { + if (sslParams.cert) { + const expires = ( + await acme.forge.readCertificateInfo(sslParams.cert as Buffer) + ).notAfter; + + let duration = intervalToDuration({start: new Date(), end: expires}); + + let daysLeft = (duration.months as number) * 30 + (duration.days as number); + + if (daysLeft > 30) { + return; + } + } + + const [certificateKey, certificateRequest] = await acme.forge.createCsr({ + commonName: config.str("relay-domain"), + }); + + sslParams.cert = await acmeClient.auto({ + csr: certificateRequest, + termsOfServiceAgreed: true, + challengeCreateFn: async (authz, challenge, keyAuthorization) => { + router.get( + `/.well-known/acme-challenge/${challenge.token}`, + (req, res) => { + res.send(keyAuthorization); + } + ); + }, + challengeRemoveFn: async () => { + router = express.Router(); + }, + challengePriority: ["http-01"], + }); + sslParams.key = certificateKey; + sslCtx = tls.createSecureContext(sslParams); + + console.log("SSL Certificate Updated"); } diff --git a/src/rpc.ts b/src/rpc.ts index 37e5702..08174f6 100644 --- a/src/rpc.ts +++ b/src/rpc.ts @@ -1,27 +1,28 @@ 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 {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"; -import { rpcMethods } from "./rpc/index.js"; +import {get as getDHT} from "./dht.js"; +import {rpcMethods} from "./rpc/index.js"; import PocketPKG from "@pokt-network/pocket-js"; -const { Configuration, HttpRpcProvider, PocketAAT, Pocket } = PocketPKG; + +const {Configuration, HttpRpcProvider, PocketAAT, Pocket} = PocketPKG; import { - JSONRPCRequest, - JSONRPCResponseWithError, - JSONRPCResponseWithResult, + JSONRPCRequest, + JSONRPCResponseWithError, + JSONRPCResponseWithResult, } from "jayson"; -import config, { updateUsePocketGateway, usePocketGateway } from "./config.js"; -import { errorExit } from "./util.js"; +import config, {updateUsePocketGateway, usePocketGateway} from "./config.js"; +import {errorExit} from "./util.js"; const require = createRequire(import.meta.url); const stringify = require("json-stable-stringify"); const pendingRequests = new NodeCache(); const processedRequests = new NodeCache({ - stdTTL: 60 * 60 * 12, + stdTTL: 60 * 60 * 12, }); type PocketAATObject = typeof PocketAAT; @@ -31,232 +32,235 @@ let _aat: PocketAATObject; let jsonServer: jayson.Server; interface RPCRequest { - force: boolean; - chain: string; - query: string; - data: string; + force: boolean; + chain: string; + query: string; + data: string; } interface RPCResponse { - updated: number; - data: - | any - | { + updated: number; + data: + | any + | { error: string | boolean; - }; + }; } function hash(data: string): string { - return crypto.createHash("sha256").update(data).digest("hex"); + return crypto.createHash("sha256").update(data).digest("hex"); } function getRequestId(request: RPCRequest) { - const clonedRequest = Object.assign({}, request); + const clonedRequest = Object.assign({}, request); - // @ts-ignore - delete clonedRequest.force; + // @ts-ignore + delete clonedRequest.force; - return hash(stringify(clonedRequest)); + return hash(stringify(clonedRequest)); } function maybeProcessRequest(request: RPCRequest) { - if (!request.chain) { - throw new Error("RPC chain missing"); - } + if (!request.chain) { + throw new Error("RPC chain missing"); + } - if (!request.data) { - throw new Error("RPC data missing"); - } + if (!request.data) { + throw new Error("RPC data missing"); + } - return processRequest(request); + return processRequest(request); } async function processRequest(request: RPCRequest): Promise { - const reqId = getRequestId(request); + const reqId = getRequestId(request); - let lock: Mutex = pendingRequests.get(reqId) as Mutex; - const lockExists = !!lock; + 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 { - rpcResp = await processRpcRequest( - { - method: request.query, - jsonrpc: "2.0", - params: request.data, - } as unknown as JSONRPCRequest, - request.chain - ); - } catch (e) { - error = (e as Error).message; - } - - let dbData: RPCResponse = { - updated: Date.now(), - data: "", - }; - - if (rpcResp) { - rpcResp = rpcResp as JSONRPCResponseWithResult; - if (false === rpcResp.result) { - error = true; + if (!lockExists) { + lock = new Mutex(); + pendingRequests.set(reqId, lock); } - rpcResp = rpcResp as unknown as JSONRPCResponseWithError; - - if (rpcResp.error) { - // @ts-ignore - error = rpcResp.error.message; + if (lock.isLocked()) { + await lock.waitForUnlock(); + return processedRequests.get(reqId) as RPCResponse; } - } + await lock.acquire(); - dbData.data = error - ? { error } - : (rpcResp as unknown as JSONRPCResponseWithResult).result; + if (!request.force && processedRequests.get(reqId)) { + return processedRequests.get(reqId) as RPCResponse; + } - if (!processedRequests.get(reqId) || request.force) { - processedRequests.set(reqId, dbData); - } + let rpcResp; - await lock.release(); + let error; + try { + rpcResp = await processRpcRequest( + { + method: request.query, + jsonrpc: "2.0", + params: request.data, + id: 1 + } as unknown as JSONRPCRequest, + request.chain + ); + } catch (e) { + error = (e as Error).message; + } - return dbData; + let dbData: RPCResponse = { + updated: Date.now(), + data: "", + }; + + if (rpcResp) { + rpcResp = rpcResp as JSONRPCResponseWithResult; + if (false === rpcResp.result) { + error = true; + } + + rpcResp = rpcResp as unknown as JSONRPCResponseWithError; + + if (rpcResp.error) { + // @ts-ignore + error = rpcResp.error.message; + } + } + + dbData.data = error + ? {error} + : (rpcResp as unknown as JSONRPCResponseWithResult).result; + + if (!processedRequests.get(reqId) || request.force) { + processedRequests.set(reqId, dbData); + } + + await lock.release(); + + return dbData; } export function updateAat(aat: PocketAATObject): void { - _aat = aat; + _aat = aat; } export function getAat(): PocketAATObject { - return _aat; + return _aat; } + export function getPocketServer(): typeof Pocket { - return pocketServer; + return pocketServer; } export async function unlockAccount( - accountPrivateKey: string, - accountPublicKey: string, - accountPassphrase: string + accountPrivateKey: string, + accountPublicKey: string, + accountPassphrase: string ): Promise { - try { - // @ts-ignore - const account = await pocketServer.keybase.importAccount( - Buffer.from(accountPrivateKey, "hex"), - accountPassphrase - ); + try { + // @ts-ignore + const account = await pocketServer.keybase.importAccount( + Buffer.from(accountPrivateKey, "hex"), + accountPassphrase + ); - if (account instanceof Error) { - // noinspection ExceptionCaughtLocallyJS - throw account; + if (account instanceof Error) { + // noinspection ExceptionCaughtLocallyJS + throw account; + } + + // @ts-ignore + await pocketServer.keybase.unlockAccount( + account.addressHex, + accountPassphrase, + 0 + ); + + // @ts-ignore + return await PocketAAT.from( + "0.0.1", + accountPublicKey, + accountPublicKey, + accountPrivateKey + ); + } catch (e) { + console.error(e); + process.exit(1); } - - // @ts-ignore - await pocketServer.keybase.unlockAccount( - account.addressHex, - accountPassphrase, - 0 - ); - - // @ts-ignore - return await PocketAAT.from( - "0.0.1", - accountPublicKey, - accountPublicKey, - accountPrivateKey - ); - } catch (e) { - console.error(e); - process.exit(1); - } } export async function processRpcRequest( - request: JSONRPCRequest, - chain: string + request: JSONRPCRequest, + chain: string ): Promise { - return new Promise((resolve) => { - jsonServer.call( - request, - { chain }, - ( - err?: JSONRPCResponseWithError | null, - result?: JSONRPCResponseWithResult - ): void => { - if (err) { - return resolve(err); - } - resolve(result); - } - ); - }); + return new Promise((resolve) => { + jsonServer.call( + request, + {chain}, + ( + err?: JSONRPCResponseWithError | null, + result?: JSONRPCResponseWithResult + ): void => { + if (err) { + return resolve(err); + } + resolve(result); + } + ); + }); } export async function start() { - if (!config.str("pocket-app-id") || !config.str("pocket-app-key")) { - const pocketHost = config.str("pocket-host"); - const pocketPort = config.uint("pocket-port"); - if (!pocketHost || !pocketPort) { - errorExit( - "Please set pocket-host and pocket-port config options if you do not have an API key set" - ); + if (!config.str("pocket-app-id") || !config.str("pocket-app-key")) { + const pocketHost = config.str("pocket-host"); + const pocketPort = config.uint("pocket-port"); + if (!pocketHost || !pocketPort) { + errorExit( + "Please set pocket-host and pocket-port config options if you do not have an API key set" + ); + } + + const dispatchURL = new URL( + `http://${config.str("pocket-host")}:${config.uint("pocket-port")}` + ); + const rpcProvider = new HttpRpcProvider(dispatchURL); + const configuration = new Configuration(); + // @ts-ignore + pocketServer = new Pocket([dispatchURL], rpcProvider, configuration); + updateUsePocketGateway(false); } - const dispatchURL = new URL( - `http://${config.str("pocket-host")}:${config.uint("pocket-port")}` - ); - const rpcProvider = new HttpRpcProvider(dispatchURL); - const configuration = new Configuration(); - // @ts-ignore - pocketServer = new Pocket([dispatchURL], rpcProvider, configuration); - updateUsePocketGateway(false); - } + if (!usePocketGateway()) { + updateAat( + await unlockAccount( + config.str("pocket-account-private-key"), + config.str("pocket-account-public-key"), + "0" + ) + ); + } - if (!usePocketGateway()) { - updateAat( - await unlockAccount( - config.str("pocket-account-private-key"), - config.str("pocket-account-public-key"), - "0" - ) - ); - } + jsonServer = new jayson.Server(rpcMethods, {useContext: true}); - jsonServer = new jayson.Server(rpcMethods, { useContext: true }); + (await getDHT("server")).on("connection", (socket: any) => { + socket.rawStream._ondestroy = () => false; + socket.on("data", async (data: any) => { + let request: RPCRequest; + try { + request = unpack(data) as RPCRequest; + } catch (e) { + return; + } - (await getDHT()).on("connection", (socket: any) => { - socket.on("data", async (data: any) => { - let request: RPCRequest; - try { - request = unpack(data) as RPCRequest; - } catch (e) { - return; - } - - try { - socket.write(pack(await maybeProcessRequest(request))); - } catch (error) { - console.trace(error); - socket.write(pack({ error })); - } - socket.end(); + try { + socket.write(pack(await maybeProcessRequest(request))); + } catch (error) { + console.trace(error); + socket.write(pack({error})); + } + socket.end(); + }); }); - }); } diff --git a/src/rpc/algorand.ts b/src/rpc/algorand.ts index e9d14d0..d912b37 100644 --- a/src/rpc/algorand.ts +++ b/src/rpc/algorand.ts @@ -1,112 +1,112 @@ -import { maybeMapChainId, reverseMapChainId } from "../util.js"; +import {maybeMapChainId, reverseMapChainId} from "../util.js"; import minimatch from "minimatch"; // @ts-ignore import HTTPClient from "algosdk/dist/cjs/src/client/client.js"; -import { sprintf } from "sprintf-js"; -import { RpcMethodList } from "./index.js"; +import {sprintf} from "sprintf-js"; +import {RpcMethodList} from "./index.js"; import config from "../config.js"; const allowedEndpoints: { [endpoint: string]: ("GET" | "POST")[] } = { - "/v2/teal/compile": ["POST"], - "/v2/accounts/*": ["GET"], + "/v2/teal/compile": ["POST"], + "/v2/accounts/*": ["GET"], }; export function proxyRestMethod( - apiServer: string, - matchChainId: string + apiServer: string, + matchChainId: string ): Function { - return async function (args: any, context: object) { - // @ts-ignore - let chain = context.chain; - let chainId = maybeMapChainId(chain); + return async function (args: any, context: object) { + // @ts-ignore + let chain = context.chain; + let chainId = maybeMapChainId(chain); - if (!chainId) { - throw new Error("Invalid Chain"); - } - - chainId = reverseMapChainId(chainId as string); - if (!chainId || chainId !== matchChainId) { - throw new Error("Invalid Chain"); - } - - let method = args.method ?? false; - let endpoint = args.endpoint ?? false; - let data = args.data ?? false; - let query = args.query ?? false; - let fullHeaders = args.fullHeaders ?? {}; - - fullHeaders = { ...fullHeaders, Referer: "lumeweb_dns_relay" }; - - if (method) { - method = method.toUpperCase(); - } - - if (!endpoint) { - throw new Error("Endpoint Missing"); - } - - let found = false; - - for (const theEndpoint in allowedEndpoints) { - if (minimatch(endpoint, theEndpoint)) { - found = true; - break; - } - } - - if (!found) { - throw new Error("Endpoint Invalid"); - } - - let apiUrl; - try { - apiUrl = sprintf(apiServer, chainId, config.str("pocket-app-id")); - } catch (e) { - apiUrl = apiServer; - } - - const client = new HTTPClient({}, apiUrl); - let resp; - switch (method) { - case "GET": - resp = await client.get(endpoint, query, fullHeaders); - break; - case "POST": - if (Array.isArray(data?.data)) { - data = new Uint8Array(Buffer.from(data.data)); + if (!chainId) { + throw new Error("Invalid Chain"); } - resp = await client.post(endpoint, data, { ...fullHeaders }); - break; - default: - throw new Error("Method Invalid"); - } - - const getCircularReplacer = () => { - const seen = new WeakSet(); - return (key: string, value: any): any => { - if (typeof value === "object" && value !== null) { - if (seen.has(value)) { - return; - } - seen.add(value); + chainId = reverseMapChainId(chainId as string); + if (!chainId || chainId !== matchChainId) { + throw new Error("Invalid Chain"); } - return value; - }; + + let method = args.method ?? false; + let endpoint = args.endpoint ?? false; + let data = args.data ?? false; + let query = args.query ?? false; + let fullHeaders = args.fullHeaders ?? {}; + + fullHeaders = {...fullHeaders, Referer: "lumeweb_dns_relay"}; + + if (method) { + method = method.toUpperCase(); + } + + if (!endpoint) { + throw new Error("Endpoint Missing"); + } + + let found = false; + + for (const theEndpoint in allowedEndpoints) { + if (minimatch(endpoint, theEndpoint)) { + found = true; + break; + } + } + + if (!found) { + throw new Error("Endpoint Invalid"); + } + + let apiUrl; + try { + apiUrl = sprintf(apiServer, chainId, config.str("pocket-app-id")); + } catch (e) { + apiUrl = apiServer; + } + + const client = new HTTPClient({}, apiUrl); + let resp; + switch (method) { + case "GET": + resp = await client.get(endpoint, query, fullHeaders); + break; + case "POST": + if (Array.isArray(data?.data)) { + data = new Uint8Array(Buffer.from(data.data)); + } + + resp = await client.post(endpoint, data, {...fullHeaders}); + break; + default: + throw new Error("Method Invalid"); + } + + const getCircularReplacer = () => { + const seen = new WeakSet(); + return (key: string, value: any): any => { + if (typeof value === "object" && value !== null) { + if (seen.has(value)) { + return; + } + seen.add(value); + } + return value; + }; + }; + + return JSON.parse(JSON.stringify(resp, getCircularReplacer())); }; - - return JSON.parse(JSON.stringify(resp, getCircularReplacer())); - }; } export default { - algorand_rest_request: proxyRestMethod( - "http://mainnet-api.algonode.network", - "algorand-mainnet" - ), - //'algorand_rest_request': proxyRestMethod("https://%s.gateway.pokt.network/v1/lb/%s", "algorand-mainnet"), - algorand_rest_indexer_request: proxyRestMethod( - "http://mainnet-idx.algonode.network", - "algorand-mainnet-indexer" - ), + algorand_rest_request: proxyRestMethod( + "http://mainnet-api.algonode.network", + "algorand-mainnet" + ), + //'algorand_rest_request': proxyRestMethod("https://%s.gateway.pokt.network/v1/lb/%s", "algorand-mainnet"), + algorand_rest_indexer_request: proxyRestMethod( + "http://mainnet-idx.algonode.network", + "algorand-mainnet-indexer" + ), } as RpcMethodList; diff --git a/src/rpc/common.ts b/src/rpc/common.ts index caef806..a6b401f 100644 --- a/src/rpc/common.ts +++ b/src/rpc/common.ts @@ -1,106 +1,106 @@ -import { ethers } from "ethers"; -import { Pocket, PocketAAT } from "@pokt-network/pocket-js"; -import { maybeMapChainId, reverseMapChainId } from "../util.js"; -import { Connection } from "@solana/web3.js"; -import { getAat, getPocketServer } from "../rpc.js"; -import config, { usePocketGateway } from "../config.js"; +import {ethers} from "ethers"; +import {Pocket, PocketAAT} from "@pokt-network/pocket-js"; +import {maybeMapChainId, reverseMapChainId} from "../util.js"; +import {Connection} from "@solana/web3.js"; +import {getAat, getPocketServer} from "../rpc.js"; +import config, {usePocketGateway} from "../config.js"; type RpcProviderMethod = (method: string, params: Array) => Promise; const gatewayProviders: { [name: string]: RpcProviderMethod } = {}; const gatewayMethods: { - [name: string]: (chainId: string) => RpcProviderMethod; + [name: string]: (chainId: string) => RpcProviderMethod; } = { - default: (chainId: string): RpcProviderMethod => { - const provider = new ethers.providers.JsonRpcProvider({ - url: `https://${chainId}.gateway.pokt.network/v1/lb/${config.str( - "pocket-api-id" - )}`, - password: config.str("pocket-api-key"), - }); - return provider.send.bind(provider); - }, - "sol-mainnet": (chainId: string): RpcProviderMethod => { - const provider = new Connection( - `https://solana-mainnet.gateway.pokt.network/v1/lb/${config.str( - "pocket-api-id" - )}` - ); + default: (chainId: string): RpcProviderMethod => { + const provider = new ethers.providers.JsonRpcProvider({ + url: `https://${chainId}.gateway.pokt.network/v1/lb/${config.str( + "pocket-api-id" + )}`, + password: config.str("pocket-api-key"), + }); + return provider.send.bind(provider); + }, + "sol-mainnet": (chainId: string): RpcProviderMethod => { + const provider = new Connection( + `https://solana-mainnet.gateway.pokt.network/v1/lb/${config.str( + "pocket-api-id" + )}` + ); - // @ts-ignore - return provider._rpcRequest.bind(provider); - }, + // @ts-ignore + return provider._rpcRequest.bind(provider); + }, }; export function proxyRpcMethod( - method: string, - chains: string[] = [] + method: string, + chains: string[] = [] ): Function { - return async function (args: any, context: object) { - // @ts-ignore - let chain = context.chain; - let chainId = maybeMapChainId(chain); + return async function (args: any, context: object) { + // @ts-ignore + let chain = context.chain; + let chainId = maybeMapChainId(chain); - let chainMatch = true; + let chainMatch = true; - if ( - chains.length > 0 && - !chains.includes(chain) && - !chains.includes(chainId.toString()) - ) { - chainMatch = false; - } + if ( + chains.length > 0 && + !chains.includes(chain) && + !chains.includes(chainId.toString()) + ) { + chainMatch = false; + } - if (!chainId || !chainMatch) { - throw new Error("Invalid Chain"); - } + if (!chainId || !chainMatch) { + throw new Error("Invalid Chain"); + } - if (usePocketGateway()) { - chainId = reverseMapChainId(chainId as string); - if (!chainId) { - throw new Error("Invalid Chain"); - } + if (usePocketGateway()) { + chainId = reverseMapChainId(chainId as string); + if (!chainId) { + throw new Error("Invalid Chain"); + } - let provider: RpcProviderMethod | boolean = - gatewayProviders[chainId as string] || false; - if (!provider) { - provider = getRpcProvider(chainId as string); - } - gatewayProviders[chainId as string] = provider; - return await provider(method, args); - } + let provider: RpcProviderMethod | boolean = + gatewayProviders[chainId as string] || false; + if (!provider) { + provider = getRpcProvider(chainId as string); + } + gatewayProviders[chainId as string] = provider; + return await provider(method, args); + } - return await sendRelay( - JSON.stringify(args), - chainId, - getAat() as unknown as PocketAAT - ); - }; + return await sendRelay( + JSON.stringify(args), + chainId, + getAat() as unknown as PocketAAT + ); + }; } // Call this every time you want to fetch RPC data async function sendRelay( - rpcQuery: string, - blockchain: string, - pocketAAT: PocketAAT + rpcQuery: string, + blockchain: string, + pocketAAT: PocketAAT ) { - try { - return await (getPocketServer() as unknown as Pocket).sendRelay( - rpcQuery, - blockchain, - pocketAAT - ); - } catch (e) { - console.log(e); - throw e; - } + try { + return await (getPocketServer() as unknown as Pocket).sendRelay( + rpcQuery, + blockchain, + pocketAAT + ); + } catch (e) { + console.log(e); + throw e; + } } function getRpcProvider(chain: string): RpcProviderMethod { - if (chain in gatewayMethods) { - return gatewayMethods[chain](chain); - } + if (chain in gatewayMethods) { + return gatewayMethods[chain](chain); + } - return gatewayMethods.default(chain); + return gatewayMethods.default(chain); } diff --git a/src/rpc/dns.ts b/src/rpc/dns.ts index 3c6b331..c4d9ebf 100644 --- a/src/rpc/dns.ts +++ b/src/rpc/dns.ts @@ -1,16 +1,16 @@ -import { isIp } from "../util.js"; -import { RpcMethodList } from "./index.js"; -import { createRequire } from "module"; +import {isIp} from "../util.js"; +import {RpcMethodList} from "./index.js"; +import {createRequire} from "module"; const require = createRequire(import.meta.url); const bns = require("bns"); -const { StubResolver, RecursiveResolver } = bns; +const {StubResolver, RecursiveResolver} = bns; const resolverOpt = { - tcp: true, - inet6: false, - edns: true, - dnssec: true, + tcp: true, + inet6: false, + edns: true, + dnssec: true, }; const globalResolver = new RecursiveResolver(resolverOpt); @@ -18,90 +18,90 @@ globalResolver.hints.setDefault(); globalResolver.open(); async function resolveNameServer(ns: string): Promise { - if (isIp(ns)) { - return ns; - } - let result = await getDnsRecords(ns, "A"); - - if (result.length) { - return result[0]; - } - - return false; -} - -async function getDnsRecords( - domain: string, - type: string, - authority: boolean = false, - resolver = globalResolver -): Promise { - let result; - - try { - result = await resolver.lookup(domain, type); - } catch (e) { - return []; - } - - let prop = authority ? "authority" : "answer"; - - if (!result || !result[prop].length) { - return []; - } - - return result[prop].map( - (item: object) => - // @ts-ignore - item.data.address ?? item.data.target ?? item.data.ns ?? null - ); -} - -export default { - dnslookup: async function (args: any) { - let dnsResults: string[] = []; - let domain = args.domain; - let ns = args.nameserver; - let dnsResolver = ns ? new StubResolver(resolverOpt) : globalResolver; - await dnsResolver.open(); - - if (ns) { - let nextNs = ns; - let prevNs = null; - - while (nextNs) { - nextNs = await resolveNameServer(nextNs); - if (!nextNs) { - nextNs = prevNs; - } - - dnsResolver.setServers([nextNs]); - - if (nextNs === prevNs) { - break; - } - let result = await getDnsRecords(domain, "NS", true, dnsResolver); - prevNs = nextNs; - nextNs = result.length ? result[0] : false; - } + if (isIp(ns)) { + return ns; } + let result = await getDnsRecords(ns, "A"); - for (const queryType of ["CNAME", "A"]) { - let result = await getDnsRecords(domain, queryType, false, dnsResolver); - - if (result) { - dnsResults = dnsResults.concat(result); - } - } - - await dnsResolver.close(); - - dnsResults = dnsResults.filter(Boolean); - - if (dnsResults.length) { - return dnsResults[0]; + if (result.length) { + return result[0]; } return false; - }, +} + +async function getDnsRecords( + domain: string, + type: string, + authority: boolean = false, + resolver = globalResolver +): Promise { + let result; + + try { + result = await resolver.lookup(domain, type); + } catch (e) { + return []; + } + + let prop = authority ? "authority" : "answer"; + + if (!result || !result[prop].length) { + return []; + } + + return result[prop].map( + (item: object) => + // @ts-ignore + item.data.address ?? item.data.target ?? item.data.ns ?? null + ); +} + +export default { + dnslookup: async function (args: any) { + let dnsResults: string[] = []; + let domain = args.domain; + let ns = args.nameserver; + let dnsResolver = ns ? new StubResolver(resolverOpt) : globalResolver; + await dnsResolver.open(); + + if (ns) { + let nextNs = ns; + let prevNs = null; + + while (nextNs) { + nextNs = await resolveNameServer(nextNs); + if (!nextNs) { + nextNs = prevNs; + } + + dnsResolver.setServers([nextNs]); + + if (nextNs === prevNs) { + break; + } + let result = await getDnsRecords(domain, "NS", true, dnsResolver); + prevNs = nextNs; + nextNs = result.length ? result[0] : false; + } + } + + for (const queryType of ["CNAME", "A"]) { + let result = await getDnsRecords(domain, queryType, false, dnsResolver); + + if (result) { + dnsResults = dnsResults.concat(result); + } + } + + await dnsResolver.close(); + + dnsResults = dnsResults.filter(Boolean); + + if (dnsResults.length) { + return dnsResults[0]; + } + + return false; + }, } as RpcMethodList; diff --git a/src/rpc/evm.ts b/src/rpc/evm.ts index db2203a..064520f 100644 --- a/src/rpc/evm.ts +++ b/src/rpc/evm.ts @@ -1,14 +1,14 @@ -import { proxyRpcMethod } from "./common.js"; -import { RpcMethodList } from "./index.js"; +import {proxyRpcMethod} from "./common.js"; +import {RpcMethodList} from "./index.js"; const rpcMethods: RpcMethodList = {}; function proxyEvmRpcMethod(method: string): Function { - return proxyRpcMethod(method); + return proxyRpcMethod(method); } ["eth_call", "eth_chainId", "net_version"].forEach((method) => { - rpcMethods[method] = proxyEvmRpcMethod(method); + rpcMethods[method] = proxyEvmRpcMethod(method); }); export default rpcMethods; diff --git a/src/rpc/handshake.ts b/src/rpc/handshake.ts index 63c25f2..2886e21 100644 --- a/src/rpc/handshake.ts +++ b/src/rpc/handshake.ts @@ -1,55 +1,88 @@ -import { RpcMethodList } from "./index.js"; +import {RpcMethodList} from "./index.js"; // @ts-ignore import rand from "random-key"; // @ts-ignore import SPVNode from "hsd/lib/node/spvnode.js"; import config from "../config.js"; -import { createRequire } from "module"; +import {createRequire} from "module"; const require = createRequire(import.meta.url); -const { NodeClient } = require("hs-client"); +const {NodeClient} = require("hs-client"); let hsdServer: SPVNode; let clientArgs = { - network: "main", - host: "127.0.0.1", - port: 12037, - apiKey: rand.generate(), + network: "main", + host: "127.0.0.1", + port: 12037, + apiKey: rand.generate(), }; -if (!config.bool("hsd-use-extenal-node")) { - hsdServer = new SPVNode({ - config: false, - argv: false, - env: true, - noDns: true, - httpHost: "127.0.0.1", - apiKey: clientArgs.apiKey, - logFile: false, - logConsole: false, - logLevel: "info", - workers: true, - network: "main", - }); +if (!config.bool("hsd-use-external-node")) { + hsdServer = new SPVNode({ + config: false, + argv: false, + env: true, + noDns: true, + memory: false, + httpHost: "127.0.0.1", + apiKey: clientArgs.apiKey, + logFile: false, + logConsole: true, + logLevel: "info", + workers: true, + network: "main", + }); + + console.log(`HSD API KEY: ${clientArgs.apiKey}`); + + hsdServer.on("abort", async (err: any) => { + const timeout = setTimeout(() => { + console.error("Shutdown is taking a long time. Exiting."); + process.exit(3); + }, 5000); + + timeout.unref(); + + try { + console.error("Shutting down..."); + await hsdServer.close(); + clearTimeout(timeout); + console.error((err as Error).stack); + process.exit(2); + } catch (e: any) { + console.error(`Error occurred during shutdown: ${(e as Error).message}`); + process.exit(3); + } + }); + + try { + await hsdServer.ensure(); + await hsdServer.open(); + await hsdServer.connect(); + + hsdServer.startSync(); + } catch (e: any) { + console.error((e as Error).stack); + } } else { - clientArgs = { - network: config.str("hsd-network-type"), - host: config.str("hsd-host"), - port: config.uint("hsd-port"), - apiKey: config.str("hsd-api-key"), - }; + clientArgs = { + network: config.str("hsd-network-type"), + host: config.str("hsd-host"), + port: config.uint("hsd-port"), + apiKey: config.str("hsd-api-key"), + }; } const hnsClient = new NodeClient(clientArgs); export default { - getnameresource: async function (args: any, context: object) { - // @ts-ignore - if ("hns" !== context.chain) { - throw new Error("Invalid Chain"); - } + getnameresource: async function (args: any, context: object) { + // @ts-ignore + if ("hns" !== context.chain) { + throw new Error("Invalid Chain"); + } - return await hnsClient.execute("getnameresource", args); - }, + return await hnsClient.execute("getnameresource", args); + }, } as RpcMethodList; diff --git a/src/rpc/index.ts b/src/rpc/index.ts index 3aa96ba..121bf43 100644 --- a/src/rpc/index.ts +++ b/src/rpc/index.ts @@ -2,17 +2,17 @@ export type RpcMethodList = { [name: string]: Function }; export * from "./common.js"; -import { default as DnsMethods } from "./dns.js"; -import { default as EvmMethods } from "./evm.js"; -import { default as HnsMethods } from "./handshake.js"; -import { default as SolMethods } from "./solana.js"; -import { default as AlgoMethods } from "./algorand.js"; +import {default as DnsMethods} from "./dns.js"; +import {default as EvmMethods} from "./evm.js"; +import {default as HnsMethods} from "./handshake.js"; +import {default as SolMethods} from "./solana.js"; +import {default as AlgoMethods} from "./algorand.js"; export const rpcMethods: RpcMethodList = Object.assign( - {}, - DnsMethods, - EvmMethods, - HnsMethods, - SolMethods, - AlgoMethods + {}, + DnsMethods, + EvmMethods, + HnsMethods, + SolMethods, + AlgoMethods ); diff --git a/src/rpc/solana.ts b/src/rpc/solana.ts index 5cd5996..5878c8b 100644 --- a/src/rpc/solana.ts +++ b/src/rpc/solana.ts @@ -1,9 +1,9 @@ -import { proxyRpcMethod } from "./common.js"; -import { RpcMethodList } from "./index.js"; -import * as chainNetworks from "../networks.json" assert { type: "json" }; +import {proxyRpcMethod} from "./common.js"; +import {RpcMethodList} from "./index.js"; +import * as chainNetworks from "../networks.json" assert {type: "json"}; export default { - getAccountInfo: proxyRpcMethod("getAccountInfo", [ - chainNetworks["sol-mainnet"], - ]), + getAccountInfo: proxyRpcMethod("getAccountInfo", [ + chainNetworks["sol-mainnet"], + ]), } as RpcMethodList; diff --git a/src/util.ts b/src/util.ts index a8b832f..a35a1b6 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,39 +1,66 @@ -import * as chainNetworks from "./networks.json" assert { type: "json" }; +import * as chainNetworks from "./networks.json" assert {type: "json"}; +import {Buffer} from "buffer"; +import {blake2b} from "libskynet"; type networks = { [net: string]: string }; export function errorExit(msg: string): void { - console.error(msg); - process.exit(1); + console.error(msg); + process.exit(1); } export function maybeMapChainId(chain: string): string | boolean { - if (chain in chainNetworks) { - return (chainNetworks as networks)[chain]; - } + if (chain in chainNetworks) { + return (chainNetworks as networks)[chain]; + } - if ( - [parseInt(chain, 16).toString(), parseInt(chain, 10).toString()].includes( - chain.toLowerCase() - ) - ) { - return chain; - } + if ( + [parseInt(chain, 16).toString(), parseInt(chain, 10).toString()].includes( + chain.toLowerCase() + ) + ) { + return chain; + } - return false; + return false; } export function reverseMapChainId(chainId: string): string | boolean { - let vals = Object.values(chainNetworks); - if (!vals.includes(chainId)) { - return false; - } + let vals = Object.values(chainNetworks); + if (!vals.includes(chainId)) { + return false; + } - return Object.keys(chainNetworks)[vals.indexOf(chainId)]; + return Object.keys(chainNetworks)[vals.indexOf(chainId)]; } export function isIp(ip: string) { - return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( - ip - ); + return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( + ip + ); +} + +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; }