*Move dns and ssl control to plugin apis

*Add files plugin api
*Add logger to api
*Add seed getter to api
*Add app router to api
This commit is contained in:
Derrick Hammer 2022-09-09 05:17:25 -04:00
parent 8e881a7dc1
commit 08fdc88874
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
8 changed files with 1062 additions and 1103 deletions

View File

@ -33,6 +33,7 @@ config.inject({
logLevel: "info", logLevel: "info",
pluginFolder: path.join(configDir, "plugins"), pluginFolder: path.join(configDir, "plugins"),
plugins: ["core"], plugins: ["core"],
ssl: true,
}); });
config.load({ config.load({

View File

@ -1,14 +1,11 @@
import cron from "node-cron"; import cron from "node-cron";
import { get as getDHT } from "./dht.js"; import { get as getDHT } from "./dht.js";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import { Parser } from "xml2js";
import { URL } from "url";
import { pack } from "msgpackr"; import { pack } from "msgpackr";
import config from "./config.js"; import config from "./config.js";
import { errorExit } from "./error.js";
import log from "loglevel"; import log from "loglevel";
import { createHash } from "crypto";
import { dynImport } from "./util.js"; import { dynImport } from "./util.js";
import type { DnsProvider } from "@lumeweb/relay-types";
let activeIp: string; let activeIp: string;
let fetch: typeof import("node-fetch").default; let fetch: typeof import("node-fetch").default;
@ -17,6 +14,12 @@ let hashDataKey: typeof import("@lumeweb/kernel-utils").hashDataKey;
const REGISTRY_NODE_KEY = "lumeweb-dht-node"; const REGISTRY_NODE_KEY = "lumeweb-dht-node";
let dnsProvider: DnsProvider = async (ip) => {};
export function setDnsProvider(provider: DnsProvider) {
dnsProvider = provider;
}
async function ipUpdate() { async function ipUpdate() {
let currentIp = await getCurrentIp(); let currentIp = await getCurrentIp();
@ -24,11 +27,7 @@ async function ipUpdate() {
return; return;
} }
let domain = await getDomainInfo(); await dnsProvider(currentIp);
await fetch(domain.url[0].toString());
activeIp = domain.address[0];
log.info(`Updated DynDNS hostname ${config.str("domain")} to ${activeIp}`); log.info(`Updated DynDNS hostname ${config.str("domain")} to ${activeIp}`);
} }
@ -57,49 +56,6 @@ export async function start() {
cron.schedule("0 * * * *", ipUpdate); cron.schedule("0 * * * *", ipUpdate);
} }
async function getDomainInfo() {
const relayDomain = config.str("domain");
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(
`${config.str("afraid-username")}|${config.str("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] === relayDomain) {
domain = item;
break;
}
}
if (!domain) {
errorExit(`Domain ${relayDomain} not found in afraid.org account`);
}
return domain;
}
async function getCurrentIp(): Promise<string> { async function getCurrentIp(): Promise<string> {
return await (await fetch("http://ip1.dynupdate.no-ip.com/")).text(); return await (await fetch("http://ip1.dynupdate.no-ip.com/")).text();
} }

View File

@ -1,45 +1,15 @@
import type { Ed25519Keypair, Err, progressiveFetchResult } from "libskynet"; import type { Err, progressiveFetchResult } from "libskynet";
// @ts-ignore // @ts-ignore
import { SkynetClient } from "@skynetlabs/skynet-nodejs"; import { SkynetClient } from "@skynetlabs/skynet-nodejs";
import { dynImport } from "./util.js"; import { dynImport } from "./util.js";
import type {
IndependentFileSmall,
IndependentFileSmallMetadata,
} from "@lumeweb/relay-types";
const ERR_EXISTS = "exists";
const ERR_NOT_EXISTS = "DNE"; const ERR_NOT_EXISTS = "DNE";
const STD_FILENAME = "file"; const STD_FILENAME = "file";
type OverwriteDataFn = (newData: Uint8Array) => Promise<Err>;
type ReadDataFn = () => Promise<[Uint8Array, Err]>;
export interface IndependentFileSmallMetadata {
largestHistoricSize: bigint;
}
export interface IndependentFileSmall {
dataKey: Uint8Array;
fileData: Uint8Array;
inode: string;
keypair: Ed25519Keypair;
metadata: IndependentFileSmallMetadata;
revision: bigint;
seed: Uint8Array;
skylink: string;
viewKey: string;
overwriteData: OverwriteDataFn;
readData: ReadDataFn;
}
interface IndependentFileSmallViewer {
fileData: Uint8Array;
skylink: string;
viewKey: string;
readData: ReadDataFn;
}
let addContextToErr: typeof import("libskynet").addContextToErr, let addContextToErr: typeof import("libskynet").addContextToErr,
blake2b: typeof import("libskynet").blake2b, blake2b: typeof import("libskynet").blake2b,
bufToHex: typeof import("libskynet").bufToHex, bufToHex: typeof import("libskynet").bufToHex,

View File

@ -4,6 +4,23 @@ import type { PluginAPI, RPCMethod, Plugin } from "@lumeweb/relay-types";
import slugify from "slugify"; import slugify from "slugify";
import * as fs from "fs"; import * as fs from "fs";
import path from "path"; import path from "path";
import {
getSavedSsl,
getSsl,
getSslContext,
saveSSl,
setSsl,
setSslContext,
} from "./ssl.js";
import log from "loglevel";
import { getSeed } from "./util.js";
import { getRouter, resetRouter, setRouter } from "./relay";
import {
createIndependentFileSmall,
openIndependentFileSmall,
overwriteIndependentFileSmall,
} from "./file";
import { setDnsProvider } from "./dns";
let pluginApi: PluginApiManager; let pluginApi: PluginApiManager;
@ -64,6 +81,30 @@ export class PluginApiManager {
}, },
loadPlugin: getPluginAPI().loadPlugin.bind(getPluginAPI()), loadPlugin: getPluginAPI().loadPlugin.bind(getPluginAPI()),
getMethods: getRpcServer().getMethods.bind(getRpcServer()), getMethods: getRpcServer().getMethods.bind(getRpcServer()),
ssl: {
setContext: setSslContext,
getContext: getSslContext,
getSaved: getSavedSsl,
set: setSsl,
get: getSsl,
save: saveSSl,
},
files: {
createIndependentFileSmall,
openIndependentFileSmall,
overwriteIndependentFileSmall,
},
dns: {
setProvider: setDnsProvider,
},
logger: log,
getSeed,
appRouter: {
get: getRouter,
set: setRouter,
reset: resetRouter,
},
}; };
} }
} }

View File

@ -8,44 +8,31 @@ import express, { Express } from "express";
import config from "./config.js"; import config from "./config.js";
import * as http from "http"; import * as http from "http";
import * as https from "https"; 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 { get as getDHT } from "./dht.js";
import WS from "ws"; import WS from "ws";
// @ts-ignore // @ts-ignore
import {
createIndependentFileSmall,
IndependentFileSmall,
openIndependentFileSmall,
overwriteIndependentFileSmall,
} from "./file.js";
import log from "loglevel"; import log from "loglevel";
import { AddressInfo } from "net"; import { AddressInfo } from "net";
import { sprintf } from "sprintf-js";
import { dynImport } from "./util.js";
// @ts-ignore // @ts-ignore
import promiseRetry from "promise-retry"; import promiseRetry from "promise-retry";
import { getSslContext } from "./ssl.js";
let sslCtx: tls.SecureContext = tls.createSecureContext();
const sslParams: tls.SecureContextOptions = { cert: "", key: "" };
let acmeClient: acme.Client;
let app: Express; let app: Express;
let router = express.Router(); let router = express.Router();
let seedPhraseToSeed: typeof import("libskynet").seedPhraseToSeed; export function getRouter(): express.Router {
return router;
}
const FILE_CERT_NAME = "/lumeweb/relay/ssl.crt"; export function setRouter(newRouter: express.Router): void {
const FILE_KEY_NAME = "/lumeweb/relay/ssl.key"; router = newRouter;
const FILE_ACCOUNT_KEY_NAME = "/lumeweb/relay/account.key"; }
export function resetRouter(): void {
setRouter(express.Router());
}
export async function start() { export async function start() {
seedPhraseToSeed = (await dynImport("libskynet")).seedPhraseToSeed;
const relayPort = config.uint("port"); const relayPort = config.uint("port");
app = express(); app = express();
app.use(function (req, res, next) { app.use(function (req, res, next) {
@ -82,23 +69,27 @@ export async function start() {
}); });
}); });
await setupSSl(true); let relayServer: https.Server | http.Server;
let httpsServer = https.createServer({ if (config.bool("ssl")) {
relayServer = https.createServer({
SNICallback(servername, cb) { SNICallback(servername, cb) {
cb(null, sslCtx); cb(null, getSslContext());
}, },
}); });
} else {
relayServer = http.createServer();
}
let wsServer = new WS.Server({ server: httpsServer }); let wsServer = new WS.Server({ server: relayServer });
wsServer.on("connection", (socket: any) => { wsServer.on("connection", (socket: any) => {
relay(dht, new Stream(false, socket)); relay(dht, new Stream(false, socket));
}); });
await new Promise((resolve) => { await new Promise((resolve) => {
httpsServer.listen(relayPort, "0.0.0.0", function () { relayServer.listen(relayPort, "0.0.0.0", function () {
const address = httpsServer.address() as AddressInfo; const address = relayServer.address() as AddressInfo;
log.info( log.info(
"DHT Relay Server started on ", "DHT Relay Server started on ",
`${address.address}:${address.port}` `${address.address}:${address.port}`
@ -106,220 +97,4 @@ export async function start() {
resolve(null); resolve(null);
}); });
}); });
cron.schedule("0 * * * *", setupSSl.bind(null, false));
}
async function setupSSl(bootup: boolean) {
let sslCert: IndependentFileSmall | boolean = false;
let sslKey: IndependentFileSmall | boolean = false;
let certInfo;
let exists = false;
let domainValid = false;
let dateValid = false;
let configDomain = config.str("domain");
let retryOptions = bootup ? {} : { retry: 0 };
try {
await promiseRetry(async (retry: any) => {
sslCert = await getSslCert();
if (!sslCert) {
retry();
}
}, retryOptions);
await promiseRetry(async (retry: any) => {
sslKey = await getSslKey();
if (!sslKey) {
retry();
}
}, retryOptions);
} catch {}
if (sslCert && sslKey) {
sslParams.cert = Buffer.from((sslCert as IndependentFileSmall).fileData);
sslParams.key = Buffer.from((sslKey as IndependentFileSmall).fileData);
certInfo = await getCertInfo();
exists = true;
}
if (exists) {
const expires = certInfo?.notAfter as Date;
let duration = intervalToDuration({ start: new Date(), end: expires });
let daysLeft = (duration.months as number) * 30 + (duration.days as number);
if (daysLeft > 30) {
dateValid = true;
}
if (certInfo?.domains.commonName === configDomain) {
domainValid = true;
}
if (
Boolean(isSSlStaging()) !==
Boolean(certInfo?.issuer.commonName.toLowerCase().includes("staging"))
) {
domainValid = false;
}
}
if (dateValid && domainValid) {
if (bootup) {
sslCtx = tls.createSecureContext(sslParams);
log.info(`Loaded SSL Certificate for ${configDomain}`);
}
return;
}
await createOrRenewSSl(
sslCert as unknown as IndependentFileSmall,
sslKey as unknown as IndependentFileSmall
);
}
async function createOrRenewSSl(
oldCert?: IndependentFileSmall,
oldKey?: IndependentFileSmall
) {
const existing = oldCert && oldKey;
log.info(
sprintf(
"%s SSL Certificate for %s",
existing ? "Renewing" : "Creating",
config.str("domain")
)
);
let accountKey: boolean | IndependentFileSmall | Buffer = await getSslFile(
FILE_ACCOUNT_KEY_NAME
);
if (accountKey) {
accountKey = Buffer.from((accountKey as IndependentFileSmall).fileData);
}
if (!accountKey) {
accountKey = await acme.forge.createPrivateKey();
await createIndependentFileSmall(
getSeed(),
FILE_ACCOUNT_KEY_NAME,
accountKey
);
}
acmeClient = new acme.Client({
accountKey: accountKey as Buffer,
directoryUrl: isSSlStaging()
? acme.directory.letsencrypt.staging
: acme.directory.letsencrypt.production,
});
const [certificateKey, certificateRequest] = await acme.forge.createCsr({
commonName: config.str("domain"),
});
try {
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.cert = Buffer.from(sslParams.cert);
} catch (e: any) {
console.error((e as Error).message);
process.exit(1);
}
sslParams.key = certificateKey;
sslCtx = tls.createSecureContext(sslParams);
await saveSsl(oldCert, oldKey);
}
async function saveSsl(
oldCert?: IndependentFileSmall,
oldKey?: IndependentFileSmall
): Promise<void> {
const seed = getSeed();
log.info(`Saving SSL Certificate for ${config.str("domain")}`);
if (oldCert) {
await overwriteIndependentFileSmall(
oldCert,
Buffer.from(sslParams.cert as any)
);
} else {
await createIndependentFileSmall(
seed,
FILE_CERT_NAME,
Buffer.from(sslParams.cert as any)
);
}
if (oldKey) {
await overwriteIndependentFileSmall(
oldKey,
Buffer.from(sslParams.key as any)
);
} else {
await createIndependentFileSmall(
seed,
FILE_KEY_NAME,
Buffer.from(sslParams.key as any)
);
}
}
async function getCertInfo() {
return acme.forge.readCertificateInfo(sslParams.cert as Buffer);
}
async function getSslCert(): Promise<IndependentFileSmall | boolean> {
return getSslFile(FILE_CERT_NAME);
}
async function getSslKey(): Promise<IndependentFileSmall | boolean> {
return getSslFile(FILE_KEY_NAME);
}
async function getSslFile(
name: string
): Promise<IndependentFileSmall | boolean> {
let seed = getSeed();
let [file, err] = await openIndependentFileSmall(seed, name);
if (err) {
return false;
}
return file;
}
function getSeed(): Uint8Array {
let [seed, err] = seedPhraseToSeed(config.str("seed"));
if (err) {
console.error(err);
process.exit(1);
}
return seed;
}
function isSSlStaging() {
return config.str("ssl-mode") === "staging";
} }

136
src/ssl.ts Normal file
View File

@ -0,0 +1,136 @@
import tls from "tls";
import {
createIndependentFileSmall,
openIndependentFileSmall,
overwriteIndependentFileSmall,
} from "./file.js";
// @ts-ignore
import promiseRetry from "promise-retry";
import config from "./config.js";
import log from "loglevel";
import { getSeed } from "./util.js";
import type {
IndependentFileSmall,
SavedSslData,
SslData,
} from "@lumeweb/relay-types";
let sslCtx: tls.SecureContext = tls.createSecureContext();
let sslObject: SslData = {};
const FILE_CERT_NAME = "/lumeweb/relay/ssl.crt";
const FILE_KEY_NAME = "/lumeweb/relay/ssl.key";
export function setSslContext(context: tls.SecureContext) {
sslCtx = context;
}
export function getSslContext(): tls.SecureContext {
return sslCtx;
}
export function setSsl(
cert: IndependentFileSmall | Uint8Array,
key: IndependentFileSmall | Uint8Array
): void {
cert = (cert as IndependentFileSmall)?.fileData || cert;
key = (key as IndependentFileSmall)?.fileData || cert;
sslObject.cert = cert as Uint8Array;
sslObject.key = key as Uint8Array;
setSslContext(
tls.createSecureContext({
cert: Buffer.from(cert),
key: Buffer.from(key),
})
);
}
export function getSsl(): SslData {
return sslObject;
}
export async function saveSSl(): Promise<void> {
const seed = getSeed();
log.info(`Saving SSL Certificate for ${config.str("domain")}`);
let oldCert = await getSslCert();
let cert: any = getSsl()?.cert;
cert = cert?.fileData;
if (oldCert) {
await overwriteIndependentFileSmall(
oldCert as IndependentFileSmall,
Buffer.from(cert)
);
} else {
await createIndependentFileSmall(seed, FILE_CERT_NAME, Buffer.from(cert));
}
let oldKey = await getSslKey();
let key: any = getSsl()?.cert;
key = key?.fileData;
if (oldKey) {
await overwriteIndependentFileSmall(
oldKey as IndependentFileSmall,
Buffer.from(key)
);
} else {
await createIndependentFileSmall(seed, FILE_KEY_NAME, Buffer.from(key));
}
}
export async function getSavedSsl(
retry = true
): Promise<boolean | SavedSslData> {
let retryOptions = retry ? {} : { retry: 0 };
let sslCert: IndependentFileSmall | boolean = false;
let sslKey: IndependentFileSmall | boolean = false;
try {
await promiseRetry(async (retry: any) => {
sslCert = await getSslCert();
if (!sslCert) {
retry();
}
}, retryOptions);
await promiseRetry(async (retry: any) => {
sslKey = await getSslKey();
if (!sslKey) {
retry();
}
}, retryOptions);
} catch {}
if (!sslCert || !sslKey) {
return false;
}
return {
cert: sslCert as IndependentFileSmall,
key: sslKey as IndependentFileSmall,
};
}
async function getSslCert(): Promise<IndependentFileSmall | boolean> {
return getSslFile(FILE_CERT_NAME);
}
async function getSslKey(): Promise<IndependentFileSmall | boolean> {
return getSslFile(FILE_KEY_NAME);
}
async function getSslFile(
name: string
): Promise<IndependentFileSmall | boolean> {
let seed = getSeed();
let [file, err] = await openIndependentFileSmall(seed, name);
if (err) {
return false;
}
return file;
}

View File

@ -1,3 +1,17 @@
import config from "./config";
import { seedPhraseToSeed } from "libskynet";
export function dynImport(module: string) { export function dynImport(module: string) {
return Function(`return import("${module}")`)() as Promise<any>; return Function(`return import("${module}")`)() as Promise<any>;
} }
export function getSeed(): Uint8Array {
let [seed, err] = seedPhraseToSeed(config.str("seed"));
if (err) {
console.error(err);
process.exit(1);
}
return seed;
}

1600
yarn.lock

File diff suppressed because it is too large Load Diff