hfs 0.52.4 → 0.53.0-alpha1
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/admin/assets/index-ClcJhcGU.js +805 -0
- package/admin/assets/{sha512-a95yq7Nq.js → sha512-BBuEMQr3.js} +1 -1
- package/admin/index.html +2 -2
- package/frontend/assets/index-legacy-DBpnLaOV.js +60 -0
- package/frontend/assets/polyfills-legacy-DMrMt_pQ.js +4 -0
- package/frontend/assets/sha512-legacy-C1fsQ1Io.js +9 -0
- package/frontend/index.html +3 -2
- package/package.json +1 -1
- package/plugins/antibrute/plugin.js +35 -23
- package/src/QuickZipStream.js +1 -1
- package/src/SendList.js +1 -3
- package/src/adminApis.js +1 -1
- package/src/api.accounts.js +2 -2
- package/src/api.auth.js +7 -2
- package/src/api.get_file_list.js +1 -1
- package/src/api.log.js +2 -6
- package/src/api.plugins.js +10 -14
- package/src/api.vfs.js +2 -2
- package/src/auth.js +14 -5
- package/src/block.js +7 -7
- package/src/comments.js +5 -1
- package/src/config.js +15 -21
- package/src/const.js +2 -2
- package/src/cross.js +6 -8
- package/src/ddns.js +19 -18
- package/src/events.js +56 -5
- package/src/geo.js +1 -1
- package/src/index.js +1 -1
- package/src/langs/hfs-lang-it.json +7 -3
- package/src/listen.js +3 -3
- package/src/log.js +4 -0
- package/src/middlewares.js +8 -5
- package/src/misc.js +2 -15
- package/src/nat.js +4 -2
- package/src/perm.js +17 -11
- package/src/roots.js +15 -23
- package/src/serveGuiAndSharedFiles.js +11 -8
- package/src/serveGuiFiles.js +2 -5
- package/src/throttler.js +14 -12
- package/src/upload.js +7 -1
- package/admin/assets/index-LkaBe_Fp.js +0 -811
- package/frontend/assets/index-c8LVGHJd.js +0 -101
- package/frontend/assets/index-oekM6dN8.css +0 -1
- package/frontend/assets/sha512-PGV0A--c.js +0 -8
- /package/admin/assets/{index-sCDewjON.css → index-CwIN7CM4.css} +0 -0
package/src/cross.js
CHANGED
|
@@ -43,8 +43,8 @@ exports.FRONTEND_OPTIONS = {
|
|
|
43
43
|
exports.SORT_BY_OPTIONS = ['name', 'extension', 'size', 'time'];
|
|
44
44
|
exports.THEME_OPTIONS = { auto: '', light: 'light', dark: 'dark' };
|
|
45
45
|
exports.CFG = constMap(['geo_enable', 'geo_allow', 'geo_list', 'geo_allow_unknown', 'dynamic_dns_url',
|
|
46
|
-
'log', 'error_log', 'log_rotation', 'dont_log_net', 'log_gui', 'log_api', 'log_ua',
|
|
47
|
-
'max_downloads', 'max_downloads_per_ip', 'max_downloads_per_account', 'roots', '
|
|
46
|
+
'log', 'error_log', 'log_rotation', 'dont_log_net', 'log_gui', 'log_api', 'log_ua', 'log_spam',
|
|
47
|
+
'max_downloads', 'max_downloads_per_ip', 'max_downloads_per_account', 'roots', 'force_address']);
|
|
48
48
|
exports.LIST = { add: '+', remove: '-', update: '=', props: 'props', ready: 'ready', error: 'e' };
|
|
49
49
|
exports.WHO_ANYONE = true;
|
|
50
50
|
exports.WHO_NO_ONE = false;
|
|
@@ -216,9 +216,7 @@ function findDefined(a, cb) {
|
|
|
216
216
|
}
|
|
217
217
|
exports.findDefined = findDefined;
|
|
218
218
|
function newObj(src, returnNewValue, recur = false) {
|
|
219
|
-
|
|
220
|
-
return {};
|
|
221
|
-
const pairs = Object.entries(src).map(([k, v]) => {
|
|
219
|
+
const pairs = Object.entries(src || {}).map(([k, v]) => {
|
|
222
220
|
if (typeof k === 'symbol')
|
|
223
221
|
return;
|
|
224
222
|
let _k = k;
|
|
@@ -315,7 +313,7 @@ function repeat(everyMs, cb) {
|
|
|
315
313
|
}
|
|
316
314
|
exports.repeat = repeat;
|
|
317
315
|
function formatTimestamp(x) {
|
|
318
|
-
return !x ? '
|
|
316
|
+
return !x ? '' : (x instanceof Date ? x : new Date(x)).toLocaleString();
|
|
319
317
|
}
|
|
320
318
|
exports.formatTimestamp = formatTimestamp;
|
|
321
319
|
function isPrimitive(x) {
|
|
@@ -407,8 +405,8 @@ function matches(s, mask, emptyMaskReturns = false) {
|
|
|
407
405
|
exports.matches = matches;
|
|
408
406
|
function replace(s, symbols, delimiter = '') {
|
|
409
407
|
const [open, close] = splitAt(' ', delimiter);
|
|
410
|
-
for (const [k, v] of
|
|
411
|
-
s = s.
|
|
408
|
+
for (const [k, v] of Object.entries(symbols))
|
|
409
|
+
s = s.replaceAll(open + k + close, v); // typescript doesn't handle overloaded functions (like replaceAll) with union types https://stackoverflow.com/a/66510061/646132
|
|
412
410
|
return s;
|
|
413
411
|
}
|
|
414
412
|
exports.replace = replace;
|
package/src/ddns.js
CHANGED
|
@@ -12,42 +12,43 @@ const node_net_1 = require("node:net");
|
|
|
12
12
|
const net_1 = require("net");
|
|
13
13
|
const const_1 = require("./const");
|
|
14
14
|
const events_1 = __importDefault(require("./events"));
|
|
15
|
-
const stream_1 = require("stream");
|
|
16
15
|
const nat_1 = require("./nat");
|
|
17
16
|
// optionally you can append '>' and a regular expression to determine what body is considered successful
|
|
18
17
|
const dynamicDnsUrl = (0, config_1.defineConfig)(cross_1.CFG.dynamic_dns_url, '');
|
|
19
18
|
let stop;
|
|
20
|
-
let last;
|
|
21
19
|
dynamicDnsUrl.sub(v => {
|
|
22
20
|
stop === null || stop === void 0 ? void 0 : stop();
|
|
23
21
|
if (!v)
|
|
24
22
|
return;
|
|
25
23
|
let lastIps;
|
|
26
|
-
const [templateUrl, re] = (0, cross_1.splitAt)('>', v);
|
|
27
24
|
stop = (0, cross_1.repeat)(cross_1.HOUR, async () => {
|
|
28
25
|
const ips = await (0, nat_1.getPublicIps)();
|
|
29
26
|
if (lodash_1.default.isEqual(lastIps, ips))
|
|
30
27
|
return;
|
|
31
28
|
lastIps = ips;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
const all = await Promise.all(v.split('\n').map(async (line) => {
|
|
30
|
+
const [templateUrl, re] = (0, cross_1.splitAt)('>', line);
|
|
31
|
+
const url = (0, cross_1.replace)(templateUrl, {
|
|
32
|
+
IPX: ips[0] || '',
|
|
33
|
+
IP4: lodash_1.default.find(ips, node_net_1.isIPv4) || '',
|
|
34
|
+
IP6: lodash_1.default.find(ips, net_1.isIPv6) || '',
|
|
35
|
+
}, '$');
|
|
36
|
+
const error = await (0, util_http_1.httpWithBody)(url, { httpThrow: false, headers: { 'User-Agent': "HFS/" + const_1.VERSION } }) // UA specified as requested by no-ip guidelines
|
|
37
|
+
.then(async (res) => {
|
|
38
|
+
const str = String(res.body).trim();
|
|
39
|
+
return (re ? str.match(re) : res.ok) ? '' : (str || res.statusMessage);
|
|
40
|
+
}, (err) => err.code || err.message || String(err));
|
|
41
|
+
return { ts: new Date().toJSON(), error, url };
|
|
42
|
+
}));
|
|
43
|
+
const best = lodash_1.default.find(all, 'error') || all[0]; // the system is designed for just one result, and we give precedence to errors
|
|
44
|
+
events_1.default.emit('dynamicDnsError', best);
|
|
45
|
+
console.log('dynamic dns update', (best === null || best === void 0 ? void 0 : best.error) || 'ok');
|
|
45
46
|
});
|
|
46
47
|
});
|
|
47
48
|
async function* get_dynamic_dns_error() {
|
|
48
49
|
while (1) {
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
const res = await events_1.default.once('dynamicDnsError');
|
|
51
|
+
yield res[0];
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
exports.get_dynamic_dns_error = get_dynamic_dns_error;
|
package/src/events.js
CHANGED
|
@@ -1,9 +1,60 @@
|
|
|
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.BetterEventEmitter = void 0;
|
|
5
|
+
class BetterEventEmitter {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.listeners = new Map();
|
|
8
|
+
}
|
|
9
|
+
on(event, listener, { warnAfter = 10 } = {}) {
|
|
10
|
+
if (typeof event === 'string')
|
|
11
|
+
event = [event];
|
|
12
|
+
for (const e of event) {
|
|
13
|
+
let cbs = this.listeners.get(e);
|
|
14
|
+
if (!cbs)
|
|
15
|
+
this.listeners.set(e, cbs = new Set());
|
|
16
|
+
cbs.add(listener);
|
|
17
|
+
if (cbs.size > warnAfter)
|
|
18
|
+
console.warn("Warning: many events listeners for ", e);
|
|
19
|
+
}
|
|
20
|
+
return () => {
|
|
21
|
+
var _a;
|
|
22
|
+
for (const e of event)
|
|
23
|
+
(_a = this.listeners.get(e)) === null || _a === void 0 ? void 0 : _a.delete(listener);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
once(event, listener) {
|
|
27
|
+
return new Promise(resolve => {
|
|
28
|
+
const off = this.on(event, function (...args) {
|
|
29
|
+
off();
|
|
30
|
+
resolve(args);
|
|
31
|
+
return listener === null || listener === void 0 ? void 0 : listener(...arguments);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
multi(map) {
|
|
36
|
+
const cbs = Object.entries(map).map(([name, cb]) => this.on(name.split(' '), cb));
|
|
37
|
+
return () => {
|
|
38
|
+
for (const cb of cbs)
|
|
39
|
+
cb();
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
emit(event, ...args) {
|
|
43
|
+
let cbs = this.listeners.get(event);
|
|
44
|
+
if (!(cbs === null || cbs === void 0 ? void 0 : cbs.size))
|
|
45
|
+
return;
|
|
46
|
+
const ret = [];
|
|
47
|
+
for (const cb of cbs) {
|
|
48
|
+
const res = cb(...args);
|
|
49
|
+
if (res !== undefined)
|
|
50
|
+
ret.push(res);
|
|
51
|
+
}
|
|
52
|
+
return ret;
|
|
53
|
+
}
|
|
54
|
+
emitAsync(event, ...args) {
|
|
55
|
+
return Promise.all(this.emit(event, ...args) || []);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.BetterEventEmitter = BetterEventEmitter;
|
|
8
59
|
// app-wide events
|
|
9
|
-
exports.default = new
|
|
60
|
+
exports.default = new BetterEventEmitter;
|
package/src/geo.js
CHANGED
|
@@ -45,7 +45,7 @@ async function checkFiles() {
|
|
|
45
45
|
const TEMP = LOCAL_FILE + '.downloading';
|
|
46
46
|
const { mtime = 0 } = await (0, promises_1.stat)(LOCAL_FILE).catch(() => ({ mtime: 0 }));
|
|
47
47
|
const now = Date.now();
|
|
48
|
-
if (mtime < now - 31 * misc_1.DAY) { // month-old or non-existing
|
|
48
|
+
if (+mtime < now - 31 * misc_1.DAY) { // month-old or non-existing
|
|
49
49
|
console.log('downloading geo-ip db');
|
|
50
50
|
await (0, misc_1.unzip)(await (0, misc_1.httpStream)(URL), path => path.toUpperCase().endsWith(ZIP_FILE) && TEMP);
|
|
51
51
|
if (await (0, promises_1.stat)(TEMP))
|
package/src/index.js
CHANGED
|
@@ -45,9 +45,9 @@ exports.app.use(middlewares_1.sessionMiddleware)
|
|
|
45
45
|
.use(middlewares_1.gzipper)
|
|
46
46
|
.use(middlewares_1.paramsDecoder) // must be done before plugins, so they can manipulate params
|
|
47
47
|
.use(middlewares_1.headRequests)
|
|
48
|
+
.use(roots_1.rootsMiddleware)
|
|
48
49
|
.use(log_1.logMw)
|
|
49
50
|
.use(throttler_1.throttler)
|
|
50
|
-
.use(roots_1.rootsMiddleware)
|
|
51
51
|
.use(plugins_1.pluginsMiddleware)
|
|
52
52
|
.use((0, koa_mount_1.default)(const_1.API_URI, (0, apiMiddleware_1.apiMiddleware)({ ...frontEndApis_1.frontEndApis, ...adminApis_1.adminApis })))
|
|
53
53
|
.use(serveGuiAndSharedFiles_1.serveGuiAndSharedFiles)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Massimo Melina",
|
|
3
|
-
"version":
|
|
4
|
-
"hfs_version": "0.
|
|
3
|
+
"version": 2.53,
|
|
4
|
+
"hfs_version": "0.53.0",
|
|
5
5
|
"translate": {
|
|
6
6
|
"Select": "Seleziona",
|
|
7
7
|
"n_files": "{n} file",
|
|
@@ -145,6 +145,10 @@
|
|
|
145
145
|
"upload_skipped": "{n,plural, one{# saltato} other{# saltati}}",
|
|
146
146
|
"Overwrite policy": "Per i file esistenti",
|
|
147
147
|
"Rename to avoid overwriting": "Rinomina per non sovrascrivere",
|
|
148
|
-
"Overwrite existing files": "Sovrascrivi i file esistenti"
|
|
148
|
+
"Overwrite existing files": "Sovrascrivi i file esistenti",
|
|
149
|
+
"Menu": "Menu",
|
|
150
|
+
"clipboard": "Appunti ({content})",
|
|
151
|
+
"to_clipboard_source_tooltip": "Vai alla cartella d'origine dei file negli appunti",
|
|
152
|
+
"more_items": "{rest,plural,one{Un altro elemento} other{# altri elementi}}"
|
|
149
153
|
}
|
|
150
154
|
}
|
package/src/listen.js
CHANGED
|
@@ -49,8 +49,8 @@ let httpSrv;
|
|
|
49
49
|
let httpsSrv;
|
|
50
50
|
const openBrowserAtStart = (0, config_1.defineConfig)('open_browser_at_start', !const_1.DEV);
|
|
51
51
|
exports.baseUrl = (0, config_1.defineConfig)('base_url', '', x => { var _a; return (_a = /(?<=\/\/)[^\/]+/.exec(x)) === null || _a === void 0 ? void 0 : _a[0]; }); // compiled is host only
|
|
52
|
-
function getBaseUrlOrDefault() {
|
|
53
|
-
return exports.baseUrl.get() || nat_1.defaultBaseUrl.get();
|
|
52
|
+
async function getBaseUrlOrDefault() {
|
|
53
|
+
return exports.baseUrl.get() || await nat_1.defaultBaseUrl.get();
|
|
54
54
|
}
|
|
55
55
|
exports.getBaseUrlOrDefault = getBaseUrlOrDefault;
|
|
56
56
|
function getHttpsWorkingPort() {
|
|
@@ -280,7 +280,7 @@ exports.getServerStatus = getServerStatus;
|
|
|
280
280
|
const ignore = /^(lo|.*loopback.*|virtualbox.*|.*\(wsl\).*|llw\d|awdl\d|utun\d|anpi\d)$/i; // avoid giving too much information
|
|
281
281
|
const isLinkLocal = (0, misc_1.makeNetMatcher)('169.254.0.0/16|FE80::/16');
|
|
282
282
|
async function getIps(external = true) {
|
|
283
|
-
const ips = (0, misc_1.onlyTruthy)(Object.entries((0, os_1.networkInterfaces)()).
|
|
283
|
+
const ips = (0, misc_1.onlyTruthy)(Object.entries((0, os_1.networkInterfaces)()).flatMap(([name, nets]) => nets && !ignore.test(name) && nets.map(net => !net.internal && net.address)));
|
|
284
284
|
const e = external && nat_1.defaultBaseUrl.externalIp;
|
|
285
285
|
if (e && !ips.includes(e))
|
|
286
286
|
ips.unshift(e);
|
package/src/log.js
CHANGED
|
@@ -85,6 +85,7 @@ errorLogFile.sub(path => {
|
|
|
85
85
|
const logRotation = (0, config_1.defineConfig)(misc_1.CFG.log_rotation, 'weekly');
|
|
86
86
|
const dontLogNet = (0, config_1.defineConfig)(misc_1.CFG.dont_log_net, '127.0.0.1|::1', v => (0, misc_1.makeNetMatcher)(v));
|
|
87
87
|
const logUA = (0, config_1.defineConfig)(misc_1.CFG.log_ua, false);
|
|
88
|
+
const logSpam = (0, config_1.defineConfig)(misc_1.CFG.log_spam, false);
|
|
88
89
|
const debounce = lodash_1.default.debounce(cb => cb(), 1000);
|
|
89
90
|
const logMw = async (ctx, next) => {
|
|
90
91
|
const now = new Date();
|
|
@@ -92,6 +93,9 @@ const logMw = async (ctx, next) => {
|
|
|
92
93
|
ctx.state.completed = Promise.race([(0, stream_1.once)(ctx.res, 'finish'), (0, stream_1.once)(ctx.res, 'close')]);
|
|
93
94
|
await next();
|
|
94
95
|
console.debug(ctx.status, ctx.method, ctx.originalUrl);
|
|
96
|
+
if (ctx.status === misc_1.HTTP_NOT_FOUND && !logSpam.get()
|
|
97
|
+
&& /wlwmanifest.xml$|robots.txt$|\.(php)$|cgi/.test(ctx.path))
|
|
98
|
+
return;
|
|
95
99
|
const conn = (0, connections_1.getConnection)(ctx); // collect reference before close
|
|
96
100
|
// don't await, as we don't want to hold the middlewares chain
|
|
97
101
|
ctx.state.completed.then(() => {
|
package/src/middlewares.js
CHANGED
|
@@ -22,7 +22,6 @@ const index_1 = require("./index");
|
|
|
22
22
|
const events_1 = __importDefault(require("./events"));
|
|
23
23
|
const forceHttps = (0, config_1.defineConfig)('force_https', true);
|
|
24
24
|
const ignoreProxies = (0, config_1.defineConfig)('ignore_proxies', false);
|
|
25
|
-
const forceBaseUrl = (0, config_1.defineConfig)('force_base_url', false);
|
|
26
25
|
exports.sessionDuration = (0, config_1.defineConfig)('session_duration', Number(process.env.SESSION_DURATION) || misc_1.DAY / 1000, v => v * 1000);
|
|
27
26
|
exports.gzipper = (0, koa_compress_1.default)({
|
|
28
27
|
threshold: 2048,
|
|
@@ -75,8 +74,6 @@ const someSecurity = async (ctx, next) => {
|
|
|
75
74
|
catch (_a) {
|
|
76
75
|
return ctx.status = const_1.HTTP_FOOL;
|
|
77
76
|
}
|
|
78
|
-
if (!ctx.state.skipFilters && forceBaseUrl.get() && listen_1.baseUrl.compiled() && !(0, misc_1.isLocalHost)(ctx) && ctx.host !== listen_1.baseUrl.compiled())
|
|
79
|
-
return (0, connections_1.disconnect)(ctx, 'force-domain');
|
|
80
77
|
if (!ctx.secure && forceHttps.get() && (0, listen_1.getHttpsWorkingPort)() && !(0, misc_1.isLocalHost)(ctx)) {
|
|
81
78
|
const { URL } = ctx;
|
|
82
79
|
URL.protocol = 'https';
|
|
@@ -98,7 +95,7 @@ exports.getProxyDetected = getProxyDetected;
|
|
|
98
95
|
const prepareState = async (ctx, next) => {
|
|
99
96
|
var _a, _b;
|
|
100
97
|
if ((_a = ctx.session) === null || _a === void 0 ? void 0 : _a.username) {
|
|
101
|
-
if (auth_1.
|
|
98
|
+
if (ctx.session.ts < auth_1.invalidateSessionBefore.get(ctx.session.username))
|
|
102
99
|
delete ctx.session.username;
|
|
103
100
|
ctx.session.maxAge = exports.sessionDuration.compiled();
|
|
104
101
|
}
|
|
@@ -123,11 +120,17 @@ const prepareState = async (ctx, next) => {
|
|
|
123
120
|
return doLogin((credentials === null || credentials === void 0 ? void 0 : credentials.name) || '', (credentials === null || credentials === void 0 ? void 0 : credentials.pass) || '');
|
|
124
121
|
}
|
|
125
122
|
async function doLogin(u, p) {
|
|
123
|
+
var _a;
|
|
124
|
+
if (!u || u === ((_a = ctx.session) === null || _a === void 0 ? void 0 : _a.username))
|
|
125
|
+
return; // providing credentials, but not needed
|
|
126
|
+
await events_1.default.emitAsync('attemptingLogin', ctx);
|
|
126
127
|
const a = await (0, auth_1.srpCheck)(u, p);
|
|
127
128
|
if (a) {
|
|
128
|
-
(0, auth_1.setLoggedIn)(ctx, a.username);
|
|
129
|
+
await (0, auth_1.setLoggedIn)(ctx, a.username);
|
|
129
130
|
ctx.headers['x-username'] = a.username; // give an easier way to determine if the login was successful
|
|
130
131
|
}
|
|
132
|
+
else if (u)
|
|
133
|
+
events_1.default.emit('failedLogin', ctx, { username: u });
|
|
131
134
|
return a;
|
|
132
135
|
}
|
|
133
136
|
};
|
package/src/misc.js
CHANGED
|
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.apiAssertTypes = exports.AsapStream = exports.asyncGeneratorToReadable = exports.same = exports.makeNetMatcher = exports.isLocalHost = exports.
|
|
21
|
+
exports.apiAssertTypes = exports.AsapStream = exports.asyncGeneratorToReadable = exports.same = exports.makeNetMatcher = exports.isLocalHost = exports.pattern2filter = exports.onFirstEvent = exports.onProcessExit = void 0;
|
|
22
22
|
const path_1 = require("path");
|
|
23
23
|
const assert_1 = __importDefault(require("assert"));
|
|
24
24
|
__exportStar(require("./util-http"), exports);
|
|
@@ -55,19 +55,6 @@ function pattern2filter(pattern) {
|
|
|
55
55
|
return (s) => !s || !pattern || matcher((0, path_1.basename)(s));
|
|
56
56
|
}
|
|
57
57
|
exports.pattern2filter = pattern2filter;
|
|
58
|
-
// install multiple handlers and returns a handy 'uninstall' function which requires no parameter. Pass a map {event:handler}
|
|
59
|
-
function onOff(em, events) {
|
|
60
|
-
events = { ...events }; // avoid later modifications, as we need this later for uninstallation
|
|
61
|
-
for (const [k, cb] of Object.entries(events))
|
|
62
|
-
for (const e of k.split(' '))
|
|
63
|
-
em.on(e, cb);
|
|
64
|
-
return () => {
|
|
65
|
-
for (const [k, cb] of Object.entries(events))
|
|
66
|
-
for (const e of k.split(' '))
|
|
67
|
-
em.off(e, cb);
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
exports.onOff = onOff;
|
|
71
58
|
function isLocalHost(c) {
|
|
72
59
|
const ip = typeof c === 'string' ? c : c.socket.remoteAddress; // don't use Context.ip as it is subject to proxied ips, and that's no use for localhost detection
|
|
73
60
|
return ip && (0, cross_1.isIpLocalHost)(ip);
|
|
@@ -120,7 +107,7 @@ function asyncGeneratorToReadable(generator) {
|
|
|
120
107
|
objectMode: true,
|
|
121
108
|
destroy() {
|
|
122
109
|
var _a;
|
|
123
|
-
(_a = iterator.return) === null || _a === void 0 ? void 0 : _a.call(iterator);
|
|
110
|
+
void ((_a = iterator.return) === null || _a === void 0 ? void 0 : _a.call(iterator));
|
|
124
111
|
},
|
|
125
112
|
read() {
|
|
126
113
|
iterator.next().then(it => this.push(it.done ? null : it.value));
|
package/src/nat.js
CHANGED
|
@@ -22,9 +22,11 @@ exports.defaultBaseUrl = (0, valtio_1.proxy)({
|
|
|
22
22
|
externalIp: '',
|
|
23
23
|
localIp: '',
|
|
24
24
|
port: 0,
|
|
25
|
-
get() {
|
|
25
|
+
async get() {
|
|
26
26
|
const defPort = this.proto === 'https' ? 443 : 80;
|
|
27
|
-
|
|
27
|
+
const status = await (0, listen_1.getServerStatus)();
|
|
28
|
+
const port = this.port || (this.proto === 'https' ? status.https.port : status.http.port);
|
|
29
|
+
return `${this.proto}://${this.publicIps[0] || this.externalIp || this.localIp}${!port || port === defPort ? '' : ':' + port}`;
|
|
28
30
|
}
|
|
29
31
|
});
|
|
30
32
|
exports.upnpClient = new nat_upnp_rejetto_1.Client({ timeout: 4000 });
|
package/src/perm.js
CHANGED
|
@@ -41,14 +41,13 @@ const createAdminConfig = (0, config_1.defineConfig)('create-admin', '');
|
|
|
41
41
|
createAdminConfig.sub(v => {
|
|
42
42
|
if (!v)
|
|
43
43
|
return;
|
|
44
|
-
createAdmin(v);
|
|
45
44
|
createAdminConfig.set('');
|
|
45
|
+
// we can't createAdmin right away, as its changes will be lost after return, when our caller (setConfig) applies undefined properties. setTimeout is good enough, as the process is sync.
|
|
46
|
+
setTimeout(() => createAdmin(v));
|
|
46
47
|
});
|
|
47
48
|
async function createAdmin(password, username = 'admin') {
|
|
48
|
-
const acc = await addAccount(username, { admin: true, password });
|
|
49
|
-
|
|
50
|
-
return console.log("cannot create, already exists");
|
|
51
|
-
console.log("account admin created");
|
|
49
|
+
const acc = await addAccount(username, { admin: true, password }, true);
|
|
50
|
+
console.log(acc ? "account admin created" : "something went wrong");
|
|
52
51
|
}
|
|
53
52
|
exports.createAdmin = createAdmin;
|
|
54
53
|
const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
|
|
@@ -59,6 +58,9 @@ async function updateAccount(account, change) {
|
|
|
59
58
|
await (change === null || change === void 0 ? void 0 : change(account));
|
|
60
59
|
else
|
|
61
60
|
Object.assign(account, (0, misc_1.objSameKeys)(change, x => x || undefined));
|
|
61
|
+
for (const [k, v] of (0, misc_1.typedEntries)(account))
|
|
62
|
+
if (!v)
|
|
63
|
+
delete account[k]; // we consider all account fields, when falsy, as equivalent to be missing (so, default value applies)
|
|
62
64
|
const { username, password } = account;
|
|
63
65
|
if (password) {
|
|
64
66
|
console.debug('hashing password for', username);
|
|
@@ -125,14 +127,18 @@ function renameAccount(from, to) {
|
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
exports.renameAccount = renameAccount;
|
|
128
|
-
async function addAccount(username, props) {
|
|
130
|
+
async function addAccount(username, props, updateExisting = false) {
|
|
129
131
|
username = normalizeUsername(username);
|
|
130
|
-
if (!username
|
|
132
|
+
if (!username)
|
|
131
133
|
return;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
let account = getAccount(username, false);
|
|
135
|
+
if (account && !updateExisting)
|
|
136
|
+
return;
|
|
137
|
+
account = (0, misc_1.setHidden)(account || {}, { username }); // hidden so that stringification won't include it
|
|
138
|
+
Object.assign(account, lodash_1.default.pickBy(props, Boolean));
|
|
139
|
+
exports.accountsConfig.set(accounts => Object.assign(accounts, { [username]: account }));
|
|
140
|
+
await updateAccount(account, account);
|
|
141
|
+
return account;
|
|
136
142
|
}
|
|
137
143
|
exports.addAccount = addAccount;
|
|
138
144
|
function delAccount(username) {
|
package/src/roots.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.rootsMiddleware = exports.roots = void 0;
|
|
7
4
|
const config_1 = require("./config");
|
|
8
5
|
const misc_1 = require("./misc");
|
|
9
6
|
const connections_1 = require("./connections");
|
|
10
|
-
const
|
|
7
|
+
const listen_1 = require("./listen");
|
|
11
8
|
exports.roots = (0, config_1.defineConfig)(misc_1.CFG.roots, {}, map => {
|
|
12
|
-
if (lodash_1.default.isArray(map)) { // legacy pre 0.51.0-alpha5, remove in 0.52
|
|
13
|
-
exports.roots.set(Object.fromEntries(map.map(x => [x.host, x.root])));
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
9
|
const list = Object.keys(map);
|
|
17
10
|
const matchers = list.map(hostMask => (0, misc_1.makeMatcher)(hostMask));
|
|
18
11
|
const values = Object.values(map);
|
|
19
12
|
return (host) => values[matchers.findIndex(m => m(host))];
|
|
20
13
|
});
|
|
21
|
-
const
|
|
14
|
+
const forceAddress = (0, config_1.defineConfig)(misc_1.CFG.force_address, false);
|
|
15
|
+
forceAddress.sub((v, { version }) => {
|
|
16
|
+
if (version === null || version === void 0 ? void 0 : version.olderThan('0.53.0'))
|
|
17
|
+
forceAddress.set((0, config_1.getConfig)('force_base_url') || (0, config_1.getConfig)('roots_mandatory') || false);
|
|
18
|
+
});
|
|
22
19
|
const rootsMiddleware = (ctx, next) => (() => {
|
|
20
|
+
var _a;
|
|
21
|
+
const root = (_a = exports.roots.compiled()) === null || _a === void 0 ? void 0 : _a(ctx.host);
|
|
22
|
+
if (!ctx.state.skipFilters && forceAddress.get())
|
|
23
|
+
if (root === undefined && !(0, misc_1.isLocalHost)(ctx) && ctx.host !== listen_1.baseUrl.compiled()) {
|
|
24
|
+
(0, connections_1.disconnect)(ctx, forceAddress.key());
|
|
25
|
+
return true; // true will avoid calling next
|
|
26
|
+
}
|
|
27
|
+
if (!root || root === '/')
|
|
28
|
+
return; // not transformation is required
|
|
23
29
|
let params; // undefined if we are not going to work on api parameters
|
|
24
30
|
if (ctx.path.startsWith(misc_1.SPECIAL_URI)) { // special uris should be excluded...
|
|
25
31
|
if (!ctx.path.startsWith(misc_1.API_URI))
|
|
@@ -30,20 +36,6 @@ const rootsMiddleware = (ctx, next) => (() => {
|
|
|
30
36
|
return; // exclude apis for admin-panel
|
|
31
37
|
params = ctx.state.params || ctx.query; // for api we'll translate params
|
|
32
38
|
}
|
|
33
|
-
if (lodash_1.default.isEmpty(exports.roots.get()))
|
|
34
|
-
return;
|
|
35
|
-
const host2root = exports.roots.compiled();
|
|
36
|
-
if (!host2root)
|
|
37
|
-
return;
|
|
38
|
-
const root = host2root(ctx.host);
|
|
39
|
-
if (root === '' || root === '/')
|
|
40
|
-
return;
|
|
41
|
-
if (root === undefined) {
|
|
42
|
-
if (ctx.state.skipFilters || !rootsMandatory.get() || (0, misc_1.isLocalHost)(ctx))
|
|
43
|
-
return;
|
|
44
|
-
(0, connections_1.disconnect)(ctx, 'bad-domain');
|
|
45
|
-
return true; // true will avoid calling next
|
|
46
|
-
}
|
|
47
39
|
if (!params) {
|
|
48
40
|
ctx.path = join(root, ctx.path);
|
|
49
41
|
return;
|
|
@@ -86,19 +86,22 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
|
|
|
86
86
|
if (node.default && path.endsWith('/') && !get) // final/ needed on browser to make resource urls correctly with html pages
|
|
87
87
|
node = (_a = await (0, vfs_1.urlToNode)(node.default, ctx, node)) !== null && _a !== void 0 ? _a : node;
|
|
88
88
|
if (!await (0, vfs_1.nodeIsDirectory)(node))
|
|
89
|
-
return
|
|
90
|
-
: !
|
|
91
|
-
:
|
|
92
|
-
:
|
|
93
|
-
:
|
|
89
|
+
return node.url ? ctx.redirect(node.url)
|
|
90
|
+
: !node.source ? (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_METHOD_NOT_ALLOWED) // !dir && !source is not supported at this moment
|
|
91
|
+
: !(0, vfs_1.statusCodeForMissingPerm)(node, 'can_read', ctx) ? (0, serveFile_1.serveFileNode)(ctx, node) // all good
|
|
92
|
+
: ctx.status !== cross_const_1.HTTP_UNAUTHORIZED ? null // all errors don't need extra handling, except unauthorized
|
|
93
|
+
: path.endsWith('/') ? (ctx.state.serveApp = true) && serveFrontendFiles(ctx, next) // since this is no dir, final / means we are dealing with default file, for which we still provide fancy login
|
|
94
|
+
: (ctx.set('WWW-Authenticate', 'Basic'), (0, errorPages_1.sendErrorPage)(ctx)); // this is necessary to support standard urls with credentials
|
|
94
95
|
if (!path.endsWith('/'))
|
|
95
96
|
return ctx.redirect(ctx.state.revProxyPath + ctx.originalUrl.replace(/(\?|$)/, '/$1')); // keep query-string, if any
|
|
96
97
|
if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_list', ctx)) {
|
|
97
98
|
if (ctx.status === cross_const_1.HTTP_FORBIDDEN)
|
|
98
99
|
return (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_FORBIDDEN);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// detect if we are dealing with a download-manager, as it may need basic authentication, while we don't want it on browsers
|
|
101
|
+
const { authenticate } = ctx.query;
|
|
102
|
+
const downloadManagerDetected = /DAP|FDM|[Mm]anager/.test(ctx.get('user-agent'));
|
|
103
|
+
if (downloadManagerDetected || authenticate)
|
|
104
|
+
return ctx.set('WWW-Authenticate', authenticate || 'Basic'); // we support basic authentication
|
|
102
105
|
ctx.state.serveApp = true;
|
|
103
106
|
return serveFrontendFiles(ctx, next);
|
|
104
107
|
}
|
package/src/serveGuiFiles.js
CHANGED
|
@@ -63,11 +63,8 @@ function serveStatic(uri) {
|
|
|
63
63
|
return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
|
|
64
64
|
const serveApp = shouldServeApp(ctx);
|
|
65
65
|
const fullPath = (0, path_1.join)(__dirname, '..', DEV_STATIC, folder, serveApp ? '/index.html' : ctx.path);
|
|
66
|
-
const content = await (0, misc_1.
|
|
67
|
-
|
|
68
|
-
return serveApp || !data ? data
|
|
69
|
-
: adjustBundlerLinks(ctx, uri, data);
|
|
70
|
-
});
|
|
66
|
+
const content = await (0, misc_1.parseFile)(fullPath, raw => serveApp || !raw.length ? raw : adjustBundlerLinks(ctx, uri, raw))
|
|
67
|
+
.catch(() => null);
|
|
71
68
|
if (content === null)
|
|
72
69
|
return ctx.status = const_1.HTTP_NOT_FOUND;
|
|
73
70
|
if (!serveApp)
|
package/src/throttler.js
CHANGED
|
@@ -18,21 +18,26 @@ const ip2group = {};
|
|
|
18
18
|
const SymThrStr = Symbol('stream');
|
|
19
19
|
const SymTimeout = Symbol('timeout');
|
|
20
20
|
const maxKbpsPerIp = (0, config_1.defineConfig)('max_kbps_per_ip', Infinity);
|
|
21
|
+
maxKbpsPerIp.sub(v => {
|
|
22
|
+
for (const [ip, { group }] of Object.entries(ip2group))
|
|
23
|
+
if (ip) // empty-string = unlimited group
|
|
24
|
+
group.updateLimit(v);
|
|
25
|
+
});
|
|
21
26
|
const throttler = async (ctx, next) => {
|
|
27
|
+
var _a;
|
|
22
28
|
await next();
|
|
23
29
|
let { body } = ctx;
|
|
30
|
+
const downloadTotal = ctx.response.length;
|
|
24
31
|
if (typeof body === 'string' || body && body instanceof Buffer)
|
|
25
32
|
ctx.body = body = stream_1.Readable.from(body);
|
|
26
33
|
if (!body || !(body instanceof stream_1.Readable))
|
|
27
34
|
return;
|
|
28
35
|
// we wrap the stream also for unlimited connections to get speed and other features
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return { group, count: 0, destroy: unsub };
|
|
35
|
-
});
|
|
36
|
+
const noLimit = ((_a = ctx.state.account) === null || _a === void 0 ? void 0 : _a.ignore_limits) || (0, misc_1.isLocalHost)(ctx);
|
|
37
|
+
const ipGroup = (0, misc_1.getOrSet)(ip2group, noLimit ? '' : ctx.ip, () => ({
|
|
38
|
+
count: 0,
|
|
39
|
+
group: new ThrottledStream_1.ThrottleGroup(noLimit ? Infinity : maxKbpsPerIp.get(), noLimit ? undefined : mainThrottleGroup),
|
|
40
|
+
}));
|
|
36
41
|
const conn = (0, connections_1.getConnection)(ctx);
|
|
37
42
|
if (!conn)
|
|
38
43
|
throw 'assert throttler connection';
|
|
@@ -57,19 +62,16 @@ const throttler = async (ctx, next) => {
|
|
|
57
62
|
});
|
|
58
63
|
++ipGroup.count;
|
|
59
64
|
ts.on('close', () => {
|
|
60
|
-
var _a;
|
|
61
65
|
update.flush();
|
|
62
66
|
closed = true;
|
|
63
67
|
if (--ipGroup.count)
|
|
64
68
|
return; // any left?
|
|
65
|
-
(_a = ipGroup.destroy) === null || _a === void 0 ? void 0 : _a.call(ipGroup);
|
|
66
69
|
delete ip2group[ctx.ip];
|
|
67
70
|
});
|
|
68
|
-
const downloadTotal = ctx.response.length;
|
|
69
71
|
ctx.state.originalStream = body;
|
|
70
72
|
ctx.body = body.pipe(ts);
|
|
71
|
-
if (downloadTotal) //
|
|
72
|
-
ctx.response.length = downloadTotal;
|
|
73
|
+
if (downloadTotal !== undefined) // undefined will break SSE
|
|
74
|
+
ctx.response.length = downloadTotal; // preserve this info
|
|
73
75
|
ts.once('close', () => // in case of compressed response, we offer calculation of real size
|
|
74
76
|
ctx.state.length = ts.getBytesSent() - offset);
|
|
75
77
|
};
|
package/src/upload.js
CHANGED
|
@@ -36,8 +36,14 @@ const cache = {};
|
|
|
36
36
|
function uploadWriter(base, path, ctx) {
|
|
37
37
|
if ((0, misc_1.dirTraversal)(path))
|
|
38
38
|
return fail(const_1.HTTP_FOOL);
|
|
39
|
-
if ((0, vfs_1.statusCodeForMissingPerm)(base, 'can_upload', ctx))
|
|
39
|
+
if ((0, vfs_1.statusCodeForMissingPerm)(base, 'can_upload', ctx)) {
|
|
40
|
+
if (!ctx.get('x-hfs-wait')) { // you can disable the following behavior
|
|
41
|
+
// avoid waiting hours for just an error
|
|
42
|
+
const t = setTimeout(() => (0, connections_1.disconnect)(ctx), 30000);
|
|
43
|
+
ctx.res.on('finish', () => clearTimeout(t));
|
|
44
|
+
}
|
|
40
45
|
return fail();
|
|
46
|
+
}
|
|
41
47
|
const fullPath = (0, path_1.join)(base.source, path);
|
|
42
48
|
const dir = (0, path_1.dirname)(fullPath);
|
|
43
49
|
const min = exports.minAvailableMb.get() * (1 << 20);
|