relay/src/relay.ts

306 lines
7.4 KiB
TypeScript
Raw Normal View History

2022-06-27 17:53:00 +00:00
// @ts-ignore
import DHT from "@hyperswarm/dht";
// @ts-ignore
2022-07-19 22:31:15 +00:00
import { relay } from "@hyperswarm/dht-relay";
2022-06-27 17:53:00 +00:00
// @ts-ignore
import Stream from "@hyperswarm/dht-relay/ws";
2022-07-19 22:31:15 +00:00
import express, { Express } from "express";
2022-07-05 19:02:07 +00:00
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";
2022-07-19 22:31:15 +00:00
import { Buffer } from "buffer";
import { intervalToDuration } from "date-fns";
import cron from "node-cron";
2022-07-19 22:31:15 +00:00
import { get as getDHT } from "./dht.js";
import WS from "ws";
// @ts-ignore
import {
createIndependentFileSmall,
IndependentFileSmall,
openIndependentFileSmall,
overwriteIndependentFileSmall,
} from "./file.js";
2022-07-24 00:24:19 +00:00
import log from "loglevel";
import { AddressInfo } from "net";
import { sprintf } from "sprintf-js";
import { dynImport } from "./util.js";
let sslCtx: tls.SecureContext = tls.createSecureContext();
const sslParams: tls.SecureContextOptions = { cert: "", key: "" };
2022-06-27 17:53:00 +00:00
let acmeClient: acme.Client;
2022-06-27 21:52:20 +00:00
let app: Express;
let router = express.Router();
2022-06-27 17:53:00 +00:00
let seedPhraseToSeed: typeof import("libskynet").seedPhraseToSeed;
const FILE_CERT_NAME = "/lumeweb/relay/ssl.crt";
const FILE_KEY_NAME = "/lumeweb/relay/ssl.key";
const FILE_ACCOUNT_KEY_NAME = "/lumeweb/relay/account.key";
export async function start() {
seedPhraseToSeed = (await dynImport("libskynet")).seedPhraseToSeed;
2022-07-26 00:35:21 +00:00
const relayPort = config.uint("port");
2022-07-19 22:31:15 +00:00
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 * * * *", setupSSl.bind(null, false));
2022-07-19 22:31:15 +00:00
await new Promise((resolve) => {
httpServer.listen(80, "0.0.0.0", function () {
2022-07-24 00:24:19 +00:00
const address = httpServer.address() as AddressInfo;
log.info("HTTP Server started on ", `${address.address}:${address.port}`);
2022-07-19 22:31:15 +00:00
resolve(null);
});
2022-07-19 22:31:15 +00:00
});
const dht = await getDHT();
2022-06-27 17:53:00 +00:00
2022-07-19 22:31:15 +00:00
let wsServer = new WS.Server({ server: httpsServer });
2022-07-19 22:31:15 +00:00
wsServer.on("connection", (socket: any) => {
relay(dht, new Stream(false, socket));
});
2022-07-19 22:31:15 +00:00
await new Promise((resolve) => {
httpsServer.listen(relayPort, "0.0.0.0", function () {
2022-07-26 00:48:06 +00:00
const address = httpsServer.address() as AddressInfo;
2022-07-24 00:24:19 +00:00
log.info(
"DHT Relay Server started on ",
`${address.address}:${address.port}`
);
2022-07-19 22:31:15 +00:00
resolve(null);
});
2022-07-19 22:31:15 +00:00
});
const statusCodeServer = http.createServer(function (req, res) {
// @ts-ignore
res.writeHead(req.headers["x-status"] ?? 200, {
"Content-Type": "text/plain",
});
res.end();
});
await new Promise((resolve) => {
statusCodeServer.listen(25252, "0.0.0.0", function () {
const address = statusCodeServer.address() as AddressInfo;
log.info(
"Status Code Server started on ",
`${address.address}:${address.port}`
);
resolve(null);
});
});
await setupSSl(true);
}
async function setupSSl(bootup: boolean) {
let sslCert = await getSslCert();
let sslKey = await getSslKey();
let certInfo;
let exists = false;
let domainValid = false;
let dateValid = false;
2022-07-24 00:24:19 +00:00
let configDomain = config.str("domain");
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;
2022-07-19 22:31:15 +00:00
let duration = intervalToDuration({ start: new Date(), end: expires });
let daysLeft = (duration.months as number) * 30 + (duration.days as number);
2022-07-19 22:31:15 +00:00
if (daysLeft > 30) {
dateValid = true;
}
2022-07-24 00:24:19 +00:00
if (certInfo?.domains.commonName === configDomain) {
domainValid = true;
}
if (
Boolean(isSSlStaging()) !==
Boolean(certInfo?.issuer.commonName.toLowerCase().includes("staging"))
) {
domainValid = false;
}
2022-07-19 22:31:15 +00:00
}
if (dateValid && domainValid) {
if (bootup) {
sslCtx = tls.createSecureContext(sslParams);
log.info(`Loaded SSL Certificate for ${configDomain}`);
}
return;
}
await createOrRenewSSl(
sslCert as IndependentFileSmall,
sslKey as IndependentFileSmall
);
}
async function createOrRenewSSl(
oldCert?: IndependentFileSmall,
oldKey?: IndependentFileSmall
) {
2022-07-24 00:24:19 +00:00
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,
});
2022-07-19 22:31:15 +00:00
const [certificateKey, certificateRequest] = await acme.forge.createCsr({
2022-07-22 23:55:09 +00:00
commonName: config.str("domain"),
2022-07-19 22:31:15 +00:00
});
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);
}
2022-07-19 22:31:15 +00:00
sslParams.key = certificateKey;
sslCtx = tls.createSecureContext(sslParams);
await saveSsl(oldCert, oldKey);
}
async function saveSsl(
oldCert?: IndependentFileSmall,
oldKey?: IndependentFileSmall
): Promise<void> {
const seed = getSeed();
2022-07-24 00:24:19 +00:00
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 {
2022-07-22 23:55:09 +00:00
let [seed, err] = seedPhraseToSeed(config.str("seed"));
if (err) {
console.error(err);
process.exit(1);
}
return seed;
2022-06-27 17:53:00 +00:00
}
function isSSlStaging() {
return config.str("ssl-mode") === "staging";
}