UNPKG

hfs

Version:
346 lines (345 loc) 17.4 kB
"use strict"; // 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))); }