hfs
Version:
HTTP File Server
346 lines (345 loc) • 17.4 kB
JavaScript
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.httpsPortCfg = exports.privateKey = exports.cert = exports.portCfg = exports.baseUrl = void 0;
exports.getBaseUrlOrDefault = getBaseUrlOrDefault;
exports.getHttpsWorkingPort = getHttpsWorkingPort;
exports.openAdmin = openAdmin;
exports.getCertObject = getCertObject;
exports.startServer = startServer;
exports.stopServer = stopServer;
exports.getServerStatus = getServerStatus;
exports.getIps = getIps;
exports.getUrls = getUrls;
const http = __importStar(require("http"));
const config_1 = require("./config");
const index_1 = require("./index");
const https = __importStar(require("https"));
const watchLoad_1 = require("./watchLoad");
const os_1 = require("os");
const connections_1 = require("./connections");
const open_1 = __importDefault(require("open"));
const misc_1 = require("./misc");
const const_1 = require("./const");
const find_process_1 = __importDefault(require("find-process"));
const adminApis_1 = require("./adminApis");
const lodash_1 = __importDefault(require("lodash"));
const crypto_1 = require("crypto");
const events_1 = __importDefault(require("./events"));
const net_1 = require("net");
const nat_1 = require("./nat");
const persistence_1 = require("./persistence");
const argv_1 = require("./argv");
let httpSrv;
let httpsSrv;
const openBrowserAtStart = (0, config_1.defineConfig)('open_browser_at_start', true);
exports.baseUrl = (0, config_1.defineConfig)(misc_1.CFG.base_url, '', x => { var _a; return (_a = /(?<=\/\/)[^\/]+/.exec(x)) === null || _a === void 0 ? void 0 : _a[0]; }); // compiled is host only
async function getBaseUrlOrDefault() {
return exports.baseUrl.get() || await nat_1.defaultBaseUrl.get();
}
function getHttpsWorkingPort() {
var _a;
return (httpsSrv === null || httpsSrv === void 0 ? void 0 : httpsSrv.listening) && ((_a = httpsSrv.address()) === null || _a === void 0 ? void 0 : _a.port);
}
const commonServerOptions = { requestTimeout: 0 };
const commonServerAssign = { headersTimeout: 30000, timeout: misc_1.MINUTE }; // 'headersTimeout' is not recognized by type lib, and 'timeout' is not effective when passed in parameters
const readyToListen = Promise.all([persistence_1.storedMap.isOpening(), events_1.default.once('app')]);
const considerHttp = (0, misc_1.debounceAsync)(async () => {
await readyToListen;
void stopServer(httpSrv);
httpSrv = Object.assign(http.createServer(commonServerOptions, index_1.app.callback()), { name: 'http' }, commonServerAssign);
const host = listenInterface.get();
const port = exports.portCfg.get();
if (!await startServer(httpSrv, { port, host }))
if (port !== 80)
return console.log(` >> try specifying a different port, enter this command: config ${exports.portCfg.key()} 1080`);
else if (!await startServer(httpSrv, { port: 8080, host }))
return;
httpSrv.on('connection', connections_1.newConnection);
printUrls(httpSrv.name);
if (openBrowserAtStart.get() && !argv_1.argv.updated)
openAdmin();
});
exports.portCfg = (0, config_1.defineConfig)('port', 80);
const listenInterface = (0, config_1.defineConfig)('listen_interface', '');
exports.portCfg.sub(considerHttp);
listenInterface.sub(considerHttp);
function openAdmin() {
for (const srv of [httpSrv, httpsSrv]) {
const a = srv === null || srv === void 0 ? void 0 : srv.address();
if (!a || typeof a === 'string')
continue;
const i = listenInterface.get();
// open() will fail with ::1, don't know why, as my browser correctly opens the resulting url
const hostname = i === '::1' || i in genericInterfaceNames ? 'localhost' : i;
const baseUrl = `${srv.name}://${hostname}:${a.port}`;
(0, open_1.default)(baseUrl + const_1.ADMIN_URI, { wait: true }).catch(async (e) => {
console.debug(String(e));
console.warn("cannot launch browser on this machine >PLEASE< open your browser and reach one of these (you may need a different address)", ...Object.values(await getUrls()).flat().map(x => '\n - ' + x + const_1.ADMIN_URI));
if (!(0, adminApis_1.anyAccountCanLoginAdmin)())
console.log(`HINT: you can enter command: create-admin YOUR_PASSWORD`);
});
return true;
}
console.log("openAdmin failed");
}
function getCertObject() {
var _a;
if (!httpsOptions.cert)
return;
const all = new crypto_1.X509Certificate(httpsOptions.cert);
const some = lodash_1.default.pick(all, ['subject', 'issuer', 'validFrom', 'validTo']);
const ret = (0, misc_1.objSameKeys)(some, v => (v === null || v === void 0 ? void 0 : v.includes('=')) ? Object.fromEntries(v.split('\n').map(x => x.split('='))) : v);
return Object.assign(ret, { altNames: (_a = all.subjectAltName) === null || _a === void 0 ? void 0 : _a.replace(/DNS:/g, '').split(/, */) });
}
const considerHttps = (0, misc_1.debounceAsync)(async () => {
var _a, _b, _c, _d, _e, _f;
await readyToListen;
void stopServer(httpsSrv);
nat_1.defaultBaseUrl.proto = 'http';
nat_1.defaultBaseUrl.port = (_a = getCurrentPort(httpSrv)) !== null && _a !== void 0 ? _a : 0;
let port = exports.httpsPortCfg.get();
try {
const moreOptions = Object.assign({}, ...await events_1.default.emitAsync('httpsServerOptions') || []);
httpsSrv = Object.assign(https.createServer(port === const_1.PORT_DISABLED ? {} : { ...commonServerOptions, key: httpsOptions.private_key, cert: httpsOptions.cert, ...moreOptions }, index_1.app.callback()), { name: 'https' }, commonServerAssign);
if (port >= 0) {
const cert = getCertObject();
if (cert) {
const cn = (_b = cert.subject) === null || _b === void 0 ? void 0 : _b.CN;
if (cn)
console.log("certificate loaded for", ((_c = cert.altNames) === null || _c === void 0 ? void 0 : _c.join(' + ')) || cn);
const now = new Date();
const from = new Date(cert.validFrom);
const to = new Date(cert.validTo);
updateError(); // error will change at from and to dates of the certificate
const cancelTo = (0, misc_1.runAt)(to.getTime(), updateError);
const cancelFrom = (0, misc_1.runAt)(from.getTime(), updateError);
httpsSrv.on('close', () => {
cancelTo();
cancelFrom();
});
function updateError() {
if (!httpsSrv)
return;
httpsSrv.error = from > now ? "certificate not valid yet" : to < now ? "certificate expired" : undefined;
}
}
const namesForOutput = { cert: 'certificate', private_key: 'private key' };
const missing = (_d = httpsNeeds.find(x => !x.get())) === null || _d === void 0 ? void 0 : _d.key();
if (missing)
return httpsSrv.error = "missing " + namesForOutput[missing];
const cantRead = (_e = httpsNeeds.find(x => !httpsOptions[x.key()])) === null || _e === void 0 ? void 0 : _e.key();
if (cantRead)
return httpsSrv.error = "cannot read " + namesForOutput[cantRead];
}
}
catch (e) {
httpsSrv || (httpsSrv = Object.assign(https.createServer({}), { name: 'https' })); // a dummy container, in case creation failed because of certificate errors
httpsSrv.error = "bad private key or certificate";
console.error("failed to create https server: check your private key and certificate", e.message);
return;
}
port = await startServer(httpsSrv, { port, host: listenInterface.get() });
if (!port)
return;
httpsSrv.on('connection', connections_1.newConnection);
printUrls(httpsSrv.name);
events_1.default.emit('httpsReady');
nat_1.defaultBaseUrl.proto = 'https';
nat_1.defaultBaseUrl.port = (_f = getCurrentPort(httpsSrv)) !== null && _f !== void 0 ? _f : 0;
}, { wait: 200 }); // give time to have key and cert ready
exports.cert = (0, config_1.defineConfig)('cert', '');
exports.privateKey = (0, config_1.defineConfig)('private_key', '');
const httpsNeeds = [exports.cert, exports.privateKey];
const httpsOptions = { cert: '', private_key: '' };
for (const cfg of httpsNeeds) {
let unwatch;
cfg.sub(async (v) => {
unwatch === null || unwatch === void 0 ? void 0 : unwatch();
const k = cfg.key();
httpsOptions[k] = v;
if (!v || v.includes('\n'))
return considerHttps();
// v is a path
httpsOptions[k] = '';
unwatch = (0, watchLoad_1.watchLoad)(v, async (data) => {
httpsOptions[k] = data;
await considerHttps();
}, { immediateFirst: true }).unwatch;
await considerHttps();
});
}
exports.httpsPortCfg = (0, config_1.defineConfig)('https_port', const_1.PORT_DISABLED);
exports.httpsPortCfg.sub(considerHttps);
listenInterface.sub(considerHttps);
const genericInterfaceNames = {
'0.0.0.0': "any IPv4",
'::': "any IPv6",
'': "any network",
};
function renderHost(host) {
return (0, misc_1.xlate)(host, genericInterfaceNames);
}
function startServer(srv, { port, host }) {
return new Promise(async (resolve) => {
if (!srv)
return resolve(0);
try {
if (port === const_1.PORT_DISABLED)
return resolve(0);
if (!host && !await testIpV4()) // !host means ipV4+6, and if v4 port alone is busy we won't be notified of the failure, so we'll first test it on its own
throw srv.error;
// from a few tests, this seems enough to support the expect-100 http/1.1 mechanism, at least with curl -T, not used by chrome|firefox anyway
srv.on('checkContinue', (req, res) => srv.emit('request', req, res));
port = await listen(host);
if (port)
console.log(srv.name, "serving on", renderHost(host || ''), ':', port);
resolve(port);
}
catch (e) {
srv.error = String(e);
console.error(srv.name, `couldn't listen on port ${port}:`, srv.error);
resolve(0);
}
});
async function testIpV4() {
const res = await listen('0.0.0.0', true);
await new Promise(res => srv === null || srv === void 0 ? void 0 : srv.close(res)); // close, if any, and wait
return res > 0;
}
function listen(host, silence = false) {
return new Promise(async (resolve, reject) => {
srv === null || srv === void 0 ? void 0 : srv.on('error', onError).listen({ port, host }, () => {
const ad = srv.address();
if (!ad)
return reject('no address');
if (typeof ad === 'string') {
srv.close();
return reject('type of socket not supported');
}
srv.removeListener('error', onError); // necessary in case someone calls stop/start many times
resolve(ad.port);
});
async function onError(e) {
if (!srv)
return;
srv.error = String(e);
srv.busy = undefined;
const { code } = e;
if (code)
srv.busy = (0, find_process_1.default)('port', port).then(res => res === null || res === void 0 ? void 0 : res.map(x => { var _a; return (0, misc_1.prefix)("Service", x.name === 'svchost.exe' && ((_a = x.cmd.split(x.name)[1]) === null || _a === void 0 ? void 0 : _a.trim())) || x.name; }).join(' + '), () => '');
if (code === 'EACCES' && port < 1024 && !srv.busy) // on Windows, when port is used by a service, we get EACCESS
srv.error = `lacking permission on port ${port}, try with permission (${const_1.IS_WINDOWS ? 'administrator' : 'sudo'}) or port > 1024`;
if (code === 'EADDRINUSE' || srv.busy)
srv.error = `port ${port} busy: ${await srv.busy || "unknown process"}`;
if (!silence)
console.error(srv.name, srv.error);
resolve(0);
}
});
}
}
function stopServer(srv) {
return new Promise(resolve => {
if (!(srv === null || srv === void 0 ? void 0 : srv.listening))
return resolve(null);
const ad = srv.address();
if (ad && typeof ad !== 'string')
console.log("stopped port", ad.port);
srv.close(err => {
if (err && err.code !== 'ERR_SERVER_NOT_RUNNING')
console.debug("failed to stop server", String(err));
resolve(err);
});
});
}
function getCurrentPort(srv) {
var _a;
return (_a = srv === null || srv === void 0 ? void 0 : srv.address()) === null || _a === void 0 ? void 0 : _a.port;
}
async function getServerStatus(includeSrv = true) {
return {
http: await serverStatus(httpSrv, exports.portCfg.get()),
https: await serverStatus(httpsSrv, exports.httpsPortCfg.get()),
};
async function serverStatus(srv, configuredPort) {
const busy = await (srv === null || srv === void 0 ? void 0 : srv.busy);
await (0, misc_1.wait)(0); // simple trick to wait for also .error to be updated. If this trickery becomes necessary elsewhere, then we should make also error a Promise.
return {
...lodash_1.default.pick(srv, ['listening', 'error']),
busy,
port: getCurrentPort(srv) || configuredPort,
configuredPort,
srv: includeSrv ? srv : undefined,
};
}
}
const ignore = /^(lo|.*loopback.*|virtualbox.*|.*\(wsl\).*|llw\d|awdl\d|utun\d|anpi\d)$/i; // avoid giving too much information
// AKA auto-ip https://en.wikipedia.org/wiki/Link-local_address
const isLinkLocal = (0, misc_1.makeNetMatcher)('169.254.0.0/16|FE80::/10');
async function getIps(external = true) {
const only = { '0.0.0.0': 'IPv4', '::': 'IPv6' }[listenInterface.get()] || '';
const ips = (0, misc_1.onlyTruthy)(Object.entries((0, os_1.networkInterfaces)()).flatMap(([name, nets]) => nets && !ignore.test(name) && nets.map(net => !net.internal && (!only || only === net.family) && net.address)));
const e = external && nat_1.defaultBaseUrl.externalIp;
if (e && !ips.includes(e))
ips.push(e);
const noLinkLocal = ips.filter(x => !isLinkLocal(x));
const ret = lodash_1.default.sortBy(noLinkLocal.length ? noLinkLocal : ips, [
x => x !== nat_1.defaultBaseUrl.localIp, // use the "nat" info to put best ip first
net_1.isIPv6 // false=IPV4 comes first
]);
nat_1.defaultBaseUrl.localIp || (nat_1.defaultBaseUrl.localIp = ret[0] || '');
return ret;
}
async function getUrls() {
const on = listenInterface.get();
const ips = on === renderHost(on) ? [on] : await getIps();
return Object.fromEntries((0, misc_1.onlyTruthy)([httpSrv, httpsSrv].map(srv => {
var _a;
if (!(srv === null || srv === void 0 ? void 0 : srv.listening))
return false;
const port = (_a = srv === null || srv === void 0 ? void 0 : srv.address()) === null || _a === void 0 ? void 0 : _a.port;
const appendPort = port === (srv.name === 'https' ? 443 : 80) ? '' : ':' + port;
const urls = ips.map(ip => `${srv.name}://${(0, misc_1.ipForUrl)(ip)}${appendPort}`);
return urls.length && [srv.name, urls];
})));
}
function printUrls(srvName) {
getUrls().then(urls => lodash_1.default.each(urls[srvName], url => console.log('serving on', url)));
}
;