hfs 0.49.0-alpha2 → 0.49.0-alpha4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import{g as OF,c as UF}from"./index-3a379840.js";function gF(sF,hF){for(var eF=0;eF<hF.length;eF++){const tF=hF[eF];if(typeof tF!="string"&&!Array.isArray(tF)){for(const w in tF)if(w!=="default"&&!(w in sF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(sF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(sF,Symbol.toStringTag,{value:"Module"}))}var dF={exports:{}};/*
1
+ import{g as OF,c as UF}from"./index-3c132d35.js";function gF(sF,hF){for(var eF=0;eF<hF.length;eF++){const tF=hF[eF];if(typeof tF!="string"&&!Array.isArray(tF)){for(const w in tF)if(w!=="default"&&!(w in sF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(sF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(sF,Symbol.toStringTag,{value:"Module"}))}var dF={exports:{}};/*
2
2
  * [js-sha512]{@link https://github.com/emn178/js-sha512}
3
3
  *
4
4
  * @version 0.8.0
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" />
6
6
  <link href="/fontello.css" rel="stylesheet" />
7
7
 
8
- <script type="module" crossorigin src="/assets/index-3a379840.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-3c132d35.js"></script>
9
9
  <link rel="stylesheet" href="/assets/index-db5f0e4b.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.49.0-alpha2",
3
+ "version": "0.49.0-alpha4",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
package/src/acme.js ADDED
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.acme_email = exports.acme_domain = exports.makeCert = exports.acmeMiddleware = void 0;
7
+ const cross_1 = require("./cross");
8
+ const http_1 = require("http");
9
+ const nat_1 = require("./nat");
10
+ const listen_1 = require("./listen");
11
+ const apiMiddleware_1 = require("./apiMiddleware");
12
+ const acme_client_1 = __importDefault(require("acme-client"));
13
+ const debounceAsync_1 = require("./debounceAsync");
14
+ const promises_1 = __importDefault(require("fs/promises"));
15
+ const config_1 = require("./config");
16
+ const events_1 = __importDefault(require("./events"));
17
+ const selfCheck_1 = require("./selfCheck");
18
+ let acmeMiddlewareEnabled = false;
19
+ const acmeTokens = {};
20
+ const acmeListener = (req, res) => {
21
+ var _a;
22
+ const BASE = '/.well-known/acme-challenge/';
23
+ if (!((_a = req.url) === null || _a === void 0 ? void 0 : _a.startsWith(BASE)))
24
+ return;
25
+ const token = req.url.slice(BASE.length);
26
+ console.debug("got http challenge", token);
27
+ res.statusCode = cross_1.HTTP_OK;
28
+ res.end(acmeTokens[token]);
29
+ return true;
30
+ };
31
+ const acmeMiddleware = (ctx, next) => {
32
+ if (!acmeMiddlewareEnabled || !Boolean(acmeListener(ctx.req, ctx.res)))
33
+ return next();
34
+ };
35
+ exports.acmeMiddleware = acmeMiddleware;
36
+ async function generateSSLCert(domain, email) {
37
+ // will answer challenge through our koa app (if on port 80) or must we spawn a dedicated server?
38
+ const { upnp, externalPort } = await (0, nat_1.getNatInfo)();
39
+ const { http } = await (0, listen_1.getServerStatus)();
40
+ const tempSrv = externalPort === 80 || http.listening && http.port === 80 ? undefined : (0, http_1.createServer)(acmeListener);
41
+ if (tempSrv)
42
+ await new Promise((resolve) => tempSrv.listen(80, resolve).on('error', (e) => {
43
+ console.debug("cannot listen on 80", e.code || e);
44
+ resolve(); // go on anyway
45
+ }));
46
+ acmeMiddlewareEnabled = true;
47
+ console.debug('acme challenge server ready');
48
+ try {
49
+ const checkUrl = `http://${domain}`;
50
+ let check = await (0, selfCheck_1.selfCheck)(checkUrl); // some check services may not consider the domain, but we already verified that
51
+ if (check && !check.success && upnp && externalPort !== 80) { // consider a short-lived mapping
52
+ console.debug("setting temporary port forward");
53
+ // @ts-ignore
54
+ await nat_1.upnpClient.createMapping({ private: 80, public: { host: '', port: 80 }, description: 'hfs temporary', ttl: 30 }).catch(() => { });
55
+ check = await (0, selfCheck_1.selfCheck)(checkUrl); // repeat test
56
+ }
57
+ //if (!check) throw new ApiError(HTTP_FAILED_DEPENDENCY, "couldn't test port 80")
58
+ if (!(check === null || check === void 0 ? void 0 : check.success))
59
+ throw new apiMiddleware_1.ApiError(cross_1.HTTP_FAILED_DEPENDENCY, "port 80 is not working on the specified domain");
60
+ const acmeClient = new acme_client_1.default.Client({
61
+ accountKey: await acme_client_1.default.crypto.createPrivateKey(),
62
+ directoryUrl: acme_client_1.default.directory.letsencrypt.production
63
+ });
64
+ const [key, csr] = await acme_client_1.default.crypto.createCsr({ commonName: domain });
65
+ const cert = await acmeClient.auto({
66
+ csr,
67
+ email,
68
+ challengePriority: ['http-01'],
69
+ skipChallengeVerification: true,
70
+ termsOfServiceAgreed: true,
71
+ async challengeCreateFn(_, c, ka) {
72
+ console.debug("producing challenge");
73
+ acmeTokens[c.token] = ka;
74
+ },
75
+ async challengeRemoveFn(_, c) {
76
+ delete acmeTokens[c.token];
77
+ },
78
+ });
79
+ return { key, cert };
80
+ }
81
+ finally {
82
+ acmeMiddlewareEnabled = false;
83
+ if (tempSrv)
84
+ await new Promise(res => tempSrv.close(res));
85
+ console.debug('acme terminated');
86
+ }
87
+ }
88
+ exports.makeCert = (0, debounceAsync_1.debounceAsync)(async (domain, email) => {
89
+ if (!domain)
90
+ return new apiMiddleware_1.ApiError(cross_1.HTTP_BAD_REQUEST, 'bad params');
91
+ const res = await generateSSLCert(domain, email);
92
+ const CERT_FILE = 'acme.cert';
93
+ const KEY_FILE = 'acme.key';
94
+ await promises_1.default.writeFile(CERT_FILE, res.cert);
95
+ await promises_1.default.writeFile(KEY_FILE, res.key);
96
+ listen_1.cert.set(CERT_FILE); // update config
97
+ listen_1.privateKey.set(KEY_FILE);
98
+ }, 0);
99
+ (0, config_1.defineConfig)('acme_renew', false); // handle config changes
100
+ events_1.default.once('https ready', () => (0, cross_1.repeat)(cross_1.HOUR, renewCert));
101
+ exports.acme_domain = (0, config_1.defineConfig)('acme_domain', '');
102
+ exports.acme_email = (0, config_1.defineConfig)('acme_email', '');
103
+ // checks if the cert is near expiration date, and if so renews it
104
+ const renewCert = (0, debounceAsync_1.debounceAsync)(async () => {
105
+ const cert = (0, listen_1.getCertObject)();
106
+ if (!cert)
107
+ return;
108
+ const now = new Date();
109
+ const validTo = new Date(cert.validTo);
110
+ // not expiring in a month
111
+ if (now > new Date(cert.validFrom) && now < validTo && validTo.getTime() - now.getTime() >= 30 * cross_1.DAY)
112
+ return console.log("certificate still good");
113
+ await (0, exports.makeCert)(exports.acme_domain.get(), exports.acme_email.get())
114
+ .catch(e => console.log("error renewing certificate: ", String(e)));
115
+ }, 0, { retain: cross_1.DAY, retainFailure: cross_1.HOUR });
package/src/adminApis.js CHANGED
@@ -53,6 +53,7 @@ const customHtml_1 = require("./customHtml");
53
53
  const lodash_1 = __importDefault(require("lodash"));
54
54
  const update_1 = require("./update");
55
55
  const consoleLog_1 = require("./consoleLog");
56
+ const path_1 = require("path");
56
57
  exports.adminApis = {
57
58
  ...api_vfs_1.default,
58
59
  ...api_accounts_1.default,
@@ -76,9 +77,15 @@ exports.adminApis = {
76
77
  return {};
77
78
  },
78
79
  get_config: config_1.getWholeConfig,
79
- update({ tag }) {
80
- return (0, update_1.update)(tag);
80
+ get_config_text() {
81
+ return {
82
+ path: config_1.configFile.getPath(),
83
+ fullPath: (0, path_1.resolve)(config_1.configFile.getPath()),
84
+ text: config_1.configFile.getText(),
85
+ };
81
86
  },
87
+ set_config_text: ({ text }) => config_1.configFile.save(text, { reparse: true }),
88
+ update: ({ tag }) => (0, update_1.update)(tag),
82
89
  async check_update() {
83
90
  return { options: await (0, update_1.getUpdates)() };
84
91
  },
@@ -106,9 +113,10 @@ exports.adminApis = {
106
113
  apiVersion: const_1.API_VERSION,
107
114
  compatibleApiVersion: const_1.COMPATIBLE_API_VERSION,
108
115
  ...await (0, listen_1.getServerStatus)(),
116
+ platform: process.platform,
109
117
  urls: await (0, listen_1.getUrls)(),
110
118
  ips: await (0, listen_1.getIps)(false),
111
- baseUrl: middlewares_1.baseUrl.get(),
119
+ baseUrl: (0, listen_1.getBaseUrlOrDefault)(),
112
120
  updatePossible: !(0, update_1.updateSupported)() ? false : await (0, update_1.localUpdateAvailable)() ? 'local' : true,
113
121
  proxyDetected: (0, middlewares_1.getProxyDetected)(),
114
122
  frpDetected: exports.localhostAdmin.get() && !(0, middlewares_1.getProxyDetected)()
@@ -1,8 +1,5 @@
1
1
  "use strict";
2
2
  // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
3
- var __importDefault = (this && this.__importDefault) || function (mod) {
4
- return (mod && mod.__esModule) ? mod : { "default": mod };
5
- };
6
3
  Object.defineProperty(exports, "__esModule", { value: true });
7
4
  exports.get_file_list = void 0;
8
5
  const vfs_1 = require("./vfs");
@@ -10,12 +7,10 @@ const apiMiddleware_1 = require("./apiMiddleware");
10
7
  const promises_1 = require("fs/promises");
11
8
  const plugins_1 = require("./plugins");
12
9
  const misc_1 = require("./misc");
13
- const lodash_1 = __importDefault(require("lodash"));
14
10
  const const_1 = require("./const");
15
11
  const comments_1 = require("./comments");
16
12
  const path_1 = require("path");
17
13
  const get_file_list = async ({ uri, offset, limit, search, c }, ctx) => {
18
- var _a;
19
14
  const node = await (0, vfs_1.urlToNode)(uri || '/', ctx);
20
15
  const list = ctx.get('accept') === 'text/event-stream' ? new apiMiddleware_1.SendListReadable() : undefined;
21
16
  if (!node)
@@ -24,11 +19,7 @@ const get_file_list = async ({ uri, offset, limit, search, c }, ctx) => {
24
19
  return fail();
25
20
  if ((0, misc_1.dirTraversal)(search))
26
21
  return fail(const_1.HTTP_FOOL);
27
- if (node.default)
28
- return ((_a = list === null || list === void 0 ? void 0 : list.custom) !== null && _a !== void 0 ? _a : lodash_1.default.identity)({
29
- redirect: uri // tell the browser to access the folder (instead of using this api), so it will get the default file
30
- });
31
- if (!await (0, vfs_1.nodeIsDirectory)(node))
22
+ if (await hasDefaultFile(node) || !await (0, vfs_1.nodeIsDirectory)(node))
32
23
  return fail(const_1.HTTP_METHOD_NOT_ALLOWED);
33
24
  offset = Number(offset);
34
25
  limit = Number(limit);
@@ -91,13 +82,16 @@ const get_file_list = async ({ uri, offset, limit, search, c }, ctx) => {
91
82
  break;
92
83
  }
93
84
  }
85
+ async function hasDefaultFile(node) {
86
+ return node.default && await (0, vfs_1.urlToNode)(node.default, ctx, node);
87
+ }
94
88
  async function nodeToDirEntry(ctx, node) {
95
- let { source, default: def } = node;
89
+ let { source } = node;
96
90
  const name = (0, vfs_1.getNodeName)(node);
97
91
  if (!source)
98
92
  return name ? { n: name + '/' } : null;
99
- if (def)
100
- return { n: name };
93
+ if (node.isFolder && await hasDefaultFile(node))
94
+ return { n: name, web: true };
101
95
  try {
102
96
  const st = await (0, promises_1.stat)(source);
103
97
  const folder = st.isDirectory();
package/src/api.net.js CHANGED
@@ -4,264 +4,80 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.makeCert = exports.acme_email = exports.acme_domain = exports.acmeMiddleware = exports.externalIp = void 0;
8
7
  const apiMiddleware_1 = require("./apiMiddleware");
9
- const nat_upnp_ts_1 = require("nat-upnp-ts");
10
8
  const const_1 = require("./const");
11
9
  const lodash_1 = __importDefault(require("lodash"));
12
10
  const listen_1 = require("./listen");
13
11
  const github_1 = require("./github");
14
- const util_http_1 = require("./util-http");
15
- const child_process_1 = require("child_process");
16
12
  const misc_1 = require("./misc");
17
- const acme_client_1 = __importDefault(require("acme-client"));
18
- const promises_1 = __importDefault(require("fs/promises"));
19
- const http_1 = require("http");
20
- const promises_2 = require("dns/promises");
21
- const config_1 = require("./config");
22
- const events_1 = __importDefault(require("./events"));
13
+ const promises_1 = require("dns/promises");
23
14
  const net_1 = require("net");
24
- const upnpClient = new nat_upnp_ts_1.Client({ timeout: 4000 });
25
- const originalMethod = upnpClient.getGateway;
26
- // other client methods call getGateway too, so this will ensure they reuse this same result
27
- upnpClient.getGateway = (0, misc_1.debounceAsync)(() => originalMethod.apply(upnpClient), 0, { retain: misc_1.HOUR, retainFailure: 30000 });
28
- upnpClient.getGateway().catch(() => { });
29
- exports.externalIp = ''; // poll external ip
30
- (0, misc_1.repeat)(10 * misc_1.MINUTE, () => upnpClient.getPublicIp().then(v => exports.externalIp = v));
31
- const getNatInfo = (0, misc_1.debounceAsync)(async () => {
32
- var _a, _b;
33
- const gettingIps = getPublicIps(); // don't wait, do it in parallel
34
- const res = await upnpClient.getGateway().catch(() => null);
35
- const status = await (0, listen_1.getServerStatus)();
36
- const mappings = res && await (0, misc_1.haveTimeout)(5000, upnpClient.getMappings()).catch(() => null);
37
- console.debug('mappings found', mappings === null || mappings === void 0 ? void 0 : mappings.map(x => x.description));
38
- const gatewayIp = res ? new URL(res.gateway.description).hostname : await findGateway().catch(() => undefined);
39
- const localIp = (res === null || res === void 0 ? void 0 : res.address) || (await (0, listen_1.getIps)())[0];
40
- const internalPort = ((_a = status === null || status === void 0 ? void 0 : status.https) === null || _a === void 0 ? void 0 : _a.listening) && status.https.port || ((_b = status === null || status === void 0 ? void 0 : status.http) === null || _b === void 0 ? void 0 : _b.listening) && status.http.port || undefined;
41
- const mapped = lodash_1.default.find(mappings, x => x.private.host === localIp && x.private.port === internalPort);
42
- return {
43
- upnp: Boolean(res),
44
- localIp,
45
- gatewayIp,
46
- publicIps: await gettingIps,
47
- externalIp: exports.externalIp,
48
- mapped,
49
- internalPort,
50
- externalPort: mapped === null || mapped === void 0 ? void 0 : mapped.public.port,
51
- };
52
- });
53
- async function getPublicIps() {
54
- const res = await (0, github_1.getProjectInfo)();
55
- const groupedByVersion = Object.values(lodash_1.default.groupBy(res.publicIpServices, x => { var _a; return (_a = x.v) !== null && _a !== void 0 ? _a : 4; }));
56
- const ips = await (0, misc_1.promiseBestEffort)(groupedByVersion.map(singleVersion => Promise.any(singleVersion.map(async (svc) => {
57
- if (typeof svc === 'string')
58
- svc = { type: 'http', url: svc };
59
- console.debug("trying ip service", svc.url || svc.name);
60
- if (svc.type === 'http')
61
- return (0, util_http_1.httpString)(svc.url);
62
- if (svc.type !== 'dns')
63
- throw "unsupported";
64
- const resolver = new promises_2.Resolver({ timeout: 2000 });
65
- resolver.setServers(svc.ips);
66
- return resolver.resolve(svc.name, svc.dnsRecord);
67
- }).map(async (ret) => {
68
- const validIps = (0, misc_1.wantArray)(await ret).map(x => x.trim()).filter(net_1.isIP);
69
- if (!validIps.length)
70
- throw "no good";
71
- return validIps;
72
- }))));
73
- return lodash_1.default.uniq(ips.flat());
74
- }
75
- function findGateway() {
76
- return new Promise((resolve, reject) => (0, child_process_1.exec)(const_1.IS_WINDOWS || const_1.IS_MAC ? 'netstat -rn' : 'route -n', (err, out) => {
77
- var _a;
78
- if (err)
79
- return reject(err);
80
- const re = const_1.IS_WINDOWS ? /(?:0\.0\.0\.0 +){2}([\d.]+)/ : const_1.IS_MAC ? /default +([\d.]+)/ : /^0\.0\.0\.0 +([\d.]+)/;
81
- resolve((_a = re.exec(out)) === null || _a === void 0 ? void 0 : _a[1]);
82
- }));
83
- }
84
- let acmeMiddlewareEnabled = false;
85
- const acmeTokens = {};
86
- const acmeListener = (req, res) => {
87
- var _a;
88
- const BASE = '/.well-known/acme-challenge/';
89
- if (!((_a = req.url) === null || _a === void 0 ? void 0 : _a.startsWith(BASE)))
90
- return;
91
- const token = req.url.slice(BASE.length);
92
- console.debug("got http challenge", token);
93
- res.statusCode = const_1.HTTP_OK;
94
- res.end(acmeTokens[token]);
95
- return true;
96
- };
97
- const acmeMiddleware = (ctx, next) => {
98
- if (!acmeMiddlewareEnabled || !Boolean(acmeListener(ctx.req, ctx.res)))
99
- return next();
100
- };
101
- exports.acmeMiddleware = acmeMiddleware;
102
- async function checkDomain(domain) {
103
- const resolver = new promises_2.Resolver();
104
- const prjInfo = await (0, github_1.getProjectInfo)();
105
- resolver.setServers(prjInfo.dnsServers);
106
- const settled = await Promise.allSettled([
107
- resolver.resolve(domain, 'A'),
108
- resolver.resolve(domain, 'AAAA'),
109
- (0, promises_2.lookup)(domain).then(x => [x.address]),
110
- ]);
111
- // merge all results
112
- const domainIps = lodash_1.default.uniq((0, misc_1.onlyTruthy)(settled.map(x => x.status === 'fulfilled' && x.value)).flat());
113
- if (!domainIps.length)
114
- throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "domain not working");
115
- const { publicIps } = await getNatInfo(); // do this before stopping the server
116
- for (const v6 of [false, true]) {
117
- const domainIpsThisVersion = domainIps.filter(x => (0, net_1.isIPv6)(x) === v6);
118
- const ipsThisVersion = publicIps.filter(x => (0, net_1.isIPv6)(x) === v6);
119
- if (domainIpsThisVersion.length && ipsThisVersion.length && !lodash_1.default.intersection(domainIpsThisVersion, ipsThisVersion).length)
120
- throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, `configure your domain to point to ${ipsThisVersion} (currently on ${domainIpsThisVersion[0]}) – a change can take hours to be effective`);
121
- }
122
- }
123
- async function generateSSLCert(domain, email) {
124
- await checkDomain(domain);
125
- // will answer challenge through our koa app (if on port 80) or must we spawn a dedicated server?
126
- const { upnp, externalPort } = await getNatInfo();
127
- const { http } = await (0, listen_1.getServerStatus)();
128
- const tempSrv = externalPort === 80 || http.listening && http.port === 80 ? undefined : (0, http_1.createServer)(acmeListener);
129
- if (tempSrv)
130
- await new Promise((resolve) => tempSrv.listen(80, resolve).on('error', (e) => {
131
- console.debug("cannot listen on 80", e.code || e);
132
- resolve(); // go on anyway
133
- }));
134
- acmeMiddlewareEnabled = true;
135
- console.debug('acme challenge server ready');
136
- try {
137
- let check = await checkPort(domain, 80); // some check services may not consider the domain, but we already verified that
138
- if (check && !check.success && upnp && externalPort !== 80) { // consider a short-lived mapping
139
- // @ts-ignore
140
- await upnpClient.createMapping({ private: 80, public: { host: '', port: 80 }, description: 'hfs temporary', ttl: 30 }).catch(() => { });
141
- check = await checkPort(domain, 80); // repeat test
142
- }
143
- if (!check)
144
- throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "couldn't test port 80");
145
- if (!check.success)
146
- throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "port 80 is not working on the specified domain");
147
- const acmeClient = new acme_client_1.default.Client({
148
- accountKey: await acme_client_1.default.crypto.createPrivateKey(),
149
- directoryUrl: acme_client_1.default.directory.letsencrypt.production
150
- });
151
- const [key, csr] = await acme_client_1.default.crypto.createCsr({ commonName: domain });
152
- const cert = await acmeClient.auto({
153
- csr,
154
- email,
155
- challengePriority: ['http-01'],
156
- skipChallengeVerification: true,
157
- termsOfServiceAgreed: true,
158
- async challengeCreateFn(_, c, ka) {
159
- console.debug("producing challenge");
160
- acmeTokens[c.token] = ka;
161
- },
162
- async challengeRemoveFn(_, c) {
163
- delete acmeTokens[c.token];
164
- },
165
- });
166
- return { key, cert };
167
- }
168
- finally {
169
- acmeMiddlewareEnabled = false;
170
- if (tempSrv)
171
- await new Promise(res => tempSrv.close(res));
172
- console.debug('acme terminated');
173
- }
174
- }
175
- async function checkPort(ip, port) {
176
- const prjInfo = await (0, github_1.getProjectInfo)();
177
- console.log(`checking server ${ip}:${port}`);
178
- for (const services of lodash_1.default.chunk(lodash_1.default.shuffle(prjInfo.checkServerServices), 2)) {
179
- try {
180
- return Promise.any(services.map(async ({ url, body, selector, regexpSuccess, regexpFailure, ...rest }) => {
181
- const service = new URL(url).hostname;
182
- console.log('trying service', service);
183
- const res = await (0, util_http_1.httpString)(applySymbols(url), { family: (0, net_1.isIPv6)(ip) ? 6 : 4, body: applySymbols(body), ...rest });
184
- const success = new RegExp(regexpSuccess).test(res);
185
- const failure = new RegExp(regexpFailure).test(res);
186
- if (success === failure)
187
- throw console.debug('inconsistent:' + service); // this result cannot be trusted
188
- console.debug(service, 'responded', success);
189
- return { success, service, ip, port };
190
- }));
191
- }
192
- catch (_a) { }
193
- }
194
- function applySymbols(s) {
195
- return s === null || s === void 0 ? void 0 : s.replace('$IP', ip).replace('$PORT', String(port));
196
- }
197
- }
15
+ const nat_1 = require("./nat");
16
+ const acme_1 = require("./acme");
17
+ const selfCheck_1 = require("./selfCheck");
198
18
  const apis = {
199
- get_nat: getNatInfo,
200
- check_domain({ domain }) {
19
+ get_nat: nat_1.getNatInfo,
20
+ async check_domain({ domain }) {
201
21
  (0, misc_1.apiAssertTypes)({ string: domain });
202
- return checkDomain(domain);
22
+ const resolver = new promises_1.Resolver();
23
+ const prjInfo = await (0, github_1.getProjectInfo)();
24
+ resolver.setServers(prjInfo.dnsServers);
25
+ const settled = await Promise.allSettled([
26
+ resolver.resolve(domain, 'A'),
27
+ resolver.resolve(domain, 'AAAA'),
28
+ (0, promises_1.lookup)(domain).then(x => [x.address]),
29
+ ]);
30
+ // merge all results
31
+ const domainIps = lodash_1.default.uniq((0, misc_1.onlyTruthy)(settled.map(x => x.status === 'fulfilled' && x.value)).flat());
32
+ if (!domainIps.length)
33
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "domain not working");
34
+ const { publicIps } = await (0, nat_1.getNatInfo)(); // do this before stopping the server
35
+ for (const v6 of [false, true]) {
36
+ const domainIpsThisVersion = domainIps.filter(x => (0, net_1.isIPv6)(x) === v6);
37
+ const ipsThisVersion = publicIps.filter(x => (0, net_1.isIPv6)(x) === v6);
38
+ if (domainIpsThisVersion.length && ipsThisVersion.length && !lodash_1.default.intersection(domainIpsThisVersion, ipsThisVersion).length)
39
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_PRECONDITION_FAILED, `configure your domain to point to ${ipsThisVersion} (currently on ${domainIpsThisVersion[0]}) – a change can take hours to be effective`);
40
+ }
41
+ return {};
203
42
  },
204
43
  async map_port({ external, internal }) {
205
- const { upnp, externalPort, internalPort } = await getNatInfo();
44
+ const { upnp, externalPort, internalPort } = await (0, nat_1.getNatInfo)();
206
45
  if (!upnp)
207
46
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE, 'upnp failed');
208
47
  if (!internalPort)
209
48
  return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, 'no internal port');
210
49
  if (externalPort)
211
50
  try {
212
- await upnpClient.removeMapping({ public: { host: '', port: externalPort } });
51
+ await nat_1.upnpClient.removeMapping({ public: { host: '', port: externalPort } });
213
52
  }
214
53
  catch (e) {
215
54
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, 'removeMapping failed: ' + String(e));
216
55
  }
217
56
  if (external) // must use the object form of 'public' to work around a bug of the library
218
- await upnpClient.createMapping({ private: internal || internalPort, public: { host: '', port: external }, description: 'hfs', ttl: 0 });
57
+ await nat_1.upnpClient.createMapping({ private: internal || internalPort, public: { host: '', port: external }, description: 'hfs', ttl: 0 });
219
58
  return {};
220
59
  },
221
- async check_server({ port }) {
222
- const { publicIps, internalPort, externalPort } = await getNatInfo();
223
- if (!publicIps.length)
60
+ async self_check({ url }) {
61
+ if (url)
62
+ return await (0, selfCheck_1.selfCheck)(url)
63
+ || new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE);
64
+ const nat = await (0, nat_1.getNatInfo)();
65
+ if (!nat.publicIps.length)
224
66
  return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, 'cannot detect public ip');
225
- if (!internalPort)
67
+ if (!nat.internalPort)
226
68
  return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, 'no internal port');
227
- port || (port = externalPort || internalPort);
228
- const res = await (0, misc_1.promiseBestEffort)(publicIps.map(ip => checkPort(ip, port)));
229
- return res.length ? res : new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE);
69
+ const finalPort = nat.externalPort || nat.internalPort;
70
+ const proto = nat.proto || ((0, listen_1.getCertObject)() ? 'https' : 'http');
71
+ const defPort = proto === 'https' ? 443 : 80;
72
+ const results = (0, misc_1.onlyTruthy)(await (0, misc_1.promiseBestEffort)(nat.publicIps.map(ip => (0, selfCheck_1.selfCheck)(`${proto}://${ip}${finalPort === defPort ? '' : ':' + finalPort}`))));
73
+ return results.length ? results : new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE);
230
74
  },
231
75
  async make_cert({ domain, email }) {
232
- await (0, exports.makeCert)(domain, email);
76
+ await (0, acme_1.makeCert)(domain, email);
233
77
  return {};
234
78
  },
235
79
  get_cert() {
236
80
  return (0, misc_1.objSameKeys)(lodash_1.default.pick((0, listen_1.getCertObject)(), ['subject', 'issuer', 'validFrom', 'validTo']), v => v);
237
81
  }
238
82
  };
239
- exports.acme_domain = (0, config_1.defineConfig)('acme_domain', '');
240
- exports.acme_email = (0, config_1.defineConfig)('acme_email', '');
241
- exports.makeCert = (0, misc_1.debounceAsync)(async (domain, email) => {
242
- if (!domain)
243
- return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad params');
244
- const res = await generateSSLCert(domain, email);
245
- const CERT_FILE = 'acme.cert';
246
- const KEY_FILE = 'acme.key';
247
- await promises_1.default.writeFile(CERT_FILE, res.cert);
248
- await promises_1.default.writeFile(KEY_FILE, res.key);
249
- listen_1.cert.set(CERT_FILE); // update config
250
- listen_1.privateKey.set(KEY_FILE);
251
- }, 0);
252
- (0, config_1.defineConfig)('acme_renew', false); // handle config changes
253
- events_1.default.once('https ready', () => (0, misc_1.repeat)(misc_1.HOUR, renewCert));
254
- // checks if the cert is near expiration date, and if so renews it
255
- const renewCert = (0, misc_1.debounceAsync)(async () => {
256
- const cert = (0, listen_1.getCertObject)();
257
- if (!cert)
258
- return;
259
- const now = new Date();
260
- const validTo = new Date(cert.validTo);
261
- // not expiring in a month
262
- if (now > new Date(cert.validFrom) && now < validTo && validTo.getTime() - now.getTime() >= 30 * misc_1.DAY)
263
- return console.log("certificate still good");
264
- await (0, exports.makeCert)(exports.acme_domain.get(), exports.acme_email.get())
265
- .catch(e => console.log("error renewing certificate: ", String(e)));
266
- }, 0, { retain: misc_1.DAY, retainFailure: misc_1.HOUR });
267
83
  exports.default = apis;
package/src/api.vfs.js CHANGED
@@ -12,6 +12,9 @@ const path_1 = require("path");
12
12
  const misc_1 = require("./misc");
13
13
  const const_1 = require("./const");
14
14
  const util_os_1 = require("./util-os");
15
+ const listen_1 = require("./listen");
16
+ const os_1 = require("os");
17
+ const open_1 = __importDefault(require("open"));
15
18
  // to manipulate the tree we need the original node
16
19
  async function urlToNodeOriginal(uri) {
17
20
  const n = await (0, vfs_1.urlToNode)(uri);
@@ -92,25 +95,32 @@ const apis = {
92
95
  var _a;
93
96
  if (!source && !name)
94
97
  return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'name or source required');
95
- parent = parent ? await urlToNodeOriginal(parent) : vfs_1.vfs;
96
- if (!parent)
98
+ const parentNode = parent ? await urlToNodeOriginal(parent) : vfs_1.vfs;
99
+ if (!parentNode)
97
100
  return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'parent not found');
98
- if (!await (0, vfs_1.nodeIsDirectory)(parent))
101
+ if (!await (0, vfs_1.nodeIsDirectory)(parentNode))
99
102
  return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_ACCEPTABLE, 'parent not a folder');
100
103
  if ((0, misc_1.isWindowsDrive)(source))
101
104
  source += '\\'; // slash must be included, otherwise it will refer to the cwd of that drive
105
+ const isDir = source && await (0, misc_1.isDirectory)(source);
106
+ if (source && isDir === undefined)
107
+ return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'source not found');
102
108
  const child = { source, name };
103
109
  name = (0, vfs_1.getNodeName)(child); // could be not given as input
104
110
  const ext = (0, path_1.extname)(name);
105
111
  const noExt = ext ? name.slice(0, -ext.length) : name;
106
112
  let idx = 2;
107
- while ((_a = parent.children) === null || _a === void 0 ? void 0 : _a.find((0, vfs_1.isSameFilenameAs)(name)))
113
+ while ((_a = parentNode.children) === null || _a === void 0 ? void 0 : _a.find((0, vfs_1.isSameFilenameAs)(name)))
108
114
  name = `${noExt} ${idx++}${ext}`;
109
115
  child.name = name;
110
116
  simplifyName(child);
111
- (parent.children || (parent.children = [])).unshift(child);
117
+ (parentNode.children || (parentNode.children = [])).unshift(child);
112
118
  await (0, vfs_1.saveVfs)();
113
- return { name };
119
+ const link = (0, listen_1.getBaseUrlOrDefault)()
120
+ + (parent ? (0, misc_1.enforceFinal)('/', parent) : '/')
121
+ + encodeURIComponent((0, vfs_1.getNodeName)(child))
122
+ + (isDir ? '/' : '');
123
+ return { name, link };
114
124
  },
115
125
  async del_vfs({ uris }) {
116
126
  if (!uris || !Array.isArray(uris))
@@ -189,7 +199,10 @@ const apis = {
189
199
  }
190
200
  }
191
201
  });
192
- }
202
+ },
203
+ async windows_integration() {
204
+ return { finish: await windowsIntegration() };
205
+ },
193
206
  };
194
207
  exports.default = apis;
195
208
  // pick only selected props, and consider null and empty string as undefined
@@ -206,3 +219,23 @@ function simplifyName(node) {
206
219
  if ((0, vfs_1.getNodeName)(noName) === name)
207
220
  delete node.name;
208
221
  }
222
+ async function windowsIntegration() {
223
+ const status = await (0, listen_1.getServerStatus)();
224
+ const url = 'http://localhost:' + status.http.port;
225
+ const content = `Windows Registry Editor Version 5.00
226
+ ` + ['*', 'Directory'].map(k => `
227
+ [HKEY_CLASSES_ROOT\\${k}\\shell\\AddToHFS3]
228
+ @="Add to HFS (new)"
229
+
230
+ [HKEY_CLASSES_ROOT\\${k}\\shell\\AddToHFS3\\command]
231
+ @="powershell -Command \\"$p = '%1'.Replace('\\\\', '\\\\\\\\'); $j = '{ \\\\\\"source\\\\\\": \\\\\\"' + $p + '\\\\\\" }'; $wsh = New-Object -ComObject Wscript.Shell; try { $res = Invoke-WebRequest -Uri '${url}/~/api/add_vfs' -Method POST -Headers @{ 'x-hfs-anti-csrf' = '1' } -ContentType 'application/json' -TimeoutSec 1 -Body $j; $json = $res.Content | ConvertFrom-Json; $link = $json.link; $link | Set-Clipboard; } catch { $wsh.Popup('Server is down', 0, 'Error', 16); }\\""
232
+ `);
233
+ const path = (0, os_1.homedir)() + '\\desktop\\hfs-windows-menu.reg';
234
+ await (0, promises_1.writeFile)(path, content, 'utf8');
235
+ try {
236
+ await (0, open_1.default)(path, { wait: true });
237
+ }
238
+ catch (_a) {
239
+ return path;
240
+ }
241
+ }
@@ -137,8 +137,8 @@ class SendListReadable extends stream_1.Readable {
137
137
  ready() {
138
138
  this._push(['ready']);
139
139
  }
140
- custom(data) {
141
- this._push(data);
140
+ custom(name, data) {
141
+ this._push([name, data]);
142
142
  }
143
143
  props(props) {
144
144
  this._push(['props', props]);