hfs 0.52.5 → 0.53.0-alpha2

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 (44) hide show
  1. package/admin/assets/index-Dsv2tjnU.js +805 -0
  2. package/admin/assets/{sha512-7RFRcaOB.js → sha512-BjLhiBem.js} +1 -1
  3. package/admin/index.html +2 -2
  4. package/frontend/assets/index-legacy-C21Ot_ue.js +60 -0
  5. package/frontend/assets/polyfills-legacy-DMrMt_pQ.js +4 -0
  6. package/frontend/assets/sha512-legacy-DYmEZJfO.js +9 -0
  7. package/frontend/index.html +3 -2
  8. package/package.json +1 -1
  9. package/plugins/antibrute/plugin.js +34 -23
  10. package/src/QuickZipStream.js +1 -1
  11. package/src/SendList.js +1 -3
  12. package/src/api.accounts.js +2 -2
  13. package/src/api.auth.js +7 -2
  14. package/src/api.get_file_list.js +1 -1
  15. package/src/api.log.js +2 -6
  16. package/src/api.plugins.js +22 -23
  17. package/src/api.vfs.js +1 -1
  18. package/src/auth.js +14 -5
  19. package/src/comments.js +5 -1
  20. package/src/config.js +15 -21
  21. package/src/const.js +2 -2
  22. package/src/cross.js +6 -8
  23. package/src/ddns.js +19 -18
  24. package/src/events.js +56 -5
  25. package/src/geo.js +1 -1
  26. package/src/github.js +21 -5
  27. package/src/index.js +1 -1
  28. package/src/langs/hfs-lang-it.json +7 -3
  29. package/src/listen.js +1 -1
  30. package/src/log.js +4 -0
  31. package/src/middlewares.js +8 -5
  32. package/src/misc.js +2 -15
  33. package/src/perm.js +15 -10
  34. package/src/plugins.js +5 -5
  35. package/src/roots.js +15 -23
  36. package/src/serveGuiAndSharedFiles.js +11 -8
  37. package/src/serveGuiFiles.js +9 -15
  38. package/src/throttler.js +11 -9
  39. package/src/upload.js +7 -1
  40. package/admin/assets/index-V2liwvHg.js +0 -811
  41. package/frontend/assets/index-c8LVGHJd.js +0 -101
  42. package/frontend/assets/index-oekM6dN8.css +0 -1
  43. package/frontend/assets/sha512-PGV0A--c.js +0 -8
  44. /package/admin/assets/{index-sCDewjON.css → index-CwIN7CM4.css} +0 -0
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/github.js CHANGED
@@ -21,7 +21,7 @@ function downloadProgress(id, status) {
21
21
  delete downloading[id];
22
22
  else
23
23
  downloading[id] = status;
24
- events_1.default.emit('pluginDownload_' + id, status);
24
+ events_1.default.emit('pluginDownload', { id, status });
25
25
  }
26
26
  // determine default branch, possibly without consuming api quota
27
27
  async function getGithubDefaultBranch(repo) {
@@ -161,13 +161,29 @@ async function apiGithub(uri) {
161
161
  : e;
162
162
  });
163
163
  }
164
- async function searchPlugins(text = '') {
164
+ async function* apiGithubPaginated(uri) {
165
+ const PAGE_SIZE = 100;
166
+ let page = 1;
167
+ let n = 0;
168
+ while (1) {
169
+ const res = await apiGithub(uri + `&page=${page++}&per_page=${PAGE_SIZE}`);
170
+ for (const x of res.items)
171
+ yield x;
172
+ const now = res.items.length;
173
+ n += now;
174
+ if (!now || n >= res.total_count)
175
+ break;
176
+ }
177
+ }
178
+ async function searchPlugins(text = '', { skipRepos = [''] } = {}) {
165
179
  const projectInfo = await (0, exports.getProjectInfo)();
166
- const list = await apiGithub('search/repositories?q=topic:hfs-plugin+' + encodeURI(text));
167
- return new misc_1.AsapStream(list.items.map(async (it) => {
180
+ const list = await Promise.all(['', 'user:'].map(specifier => // search text with multiple specifiers
181
+ (0, misc_1.asyncGeneratorToArray)(apiGithubPaginated(`search/repositories?q=topic:hfs-plugin+${specifier}${encodeURI(text)}`))))
182
+ .then(all => all.flat()); // make it a single array
183
+ return new misc_1.AsapStream(list.map(async (it) => {
168
184
  var _a, _b;
169
185
  const repo = it.full_name;
170
- if ((_a = projectInfo === null || projectInfo === void 0 ? void 0 : projectInfo.plugins_blacklist) === null || _a === void 0 ? void 0 : _a.includes(repo))
186
+ if (((_a = projectInfo === null || projectInfo === void 0 ? void 0 : projectInfo.plugins_blacklist) === null || _a === void 0 ? void 0 : _a.includes(repo)) || skipRepos.includes(repo))
171
187
  return;
172
188
  const pl = await readOnlineCompatiblePlugin(repo, it.default_branch).catch(() => undefined);
173
189
  if (!pl)
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
@@ -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/perm.js CHANGED
@@ -46,10 +46,8 @@ createAdminConfig.sub(v => {
46
46
  setTimeout(() => createAdmin(v));
47
47
  });
48
48
  async function createAdmin(password, username = 'admin') {
49
- const acc = await addAccount(username, { admin: true, password });
50
- if (!acc)
51
- return console.log("cannot create, already exists");
52
- 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");
53
51
  }
54
52
  exports.createAdmin = createAdmin;
55
53
  const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
@@ -60,6 +58,9 @@ async function updateAccount(account, change) {
60
58
  await (change === null || change === void 0 ? void 0 : change(account));
61
59
  else
62
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)
63
64
  const { username, password } = account;
64
65
  if (password) {
65
66
  console.debug('hashing password for', username);
@@ -126,14 +127,18 @@ function renameAccount(from, to) {
126
127
  }
127
128
  }
128
129
  exports.renameAccount = renameAccount;
129
- async function addAccount(username, props) {
130
+ async function addAccount(username, props, updateExisting = false) {
130
131
  username = normalizeUsername(username);
131
- if (!username || getAccount(username, false))
132
+ if (!username)
132
133
  return;
133
- 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
134
- exports.accountsConfig.set(accounts => Object.assign(accounts, { [username]: copy }));
135
- await updateAccount(copy, copy);
136
- 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;
137
142
  }
138
143
  exports.addAccount = addAccount;
139
144
  function delAccount(username) {
package/src/plugins.js CHANGED
@@ -27,7 +27,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
27
27
  return (mod && mod.__esModule) ? mod : { "default": mod };
28
28
  };
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.getMissingDependencies = exports.parsePluginSource = exports.rescan = exports.pluginsConfig = exports.enablePlugins = exports.pluginsWatcher = exports.getAvailablePlugins = exports.mapPlugins = exports.Plugin = exports.pluginsMiddleware = exports.getPluginConfigFields = exports.findPluginByRepo = exports.getPluginInfo = exports.setPluginConfig = exports.startPlugin = exports.stopPlugin = exports.enablePlugin = exports.isPluginEnabled = exports.isPluginRunning = exports.STORAGE_FOLDER = exports.DISABLING_SUFFIX = exports.PATH = void 0;
30
+ exports.getMissingDependencies = exports.parsePluginSource = exports.rescan = exports.pluginsConfig = exports.enablePlugins = exports.pluginsWatcher = exports.getAvailablePlugins = exports.mapPlugins = exports.SERVER_CODE_ID = exports.Plugin = exports.pluginsMiddleware = exports.getPluginConfigFields = exports.findPluginByRepo = exports.getPluginInfo = exports.setPluginConfig = exports.startPlugin = exports.stopPlugin = exports.enablePlugin = exports.isPluginEnabled = exports.isPluginRunning = exports.STORAGE_FOLDER = exports.DISABLING_SUFFIX = exports.PATH = void 0;
31
31
  const fast_glob_1 = __importDefault(require("fast-glob"));
32
32
  const watchLoad_1 = require("./watchLoad");
33
33
  const lodash_1 = __importDefault(require("lodash"));
@@ -232,7 +232,7 @@ class Plugin {
232
232
  const { id } = this;
233
233
  try {
234
234
  await ((_b = (_a = this.data) === null || _a === void 0 ? void 0 : _a.unload) === null || _b === void 0 ? void 0 : _b.call(_a));
235
- if (!reloading && id !== SERVER_CODE_ID) // we already printed 'reloading'
235
+ if (!reloading && id !== exports.SERVER_CODE_ID) // we already printed 'reloading'
236
236
  console.log('unloaded plugin', id);
237
237
  }
238
238
  catch (e) {
@@ -244,12 +244,12 @@ class Plugin {
244
244
  }
245
245
  }
246
246
  exports.Plugin = Plugin;
247
- const SERVER_CODE_ID = '.';
247
+ exports.SERVER_CODE_ID = '.'; // a name that will surely be not found among plugin folders
248
248
  const serverCode = (0, config_1.defineConfig)('server_code', '', async (script, { k }) => {
249
249
  const res = {};
250
250
  try {
251
251
  new Function('exports', script)(res); // parse
252
- return new Plugin(SERVER_CODE_ID, '', await initPlugin(res), lodash_1.default.noop); // '.' is a name that will surely be not found among plugin folders
252
+ return new Plugin(exports.SERVER_CODE_ID, '', await initPlugin(res), lodash_1.default.noop);
253
253
  }
254
254
  catch (e) {
255
255
  return console.error(k + ':', e.message || String(e));
@@ -260,7 +260,7 @@ serverCode.sub(() => { var _a; return (_a = serverCode.compiled()) === null || _
260
260
  function mapPlugins(cb, includeServerCode = true) {
261
261
  const entries = Object.entries(plugins);
262
262
  return entries.map(([plName, pl]) => {
263
- if (!includeServerCode && plName === SERVER_CODE_ID)
263
+ if (!includeServerCode && plName === exports.SERVER_CODE_ID)
264
264
  return;
265
265
  try {
266
266
  return cb(pl, plName);
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)
@@ -94,7 +91,7 @@ async function treatIndex(ctx, filesUri, body) {
94
91
  ctx.set('etag', '');
95
92
  ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate');
96
93
  ctx.type = 'html';
97
- const isFrontend = filesUri === const_1.FRONTEND_URI;
94
+ const isFrontend = filesUri === const_1.FRONTEND_URI ? ' ' : ''; // as a string will allow neater code later
98
95
  const pub = ctx.state.revProxyPath + const_1.PLUGINS_PUB_URI;
99
96
  // expose plugins' configs that are declared with 'frontend' attribute
100
97
  const plugins = Object.fromEntries((0, misc_1.onlyTruthy)((0, plugins_1.mapPlugins)((pl, name) => {
@@ -117,7 +114,7 @@ async function treatIndex(ctx, filesUri, body) {
117
114
  const isOpen = !isClose;
118
115
  if (isHead && isOpen)
119
116
  return all + `
120
- ${!isFrontend ? '' : `
117
+ ${isFrontend && `
121
118
  <title>${adminApis_1.title.get()}</title>
122
119
  <link rel="shortcut icon" href="/favicon.ico?${timestamp}" />
123
120
  ` + (0, customHtml_1.getSection)('htmlHead')}
@@ -128,6 +125,7 @@ async function treatIndex(ctx, filesUri, body) {
128
125
  SPECIAL_URI: const_1.SPECIAL_URI, PLUGINS_PUB_URI: const_1.PLUGINS_PUB_URI, FRONTEND_URI: const_1.FRONTEND_URI,
129
126
  session: session instanceof apiMiddleware_1.ApiError ? null : session,
130
127
  plugins,
128
+ loadScripts: Object.fromEntries((0, plugins_1.mapPlugins)((p, id) => { var _a; return [id, (_a = p.frontend_js) === null || _a === void 0 ? void 0 : _a.map(f => f.includes('//') ? f : pub + id + '/' + f)]; })),
131
129
  prefixUrl: ctx.state.revProxyPath,
132
130
  dontOverwriteUploading: upload_1.dontOverwriteUploading.get(),
133
131
  customHtml: lodash_1.default.omit(Object.fromEntries(customHtml_1.customHtmlState.sections), ['top', 'bottom']),
@@ -139,20 +137,16 @@ async function treatIndex(ctx, filesUri, body) {
139
137
  `;
140
138
  if (isBody && isOpen)
141
139
  return all + `
142
- ${!isFrontend ? '' : (0, customHtml_1.getSection)('top')}
140
+ ${isFrontend && (0, customHtml_1.getSection)('top')}
143
141
  <style>
144
142
  :root {
145
- ${lodash_1.default.map(plugins, (configs, pluginName) => lodash_1.default.map(configs, (v, k) => `--${pluginName}-${k}: ${serializeCss(v)};`).join('\n')).join('')}
143
+ ${lodash_1.default.map(plugins, (configs, pluginName) => // make plugin configs accessible via css
144
+ lodash_1.default.map(configs, (v, k) => `--${pluginName}-${k}: ${serializeCss(v)};`).join('\n')).join('')}
146
145
  }
147
146
  </style>
148
- ${!isFrontend ? '' : (0, plugins_1.mapPlugins)((plug, id) => {
147
+ ${isFrontend && (0, plugins_1.mapPlugins)((plug, id) => {
149
148
  var _a;
150
149
  return (_a = plug.frontend_css) === null || _a === void 0 ? void 0 : _a.map(f => `<link rel='stylesheet' type='text/css' href='${f.includes('//') ? f : pub + id + '/' + f}' plugin=${JSON.stringify(id)}/>`);
151
- })
152
- .flat().filter(Boolean).join('\n')}
153
- ${!isFrontend ? '' : (0, plugins_1.mapPlugins)((plug, id) => {
154
- var _a;
155
- return (_a = plug.frontend_js) === null || _a === void 0 ? void 0 : _a.map(f => `<script defer plugin=${JSON.stringify(id)} src='${f.includes('//') ? f : pub + id + '/' + f}'></script>`);
156
150
  })
157
151
  .flat().filter(Boolean).join('\n')}
158
152
  `;
package/src/throttler.js CHANGED
@@ -18,7 +18,13 @@ 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;
24
30
  const downloadTotal = ctx.response.length;
@@ -27,13 +33,11 @@ const throttler = async (ctx, next) => {
27
33
  if (!body || !(body instanceof stream_1.Readable))
28
34
  return;
29
35
  // we wrap the stream also for unlimited connections to get speed and other features
30
- const ipGroup = (0, misc_1.getOrSet)(ip2group, ctx.ip, () => {
31
- var _a;
32
- const doLimit = ((_a = ctx.state.account) === null || _a === void 0 ? void 0 : _a.ignore_limits) || (0, misc_1.isLocalHost)(ctx) ? undefined : true;
33
- const group = new ThrottledStream_1.ThrottleGroup(Infinity, doLimit && mainThrottleGroup);
34
- const unsub = doLimit && maxKbpsPerIp.sub(v => group.updateLimit(v));
35
- return { group, count: 0, destroy: unsub };
36
- });
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
+ }));
37
41
  const conn = (0, connections_1.getConnection)(ctx);
38
42
  if (!conn)
39
43
  throw 'assert throttler connection';
@@ -58,12 +62,10 @@ const throttler = async (ctx, next) => {
58
62
  });
59
63
  ++ipGroup.count;
60
64
  ts.on('close', () => {
61
- var _a;
62
65
  update.flush();
63
66
  closed = true;
64
67
  if (--ipGroup.count)
65
68
  return; // any left?
66
- (_a = ipGroup.destroy) === null || _a === void 0 ? void 0 : _a.call(ipGroup);
67
69
  delete ip2group[ctx.ip];
68
70
  });
69
71
  ctx.state.originalStream = body;