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.
Files changed (45) hide show
  1. package/admin/assets/index-ClcJhcGU.js +805 -0
  2. package/admin/assets/{sha512-a95yq7Nq.js → sha512-BBuEMQr3.js} +1 -1
  3. package/admin/index.html +2 -2
  4. package/frontend/assets/index-legacy-DBpnLaOV.js +60 -0
  5. package/frontend/assets/polyfills-legacy-DMrMt_pQ.js +4 -0
  6. package/frontend/assets/sha512-legacy-C1fsQ1Io.js +9 -0
  7. package/frontend/index.html +3 -2
  8. package/package.json +1 -1
  9. package/plugins/antibrute/plugin.js +35 -23
  10. package/src/QuickZipStream.js +1 -1
  11. package/src/SendList.js +1 -3
  12. package/src/adminApis.js +1 -1
  13. package/src/api.accounts.js +2 -2
  14. package/src/api.auth.js +7 -2
  15. package/src/api.get_file_list.js +1 -1
  16. package/src/api.log.js +2 -6
  17. package/src/api.plugins.js +10 -14
  18. package/src/api.vfs.js +2 -2
  19. package/src/auth.js +14 -5
  20. package/src/block.js +7 -7
  21. package/src/comments.js +5 -1
  22. package/src/config.js +15 -21
  23. package/src/const.js +2 -2
  24. package/src/cross.js +6 -8
  25. package/src/ddns.js +19 -18
  26. package/src/events.js +56 -5
  27. package/src/geo.js +1 -1
  28. package/src/index.js +1 -1
  29. package/src/langs/hfs-lang-it.json +7 -3
  30. package/src/listen.js +3 -3
  31. package/src/log.js +4 -0
  32. package/src/middlewares.js +8 -5
  33. package/src/misc.js +2 -15
  34. package/src/nat.js +4 -2
  35. package/src/perm.js +17 -11
  36. package/src/roots.js +15 -23
  37. package/src/serveGuiAndSharedFiles.js +11 -8
  38. package/src/serveGuiFiles.js +2 -5
  39. package/src/throttler.js +14 -12
  40. package/src/upload.js +7 -1
  41. package/admin/assets/index-LkaBe_Fp.js +0 -811
  42. package/frontend/assets/index-c8LVGHJd.js +0 -101
  43. package/frontend/assets/index-oekM6dN8.css +0 -1
  44. package/frontend/assets/sha512-PGV0A--c.js +0 -8
  45. /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', 'roots_mandatory']);
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
- if (!src)
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 ? '-' : (x instanceof Date ? x : new Date(x)).toLocaleString();
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 typedEntries(symbols))
411
- s = s.replace(open + k + close, lodash_1.default.isFunction(v) ? v() : v);
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 url = (0, cross_1.replace)(templateUrl, {
33
- IPX: ips[0] || '',
34
- IP4: lodash_1.default.find(ips, node_net_1.isIPv4) || '',
35
- IP6: lodash_1.default.find(ips, net_1.isIPv6) || '',
36
- }, '$');
37
- 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
38
- .then(async (res) => {
39
- const str = String(res.body).trim();
40
- return (re ? str.match(re) : res.ok) ? '' : (str || res.statusMessage);
41
- }, (err) => err.code || err.message || String(err));
42
- last = { ts: new Date().toJSON(), error, url };
43
- events_1.default.emit('dynamicDnsError', last);
44
- console.log('dynamic dns update', error || 'ok');
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
- yield last;
50
- await (0, stream_1.once)(events_1.default, 'dynamicDnsError');
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
- const events_1 = __importDefault(require("events"));
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 events_1.default().setMaxListeners(100);
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": 1.9,
4
- "hfs_version": "0.52.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)()).map(([name, nets]) => nets && !ignore.test(name) && nets.map(net => !net.internal && net.address)).flat());
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(() => {
@@ -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.invalidSessions.delete(ctx.session.username))
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.onOff = exports.pattern2filter = exports.onFirstEvent = exports.onProcessExit = void 0;
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
- return `${this.proto}://${this.publicIps[0] || this.externalIp || this.localIp}${!this.port || this.port === defPort ? '' : ':' + this.port}`;
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
- if (!acc)
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 || getAccount(username, false))
132
+ if (!username)
131
133
  return;
132
- const copy = (0, misc_1.setHidden)(lodash_1.default.pickBy(props, Boolean), { username }); // have the field in the object but hidden so that stringification won't include it
133
- exports.accountsConfig.set(accounts => Object.assign(accounts, { [username]: copy }));
134
- await updateAccount(copy, copy);
135
- return copy;
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 lodash_1 = __importDefault(require("lodash"));
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 rootsMandatory = (0, config_1.defineConfig)(misc_1.CFG.roots_mandatory, false);
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 !node.source ? (0, errorPages_1.sendErrorPage)(ctx, cross_const_1.HTTP_METHOD_NOT_ALLOWED) // !dir && !source is not supported at this moment
90
- : !(0, vfs_1.statusCodeForMissingPerm)(node, 'can_read', ctx) ? (0, serveFile_1.serveFileNode)(ctx, node) // all good
91
- : ctx.status !== cross_const_1.HTTP_UNAUTHORIZED ? null // all errors don't need extra handling, except unauthorized
92
- : 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
93
- : (ctx.set('WWW-Authenticate', 'Basic'), (0, errorPages_1.sendErrorPage)(ctx)); // this is necessary to support standard urls with credentials
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
- const browserDetected = ctx.get('Upgrade-Insecure-Requests') || ctx.get('Sec-Fetch-Mode'); // ugh, heuristics
100
- if (!browserDetected) // we don't want to trigger basic authentication on browsers, it's meant for download managers only
101
- return ctx.set('WWW-Authenticate', 'Basic'); // we support basic authentication
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
  }
@@ -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.getOrSet)(cache, ctx.path, async () => {
67
- const data = await promises_1.default.readFile(fullPath).catch(() => null);
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 ipGroup = (0, misc_1.getOrSet)(ip2group, ctx.ip, () => {
30
- var _a;
31
- const doLimit = ((_a = ctx.state.account) === null || _a === void 0 ? void 0 : _a.ignore_limits) || (0, misc_1.isLocalHost)(ctx) ? undefined : true;
32
- const group = new ThrottledStream_1.ThrottleGroup(Infinity, doLimit && mainThrottleGroup);
33
- const unsub = doLimit && maxKbpsPerIp.sub(v => group.updateLimit(v));
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) // preserve this info
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);