diff --git a/package.json b/package.json index e1322da..17dfcdf 100644 --- a/package.json +++ b/package.json @@ -4,21 +4,34 @@ "version": "0.1.0", "description": "", "main": "build/index.js", + "author": { + "name": "Derrick Hammer", + "email": "contact@lumeweb.com" + }, "dependencies": { "@hyperswarm/dht": "^6.0.1", "@hyperswarm/dht-relay": "^0.3.0", + "@root/greenlock": "^4.0.5", "@types/node": "^18.0.0", + "@types/node-cron": "^3.0.2", "@types/ws": "^8.5.3", + "@types/xml2js": "^0.4.11", "async-mutex": "^0.3.2", + "express": "^4.18.1", + "greenlock-express": "^4.0.3", "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" + "node-cron": "^3.0.1", + "node-fetch": "^3.2.6", + "random-access-memory": "^4.1.0", + "xml2js": "^0.4.23" }, "devDependencies": { + "@types/express": "^4.17.13", "hyper-typings": "^1.0.0", "prettier": "^2.7.1" } diff --git a/src/constant_vars.ts b/src/constant_vars.ts new file mode 100644 index 0000000..31dc777 --- /dev/null +++ b/src/constant_vars.ts @@ -0,0 +1,5 @@ +export const RELAY_PORT = process.env.RELAY_PORT ?? 8080; +export const RELAY_DOMAIN = process.env.RELAY_DOMAIN; +export const AFRAID_USERNAME = process.env.AFRAID_USERNAME; +export const AFRAID_PASSWORD = process.env.AFRAID_PASSWORD; +export const RELAY_SEED = process.env.RELAY_SEED; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..6b197be --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,17 @@ +import * as CONFIG from "./constant_vars.js"; + +let error = false; + +for (const constant in CONFIG) { + // @ts-ignore + if (!CONFIG[constant]) { + console.error(`Missing constant ${constant}`); + error = true; + } +} + +if (error) { + process.exit(1); +} + +export * from "./constant_vars.js"; diff --git a/src/dht.ts b/src/dht.ts index 3e2f49e..af00cc9 100644 --- a/src/dht.ts +++ b/src/dht.ts @@ -8,6 +8,7 @@ import { seedPhraseToSeed, validSeedPhrase, } from "libskynet"; +import { RELAY_SEED } from "./constant_vars"; let server: { listen: (arg0: ed25519Keypair) => void; @@ -15,12 +16,6 @@ let server: { }; 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."); diff --git a/src/dns.ts b/src/dns.ts new file mode 100644 index 0000000..d711f01 --- /dev/null +++ b/src/dns.ts @@ -0,0 +1,124 @@ +import { + AFRAID_USERNAME, + AFRAID_PASSWORD, + RELAY_PORT, + RELAY_DOMAIN, +} from "./constants.js"; +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"; + +const { createHash } = await import("crypto"); + +let activeIp: string; + +const REGISTRY_DHT_KEY = "lumeweb-dht-relay"; + +async function ipUpdate() { + let currentIp = await getCurrentIp(); + + if (activeIp && currentIp === activeIp) { + return; + } + + let domain = await getDomainInfo(); + + await fetch(domain.url[0].toString()); + + activeIp = domain.address[0]; +} + +export async function start() { + const dht = await getDHT(); + + await ipUpdate(); + + await overwriteRegistryEntry( + dht.defaultKeyPair, + hashDataKey(REGISTRY_DHT_KEY), + pack(`${RELAY_DOMAIN}:${RELAY_PORT}`) + ); + + cron.schedule("0 * * * *", ipUpdate); +} + +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; +} + +async function getDomainInfo() { + const parser = new Parser(); + + const url = new URL("https://freedns.afraid.org/api/"); + + const params = url.searchParams; + + params.append("action", "getdyndns"); + params.append("v", "2"); + params.append("style", "xml"); + + const hash = createHash("sha1"); + hash.update(`${AFRAID_USERNAME}|${AFRAID_PASSWORD}`); + + params.append("sha", hash.digest().toString("hex")); + + 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] === RELAY_DOMAIN) { + domain = item; + break; + } + } + + if (!domain) { + errorExit(`Domain ${RELAY_DOMAIN} 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(); +} diff --git a/src/index.ts b/src/index.ts index a43a6fb..fa7bf76 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,10 @@ +import { start as startDns } from "./dns.js"; import { start as startRpc } from "./rpc.js"; import { start as startRelay } from "./relay.js"; -startRelay(); -startRpc(); +await startDns(); +await startRpc(); +await startRelay(); process.on("uncaughtException", function (err) { console.log("Caught exception: " + err); diff --git a/src/relay.ts b/src/relay.ts index a0c4448..ada1990 100644 --- a/src/relay.ts +++ b/src/relay.ts @@ -7,53 +7,60 @@ 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"; +import { RELAY_DOMAIN, RELAY_PORT } from "./constants.js"; +// @ts-ignore +import GLE from "greenlock-express"; +// @ts-ignore +import Greenlock from "@root/greenlock"; +import path from "path"; +import { fileURLToPath } from "url"; -const REGISTRY_DHT_KEY = "lumeweb-dht-relay"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const config = { + packageRoot: path.dirname(__dirname), + configDir: path.resolve(__dirname, "../", "./data/greenlock.d/"), + cluster: false, + agreeToTerms: true, + staging: true, +}; 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 greenlock = Greenlock.create(config); + await greenlock.add({ + subject: RELAY_DOMAIN, + altnames: [RELAY_DOMAIN], }); + // @ts-ignore + config.greenlock = greenlock; + GLE.init(config).ready(async (GLEServer: any) => { + let httpsServer = GLEServer.httpsServer(); + var httpServer = GLEServer.httpServer(); - const dht = await getDHT(); + await new Promise((resolve) => { + httpServer.listen(80, "0.0.0.0", function () { + console.info("HTTP Listening on ", httpServer.address()); + resolve(null); + }); + }); - await overwriteRegistryEntry( - dht.defaultKeyPair, - hashDataKey(REGISTRY_DHT_KEY), - stringToUint8ArrayUtf8(`${dht.localAddress()}:${RELAY_PORT}`) - ); + const dht = await getDHT(); - server.on("connection", (socket) => { - relay(dht, new Stream(false, socket)); + let wsServer = new WS.Server({ server: httpServer }); + + wsServer.on("connection", (socket: any) => { + relay(dht, new Stream(false, socket)); + }); + await new Promise((resolve) => { + httpsServer.listen(RELAY_PORT, "0.0.0.0", function () { + console.info("Relay started on ", httpsServer.address()); + resolve(null); + }); + }); + + await greenlock.get({ + servername: RELAY_DOMAIN, + }); }); } - -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/src/rpc.ts b/src/rpc.ts index c86dd8f..f1a8944 100644 --- a/src/rpc.ts +++ b/src/rpc.ts @@ -61,9 +61,7 @@ function getRequestId(request: RPCRequest) { return hash(stringify(clonedRequest)); } -function maybeProcessRequest(item: any) { - let request: RPCRequest = unpack(item) as RPCRequest; - +function maybeProcessRequest(request: RPCRequest) { if (!request.chain) { throw new Error("RPC chain missing"); } @@ -137,8 +135,15 @@ async function processRequest(request: RPCRequest): Promise { export async function start() { (await getDHT()).on("connection", (socket: any) => { socket.on("data", async (data: any) => { + let request: RPCRequest; try { - socket.write(pack(await maybeProcessRequest(data))); + request = unpack(data) as RPCRequest; + } catch (e) { + return; + } + + try { + socket.write(pack(await maybeProcessRequest(request))); } catch (error) { console.trace(error); socket.write(pack({ error })); diff --git a/src/util.ts b/src/util.ts index 740b339..8a993ab 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,4 @@ export function errorExit(msg: string): void { - console.log(msg); + console.error(msg); process.exit(1); }