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.
- package/README.md +8 -17
- package/admin/Thumbs.db +0 -0
- package/admin/assets/{index-6afd4b06.js → index-5436e6f5.js} +78 -73
- package/admin/assets/{sha512-f5f12dc1.js → sha512-c4295d63.js} +1 -1
- package/admin/index.html +1 -1
- package/admin/win-shell.png +0 -0
- package/central.json +12 -0
- package/frontend/assets/{index-3a379840.js → index-3c132d35.js} +3 -3
- package/frontend/assets/{sha512-3392c31f.js → sha512-60af0ce6.js} +1 -1
- package/frontend/index.html +1 -1
- package/package.json +1 -1
- package/src/acme.js +115 -0
- package/src/adminApis.js +11 -3
- package/src/api.file_list.js +7 -13
- package/src/api.net.js +42 -226
- package/src/api.vfs.js +40 -7
- package/src/apiMiddleware.js +2 -2
- package/src/commands.js +3 -10
- package/src/config.js +9 -6
- package/src/const.js +7 -30
- package/src/cross-const.js +31 -0
- package/src/cross.js +18 -3
- package/src/frontEndApis.js +1 -1
- package/src/index.js +5 -3
- package/src/listen.js +25 -11
- package/src/middlewares.js +4 -4
- package/src/misc.js +1 -1
- package/src/nat.js +101 -0
- package/src/perm.js +16 -1
- package/src/plugins.js +2 -1
- package/src/selfCheck.js +60 -0
- package/src/serveFile.js +1 -1
- package/src/serveGuiFiles.js +2 -1
- package/src/update.js +13 -5
- package/src/util-files.js +4 -5
- package/src/util-http.js +1 -3
- package/src/vfs.js +6 -2
- package/src/watchLoad.js +6 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{g as OF,c as UF}from"./index-
|
|
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
|
package/frontend/index.html
CHANGED
|
@@ -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-
|
|
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
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
|
-
|
|
80
|
-
return
|
|
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:
|
|
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)()
|
package/src/api.file_list.js
CHANGED
|
@@ -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.
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
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,
|
|
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
|
-
|
|
96
|
-
if (!
|
|
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)(
|
|
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 =
|
|
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
|
-
(
|
|
117
|
+
(parentNode.children || (parentNode.children = [])).unshift(child);
|
|
112
118
|
await (0, vfs_1.saveVfs)();
|
|
113
|
-
|
|
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
|
+
}
|
package/src/apiMiddleware.js
CHANGED
|
@@ -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]);
|