hfs 3.3.4 → 3.4.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 (150) hide show
  1. package/admin/assets/AccountsPage-CoiPofEv.js +4 -0
  2. package/admin/assets/ConfigFilePage-jUH2feqi.js +4 -0
  3. package/admin/assets/CustomHtmlPage-Pl_AJZrl.js +2 -0
  4. package/admin/assets/InternetPage-DIcAwhFk.js +6 -0
  5. package/admin/assets/LangPage-BwVY8QRD.js +2 -0
  6. package/admin/assets/LogsPage-CcjS12cP.js +3 -0
  7. package/admin/assets/MonitorPage-DMVFXyMe.js +2 -0
  8. package/admin/assets/OptionsPage-BB40ax9G.js +3 -0
  9. package/admin/assets/PluginsPage-CC2dINcb.js +1 -0
  10. package/admin/assets/icons-C_ux1zLA.js +1 -0
  11. package/admin/assets/index-BSSQOMG_.js +755 -0
  12. package/admin/assets/index-mI9kZlpb.css +1 -0
  13. package/admin/assets/routing-Be72yFmd.js +1 -0
  14. package/admin/index.html +2 -2
  15. package/central.json +100 -1
  16. package/frontend/assets/{dist-legacy-CnYHQXZ6.js → dist-legacy-D7Dg5w3L.js} +1 -1
  17. package/frontend/assets/index-legacy-CnazC3Dj.js +1 -0
  18. package/frontend/assets/{lib-legacy-3xYueNHt.js → lib-legacy-BxFHpdER.js} +1 -1
  19. package/frontend/assets/{sha512-legacy-Cl3f_PhN.js → sha512-legacy-C8xoQBnP.js} +1 -1
  20. package/frontend/index.html +1 -1
  21. package/npm-shrinkwrap.json +9 -9
  22. package/package.json +2 -2
  23. package/plugins/antibrute/plugin.js +62 -15
  24. package/src/acme.js +143 -50
  25. package/src/acmeConfig.js +43 -0
  26. package/src/acmeDns.js +285 -0
  27. package/src/admin-langs/embedded.js +33 -0
  28. package/src/admin-langs/hfs-admin-lang-ar.json +1149 -0
  29. package/src/admin-langs/hfs-admin-lang-bg.json +1149 -0
  30. package/src/admin-langs/hfs-admin-lang-de.json +1149 -0
  31. package/src/admin-langs/hfs-admin-lang-el.json +1149 -0
  32. package/src/admin-langs/hfs-admin-lang-en.json +1149 -0
  33. package/src/admin-langs/hfs-admin-lang-es.json +1149 -0
  34. package/src/admin-langs/hfs-admin-lang-fi.json +1149 -0
  35. package/src/admin-langs/hfs-admin-lang-fr.json +1149 -0
  36. package/src/admin-langs/hfs-admin-lang-hu.json +1149 -0
  37. package/src/admin-langs/hfs-admin-lang-it.json +1149 -0
  38. package/src/admin-langs/hfs-admin-lang-ja.json +1149 -0
  39. package/src/admin-langs/hfs-admin-lang-ko.json +1149 -0
  40. package/src/admin-langs/hfs-admin-lang-lt.json +1149 -0
  41. package/src/admin-langs/hfs-admin-lang-ms.json +1149 -0
  42. package/src/admin-langs/hfs-admin-lang-nl.json +1149 -0
  43. package/src/admin-langs/hfs-admin-lang-pl.json +1149 -0
  44. package/src/admin-langs/hfs-admin-lang-pt-br.json +1149 -0
  45. package/src/admin-langs/hfs-admin-lang-ro.json +1149 -0
  46. package/src/admin-langs/hfs-admin-lang-ru.json +1149 -0
  47. package/src/admin-langs/hfs-admin-lang-sr-latn.json +1149 -0
  48. package/src/admin-langs/hfs-admin-lang-sr.json +1149 -0
  49. package/src/admin-langs/hfs-admin-lang-th.json +1149 -0
  50. package/src/admin-langs/hfs-admin-lang-tr.json +1149 -0
  51. package/src/admin-langs/hfs-admin-lang-uk.json +1149 -0
  52. package/src/admin-langs/hfs-admin-lang-vi.json +1149 -0
  53. package/src/admin-langs/hfs-admin-lang-zh-hant.json +1149 -0
  54. package/src/admin-langs/hfs-admin-lang-zh.json +1149 -0
  55. package/src/adminApis.js +2 -2
  56. package/src/adminLang.js +51 -0
  57. package/src/api.auth.js +1 -1
  58. package/src/api.lang.js +22 -13
  59. package/src/api.log.js +5 -3
  60. package/src/api.net.js +20 -3
  61. package/src/api.plugins.js +53 -19
  62. package/src/api.vfs.js +3 -2
  63. package/src/argv.js +4 -0
  64. package/src/auth.js +13 -9
  65. package/src/basicWeb.js +6 -2
  66. package/src/block.js +1 -1
  67. package/src/config.js +1 -0
  68. package/src/connections.js +4 -7
  69. package/src/consoleLog.js +4 -0
  70. package/src/const.js +1 -1
  71. package/src/cross-const.js +8 -6
  72. package/src/cross.js +16 -2
  73. package/src/customHtml.js +3 -2
  74. package/src/frontEndApis.js +44 -7
  75. package/src/github.js +6 -37
  76. package/src/i18n.js +16 -4
  77. package/src/index.js +4 -3
  78. package/src/lang.js +21 -9
  79. package/src/langs/embedded.js +3 -2
  80. package/src/langs/hfs-lang-ar.json +7 -2
  81. package/src/langs/hfs-lang-bg.json +7 -2
  82. package/src/langs/hfs-lang-de.json +7 -2
  83. package/src/langs/hfs-lang-el.json +7 -2
  84. package/src/langs/hfs-lang-en.json +189 -0
  85. package/src/langs/hfs-lang-es.json +9 -4
  86. package/src/langs/hfs-lang-fi.json +8 -3
  87. package/src/langs/hfs-lang-fr.json +12 -7
  88. package/src/langs/hfs-lang-hu.json +7 -2
  89. package/src/langs/hfs-lang-it.json +11 -5
  90. package/src/langs/hfs-lang-ja.json +7 -2
  91. package/src/langs/hfs-lang-ko.json +7 -2
  92. package/src/langs/hfs-lang-lt.json +7 -2
  93. package/src/langs/hfs-lang-ms.json +9 -2
  94. package/src/langs/hfs-lang-nl.json +7 -2
  95. package/src/langs/hfs-lang-pl.json +7 -2
  96. package/src/langs/hfs-lang-pt-br.json +9 -4
  97. package/src/langs/hfs-lang-ro.json +7 -2
  98. package/src/langs/hfs-lang-ru.json +9 -4
  99. package/src/langs/hfs-lang-sr-latn.json +7 -2
  100. package/src/langs/hfs-lang-sr.json +7 -2
  101. package/src/langs/hfs-lang-th.json +8 -3
  102. package/src/langs/hfs-lang-tr.json +8 -3
  103. package/src/langs/hfs-lang-uk.json +16 -11
  104. package/src/langs/hfs-lang-vi.json +7 -2
  105. package/src/langs/hfs-lang-zh-hant.json +189 -0
  106. package/src/langs/hfs-lang-zh.json +97 -95
  107. package/src/listen.js +6 -3
  108. package/src/log.js +7 -1
  109. package/src/middlewares.js +87 -31
  110. package/src/multipartUpload.js +21 -0
  111. package/src/perm.js +38 -20
  112. package/src/pluginCatalog.js +66 -0
  113. package/src/pluginUsage.js +64 -0
  114. package/src/plugins.js +14 -2
  115. package/src/roots.js +22 -13
  116. package/src/serveFile.js +28 -11
  117. package/src/serveGuiAndSharedFiles.js +6 -6
  118. package/src/serveGuiFiles.js +7 -4
  119. package/src/update.js +51 -18
  120. package/src/upload.js +49 -8
  121. package/src/util-http.js +31 -5
  122. package/src/vfs.js +93 -4
  123. package/src/webdav.js +14 -8
  124. package/admin/assets/AccountsPage-CnZGcUox.js +0 -6
  125. package/admin/assets/ArrayField-F7fpCJIv.js +0 -2
  126. package/admin/assets/ConfigFilePage-CEgZ8Wq4.js +0 -4
  127. package/admin/assets/CustomHtmlPage-DDUNxF1H.js +0 -2
  128. package/admin/assets/DataTable-D_TeYxvk.js +0 -119
  129. package/admin/assets/DateTimeField-CsIdkTqz.js +0 -1
  130. package/admin/assets/EventManager--HiXIAY0.js +0 -1
  131. package/admin/assets/FileField-HsDCdr5S.js +0 -117
  132. package/admin/assets/InternetPage-DDwKrXJH.js +0 -8
  133. package/admin/assets/LangPage-Dd8hi_9K.js +0 -2
  134. package/admin/assets/LogsPage-BcPfugNY.js +0 -4
  135. package/admin/assets/MenuButton-cuehhVig.js +0 -1
  136. package/admin/assets/MonitorPage-Bre9d6MA.js +0 -2
  137. package/admin/assets/OptionsPage-Dd_cRDf4.js +0 -3
  138. package/admin/assets/PluginsPage-DJiseD4G.js +0 -1
  139. package/admin/assets/Tabs-C87E85Fh.js +0 -1
  140. package/admin/assets/TextEditor-BeS-mR2I.css +0 -1
  141. package/admin/assets/TextEditor-Cr0VI9Xi.js +0 -34
  142. package/admin/assets/VfsPathField-D1BkfhAM.js +0 -1
  143. package/admin/assets/agentIcons-CDoUTPUC.js +0 -1
  144. package/admin/assets/baseUrl-CnrV7Huu.js +0 -2
  145. package/admin/assets/icons-DJJYoK9E.js +0 -1
  146. package/admin/assets/index-11n5z0Ez.css +0 -1
  147. package/admin/assets/index-DwXlUHAs.js +0 -477
  148. package/admin/assets/routing-BQpoUdjq.js +0 -1
  149. package/frontend/assets/index-legacy-JYULclbY.js +0 -1
  150. package/src/langs/hfs-lang-zh-tw.json +0 -183
package/src/perm.js CHANGED
@@ -33,17 +33,7 @@ const auth_1 = require("./auth");
33
33
  const sessionStamp_1 = require("./sessionStamp");
34
34
  // provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
35
35
  function expandUsername(who) {
36
- const ret = new Set();
37
- const q = [who];
38
- for (const u of q) {
39
- const a = getAccount(u);
40
- if (!a || a.disabled)
41
- continue;
42
- ret.add(u);
43
- if (a.belongs)
44
- q.push(...a.belongs);
45
- }
46
- return ret;
36
+ return new Set(Array.from(walkActiveAccounts([who]), a => a.username));
47
37
  }
48
38
  // check if current username or any ancestor match the provided usernames
49
39
  function ctxBelongsTo(ctx, usernames) {
@@ -235,14 +225,29 @@ function delAccount(username) {
235
225
  }
236
226
  // get some property from account, searching in its groups if necessary. Search is breadth-first, and this determines priority of inheritance.
237
227
  function getFromAccount(account, getter) {
238
- const search = [account];
228
+ const root = typeof account === 'string' ? getAccount(account) : account;
229
+ if (!root)
230
+ return;
231
+ let res = getter(root); // always consider direct properties; only inherited groups are filtered by active status
232
+ if (res !== undefined)
233
+ return res;
234
+ for (const a of walkActiveAccounts(root.belongs || [])) {
235
+ res = getter(a);
236
+ if (res !== undefined)
237
+ return res;
238
+ }
239
+ }
240
+ function* walkActiveAccounts(roots) {
241
+ const search = [...roots];
242
+ const visited = new Set();
239
243
  for (const accountOrUsername of search) {
240
244
  const a = typeof accountOrUsername === 'string' ? getAccount(accountOrUsername) : accountOrUsername;
241
- if (!a)
245
+ if (!a || visited.has(a.username))
242
246
  continue;
243
- const res = getter(a);
244
- if (res !== undefined)
245
- return res;
247
+ visited.add(a.username);
248
+ if (accountIsDisabled(a))
249
+ continue;
250
+ yield a;
246
251
  if (a.belongs)
247
252
  search.push(...a.belongs);
248
253
  }
@@ -257,10 +262,23 @@ function accountCanLogin(account) {
257
262
  return accountHasLoginMethod(account) && !accountIsDisabled(account);
258
263
  }
259
264
  function accountIsDisabled(account) {
260
- return Boolean(account.disabled
261
- || account.expire < Date.now()
262
- || account.belongs?.length // don't every() on empty array, as it returns true
263
- && account.belongs.map(u => getAccount(u, false)).every(a => a && accountIsDisabled(a)));
265
+ return check(account, new Set());
266
+ function check(account, visiting) {
267
+ if (account.disabled || account.expire < Date.now())
268
+ return true;
269
+ if (!account.belongs?.length)
270
+ return false;
271
+ // consider invalid membership cycles disabled so they cannot grant privileges
272
+ if (visiting.has(account.username))
273
+ return true;
274
+ visiting.add(account.username);
275
+ const ret = account.belongs.every(u => {
276
+ const a = getAccount(u, false);
277
+ return Boolean(a && check(a, visiting));
278
+ });
279
+ visiting.delete(account.username);
280
+ return ret;
281
+ }
264
282
  }
265
283
  function accountCanLoginAdmin(account) {
266
284
  return accountCanLogin(account) && getFromAccount(account, a => a.admin) || false;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getPluginCatalog = getPluginCatalog;
7
+ exports.searchPluginCatalog = searchPluginCatalog;
8
+ const github_1 = require("./github");
9
+ const persistence_1 = require("./persistence");
10
+ const const_1 = require("./const");
11
+ const lodash_1 = __importDefault(require("lodash"));
12
+ const CACHE_KEY = 'pluginCatalog';
13
+ let refreshing;
14
+ async function* getPluginCatalog() {
15
+ await persistence_1.storedMap.ready();
16
+ const saved = await persistence_1.storedMap.get(CACHE_KEY);
17
+ // compatibility is determined by this HFS version, including fallback api branches
18
+ const previous = saved?.version === const_1.VERSION ? saved : undefined;
19
+ const age = previous ? Date.now() - previous.updatedAt : Infinity;
20
+ if (previous && age < 24 * 60 * 60_000)
21
+ yield { ...previous, refreshing: age >= 5 * 60_000 };
22
+ if (age < 5 * 60_000)
23
+ return;
24
+ // all searches share the same refresh, even if the initiating client disconnects
25
+ refreshing ??= refresh().finally(() => { refreshing = undefined; });
26
+ yield { ...await refreshing, refreshing: false };
27
+ async function refresh() {
28
+ const repositories = [];
29
+ for await (const repo of (0, github_1.apiGithubPaginated)('search/repositories?q=topic:hfs-plugin', { requireComplete: true }))
30
+ repositories.push(lodash_1.default.pick(repo, ['full_name', 'description', 'pushed_at', 'default_branch', 'stargazers_count', 'license']));
31
+ const old = new Map(previous?.entries.map(entry => [entry.repository.full_name, entry]));
32
+ const entries = await Promise.all(repositories.map(async (repository) => {
33
+ const cached = old.get(repository.full_name);
34
+ let plugin = cached?.plugin;
35
+ if (!cached || cached.repository.pushed_at !== repository.pushed_at
36
+ || cached.repository.default_branch !== repository.default_branch) {
37
+ plugin = await (0, github_1.readOnlineCompatiblePlugin)(repository.full_name, repository.default_branch)
38
+ .catch((error) => {
39
+ // repositories can carry the topic without providing a plugin at the expected path
40
+ if (error.message !== '404')
41
+ throw error;
42
+ }) ?? null;
43
+ }
44
+ return { repository, plugin: plugin ?? null };
45
+ }));
46
+ const next = { version: const_1.VERSION, updatedAt: Date.now(), entries };
47
+ // never replace a complete catalog with the result of a failed refresh
48
+ await persistence_1.storedMap.put(CACHE_KEY, next);
49
+ return next;
50
+ }
51
+ }
52
+ async function searchPluginCatalog(catalog, text = '', skipRepos = []) {
53
+ const central = await (0, github_1.getProjectInfo)();
54
+ const terms = text.toLowerCase().split(/\s+/).filter(Boolean);
55
+ return catalog.entries.flatMap(({ repository, plugin }) => {
56
+ const repo = repository.full_name;
57
+ const searchable = `${repo} ${repository.description || ''} ${plugin?.description || ''}`.toLowerCase();
58
+ if (!plugin || skipRepos.includes(repo) || central?.repo_blacklist?.[repo]?.message
59
+ || !terms.every(term => searchable.includes(term)))
60
+ return [];
61
+ // API responses add live state and dependency errors; keep the persisted metadata untouched
62
+ return [{ ...plugin, id: repo, repo, pushed_at: repository.pushed_at,
63
+ stargazers_count: repository.stargazers_count, default_branch: repository.default_branch,
64
+ license: repository.license?.spdx_id }];
65
+ });
66
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_crypto_1 = require("node:crypto");
4
+ const misc_1 = require("./misc");
5
+ const config_1 = require("./config");
6
+ const plugins_1 = require("./plugins");
7
+ const persistence_1 = require("./persistence");
8
+ const util_http_1 = require("./util-http");
9
+ const debounceAsync_1 = require("./debounceAsync");
10
+ const shareUsageStats = (0, config_1.defineConfig)(misc_1.CFG.share_usage_stats, false);
11
+ const installationId = persistence_1.storedMap.singleSync('pluginUsageId', '');
12
+ const lastReport = persistence_1.storedMap.singleSync('lastPluginUsageReport', 0);
13
+ const ready = Promise.all([config_1.configReady, plugins_1.pluginsScanned, installationId.ready(), lastReport.ready()]);
14
+ const syncPluginUsage = (0, debounceAsync_1.debounceAsync)(async () => {
15
+ await ready;
16
+ let uuid = installationId.get();
17
+ if (!shareUsageStats.get()) {
18
+ if (!uuid)
19
+ return;
20
+ await send('DELETE', { uuid });
21
+ installationId.set('');
22
+ lastReport.set(0);
23
+ return;
24
+ }
25
+ if (Date.now() < lastReport.get() + misc_1.DAY)
26
+ return;
27
+ if (!uuid) {
28
+ uuid = (0, node_crypto_1.randomUUID)();
29
+ installationId.set(uuid);
30
+ }
31
+ const enabled = plugins_1.enablePlugins.get();
32
+ const plugins = selectReportedPlugins([
33
+ ...(0, plugins_1.mapPlugins)((plugin, id) => ({ ...plugin.getData(), id, enabled: enabled.includes(id) }), false),
34
+ ...(0, plugins_1.getInactivePlugins)().map(plugin => ({ ...plugin, enabled: enabled.includes(plugin.id) })),
35
+ ]);
36
+ await send('POST', { uuid, hfsVersion: String(config_1.currentVersion), plugins });
37
+ lastReport.set(Date.now());
38
+ function selectReportedPlugins(installed) {
39
+ const selected = new Map();
40
+ for (const plugin of installed) {
41
+ const repo = typeof plugin.repo === 'string' ? plugin.repo : plugin.repo?.main;
42
+ if (!repo)
43
+ continue;
44
+ const current = selected.get(repo);
45
+ if (!current || better(plugin, current))
46
+ selected.set(repo, plugin);
47
+ }
48
+ return Array.from(selected, ([repo, plugin]) => ({
49
+ repo,
50
+ version: plugin.version,
51
+ enabled: plugin.enabled,
52
+ })).sort((a, b) => a.repo.localeCompare(b.repo));
53
+ function better(a, b) {
54
+ return Number(a.enabled) > Number(b.enabled)
55
+ || a.enabled === b.enabled && (a.id.length < b.id.length
56
+ || a.id.length === b.id.length && a.id.localeCompare(b.id) < 0);
57
+ }
58
+ }
59
+ });
60
+ shareUsageStats.sub(() => void syncPluginUsage().catch(() => { }));
61
+ (0, misc_1.repeat)(misc_1.HOUR, syncPluginUsage);
62
+ async function send(method, body) {
63
+ await (0, util_http_1.httpWithBody)(misc_1.PLUGIN_USAGE_URL + '/report', { method, body, timeout: 5000 });
64
+ }
package/src/plugins.js CHANGED
@@ -53,6 +53,7 @@ exports.getInactivePlugins = getInactivePlugins;
53
53
  exports.rescan = rescan;
54
54
  exports.parsePluginSource = parsePluginSource;
55
55
  exports.getMissingDependencies = getMissingDependencies;
56
+ const acmeDns_1 = require("./acmeDns");
56
57
  const fast_glob_1 = __importDefault(require("fast-glob"));
57
58
  const watchLoad_1 = require("./watchLoad");
58
59
  const lodash_1 = __importDefault(require("lodash"));
@@ -232,6 +233,13 @@ async function initPlugin(pl, morePassedToInit, onInitError) {
232
233
  return result;
233
234
  }
234
235
  },
236
+ registerAcmeDnsProvider(...args) {
237
+ if (unloading)
238
+ throw Error("Plugin is unloading");
239
+ const unregister = (0, acmeDns_1.registerAcmeDnsProvider)(...args);
240
+ cleanups.push(unregister);
241
+ return unregister;
242
+ },
235
243
  misc, _: lodash_1.default,
236
244
  customApiCall, notifyClient: frontEndApis_1.notifyClient, addBlock: block_1.addBlock, ctxBelongsTo: perm_1.ctxBelongsTo, getConnections: connections_1.getConnections, normalizeFilename: vfs_1.normalizeFilename,
237
245
  getCurrentUsername: auth_1.getCurrentUsername, getAccount: perm_1.getAccount, getUsernames: perm_1.getUsernames, addAccount: perm_1.addAccount, delAccount: perm_1.delAccount, updateAccount: perm_1.updateAccount, renameAccount: perm_1.renameAccount,
@@ -290,8 +298,8 @@ const pluginsMiddleware = async (ctx, next) => {
290
298
  const a = path.substring(const_1.PLUGINS_PUB_URI.length).split('/');
291
299
  const name = a.shift();
292
300
  if (plugins.has(name)) { // do it only if the plugin is loaded
293
- if (ctx.get('referer')?.endsWith('/'))
294
- ctx.state.considerAsGui = true;
301
+ // this server-owned route is GUI content; client headers must not grant the exemption
302
+ ctx.state.considerAsGui = true;
295
303
  await (0, serveFile_1.serveFile)(ctx, plugins.get(name).folder + '/public/' + a.join('/'), const_1.MIME_AUTO);
296
304
  }
297
305
  return;
@@ -734,6 +742,10 @@ function parsePluginSource(id, source) {
734
742
  pl.depend = (0, misc_1.tryJson)(/exports.depend\s*=\s*(\[[\s\S]*?])/m.exec(source)?.[1])?.filter((x) => typeof x.repo === 'string' && x.version === undefined || typeof x.version === 'number'
735
743
  || console.warn("Plugin dependency discarded", x));
736
744
  pl.changelog = (0, misc_1.tryJson)(/exports.changelog\s*=\s*(\[[\s\S]*?])/m.exec(source)?.[1]);
745
+ if (!Array.isArray(pl.changelog))
746
+ delete pl.changelog;
747
+ else if (lodash_1.default.remove(pl.changelog, entry => !Number.isFinite(entry?.version) || typeof entry?.message !== 'string').length)
748
+ console.warn("Invalid plugin changelog entries discarded", id);
737
749
  if (Array.isArray(pl.apiRequired) && (pl.apiRequired.length !== 2 || !pl.apiRequired.every(lodash_1.default.isFinite))) // validate [from,to] form
738
750
  pl.apiRequired = undefined;
739
751
  calculateBadApi(pl);
package/src/roots.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.rootsMiddleware = exports.roots = void 0;
6
+ exports.rootsMiddleware = exports.rootsHostGuard = exports.roots = void 0;
7
7
  const config_1 = require("./config");
8
8
  const misc_1 = require("./misc");
9
9
  const connections_1 = require("./connections");
@@ -17,6 +17,26 @@ exports.roots = (0, config_1.defineConfig)(misc_1.CFG.roots, {}, map => {
17
17
  return (host) => values[matchers.findIndex(m => m(host))];
18
18
  });
19
19
  const forceAddress = (0, config_1.defineConfig)(misc_1.CFG.force_address, false);
20
+ const rootsHostGuard = (ctx, next) => {
21
+ if (!ctx.path || lodash_1.default.isEmpty(exports.roots.get()))
22
+ return next();
23
+ if (ctx.path.startsWith(misc_1.SPECIAL_URI)) {
24
+ if (!ctx.path.startsWith(misc_1.API_URI))
25
+ return next();
26
+ const { referer } = ctx.headers;
27
+ const fromAdmin = referer && (0, misc_1.try_)(() => new URL(referer).pathname.startsWith(ctx.state.revProxyPath + misc_1.ADMIN_URI));
28
+ // authentication must remain reachable before ctxAdminAccess can succeed
29
+ if (fromAdmin && ((0, adminApis_1.ctxAdminAccess)(ctx)
30
+ || ['login', 'loginSrp1', 'loginSrp2'].some(x => ctx.path === misc_1.API_URI + x)))
31
+ return next();
32
+ }
33
+ const root = ctx.state.root = exports.roots.compiled()(ctx.host);
34
+ return !ctx.state.skipFilters && forceAddress.get()
35
+ && root === undefined && !(0, misc_1.isLocalHost)(ctx) && ctx.host !== listen_1.baseUrl.compiled()
36
+ ? (0, connections_1.disconnect)(ctx, forceAddress.key())
37
+ : next();
38
+ };
39
+ exports.rootsHostGuard = rootsHostGuard;
20
40
  const rootsMiddleware = (ctx, next) => (() => {
21
41
  if (!ctx.path) // it was once reported "Cannot read properties of null (reading 'startsWith')" but I can't reproduce it, and it shouldn't happen anyway
22
42
  return (0, connections_1.disconnect)(ctx, 'no path'); // let's assume that such requests can't be served anyway. This will leave a trace with an ip, anyway
@@ -27,19 +47,8 @@ const rootsMiddleware = (ctx, next) => (() => {
27
47
  return; // ...unless it's an api
28
48
  params = ctx.state.params || ctx.query; // for api we'll translate params
29
49
  changeUriParams(v => (0, misc_1.removeStarting)(ctx.state.revProxyPath, v)); // this removal must be done before adding the root; this operation doesn't conceptually belong to "roots", and it may be placed in different middleware, but it's convenient to do it here
30
- const { referer } = ctx.headers;
31
- const fromAdmin = referer && (0, misc_1.try_)(() => new URL(referer).pathname.startsWith(ctx.state.revProxyPath + misc_1.ADMIN_URI));
32
- // authentication must remain reachable before ctxAdminAccess can succeed
33
- if (fromAdmin && ((0, adminApis_1.ctxAdminAccess)(ctx)
34
- || ['login', 'loginSrp1', 'loginSrp2'].some(x => ctx.path === misc_1.API_URI + x)))
35
- return;
36
50
  }
37
- if (lodash_1.default.isEmpty(exports.roots.get()))
38
- return;
39
- const root = ctx.state.root = exports.roots.compiled()(ctx.host);
40
- if (!ctx.state.skipFilters && forceAddress.get()
41
- && root === undefined && !(0, misc_1.isLocalHost)(ctx) && ctx.host !== listen_1.baseUrl.compiled())
42
- return (0, connections_1.disconnect)(ctx, forceAddress.key()); // returning truthy will not call next
51
+ const { root } = ctx.state;
43
52
  if (!root || root === '/')
44
53
  return; // no transformation is required
45
54
  changeUriParams(v => (0, misc_1.join)(root, v));
package/src/serveFile.js CHANGED
@@ -8,6 +8,9 @@ exports.forceDownload = forceDownload;
8
8
  exports.disposition = disposition;
9
9
  exports.serveFileNode = serveFileNode;
10
10
  exports.enforceDownloadLimits = enforceDownloadLimits;
11
+ exports.resolveFileMime = resolveFileMime;
12
+ exports.getNodeMime = getNodeMime;
13
+ exports.isActiveContentMime = isActiveContentMime;
11
14
  exports.serveFile = serveFile;
12
15
  exports.monitorAsDownload = monitorAsDownload;
13
16
  exports.applyRange = applyRange;
@@ -26,11 +29,12 @@ const errorPages_1 = require("./errorPages");
26
29
  const stream_1 = require("stream");
27
30
  const crypto_1 = require("crypto");
28
31
  const iconv_lite_1 = __importDefault(require("iconv-lite"));
32
+ const upload_1 = require("./upload");
29
33
  const allowedReferer = (0, config_1.defineConfig)(misc_1.CFG.allowed_referer, '');
34
+ const requireUploadApprovalForActiveContent = (0, config_1.defineConfig)(misc_1.CFG.require_upload_approval_for_active_content, true);
30
35
  const maxDownloads = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads, 0), () => true);
31
36
  const maxDownloadsPerIp = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_ip, 0), ctx => ctx.ip);
32
37
  const maxDownloadsPerAccount = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_account, 0), ctx => (0, auth_1.getCurrentUsername)(ctx) || undefined);
33
- const GUI_ASSET_MIME = /^(image\/|audio\/|video\/|font\/|text\/css$|(?:text|application)\/(?:javascript|ecmascript)$|application\/x-javascript$)/i;
34
38
  function toAsciiEquivalent(s) {
35
39
  return iconv_lite_1.default.encode(iconv_lite_1.default.decode(Buffer.from(s), 'utf-8'), 'ascii').toString().replaceAll('?', '');
36
40
  }
@@ -49,6 +53,7 @@ async function serveFileNode(ctx, node) {
49
53
  const name = (0, vfs_1.getNodeName)(node);
50
54
  const mimeString = typeof mime === 'string' ? mime
51
55
  : lodash_1.default.find(mime, (_val, mask) => (0, misc_1.matches)(name, mask));
56
+ const effectiveMime = resolveFileMime(source || '', mimeString);
52
57
  if (allowedReferer.get()) {
53
58
  const ref = (0, misc_1.try_)(() => new URL(ctx.get('referer') || '').host);
54
59
  if (ref && ref !== ctx.host // automatically accept if the referer is basically the hosting domain
@@ -58,12 +63,13 @@ async function serveFileNode(ctx, node) {
58
63
  ctx.state.vfsNode = node; // useful to tell service files from files shared by the user
59
64
  const download = 'dl' in ctx.query;
60
65
  disposition(ctx, name, download);
61
- const fetchDest = ctx.get('sec-fetch-dest');
62
- ctx.state.considerAsGui ??= !download && ctx.get('referer')?.endsWith('/')
63
- && (fetchDest ? fetchDest !== 'document' && fetchDest !== 'empty' // modern clients
64
- // legacy clients often send Accept: */* for archive downloads, so the served mime is a safer signal than request headers here
65
- : GUI_ASSET_MIME.test(mimeString || mime_types_1.default.lookup(source || '') || ''));
66
- await serveFile(ctx, source || '', mimeString);
66
+ if (!download && source && isActiveContentMime(effectiveMime)) {
67
+ if ((0, upload_1.isUploading)(source)
68
+ || requireUploadApprovalForActiveContent.get() && (await (0, upload_1.getUploadMeta)(source))?.approved === false)
69
+ return (0, errorPages_1.sendErrorPage)(ctx, const_1.HTTP_FORBIDDEN);
70
+ }
71
+ // shared files count even when embedded: client headers cannot grant exemptions from limits or logging
72
+ await serveFile(ctx, source || '', effectiveMime);
67
73
  await enforceDownloadLimits(ctx);
68
74
  }
69
75
  async function enforceDownloadLimits(ctx) {
@@ -75,14 +81,25 @@ const mimeCfg = (0, config_1.defineConfig)(misc_1.CFG.mime, {}, obj => {
75
81
  const values = Object.values(obj);
76
82
  return (name) => values[matchers.findIndex(matcher => matcher(name))];
77
83
  });
84
+ function resolveFileMime(filePath, mime) {
85
+ mime ??= mimeCfg.compiled()((0, path_1.basename)(filePath));
86
+ return mime === undefined || mime === const_1.MIME_AUTO ? mime_types_1.default.lookup(filePath) || '' : mime;
87
+ }
88
+ function getNodeMime(node) {
89
+ const name = (0, vfs_1.getNodeName)(node);
90
+ const mime = typeof node.mime === 'string' ? node.mime
91
+ : lodash_1.default.find(node.mime, (_val, mask) => (0, misc_1.matches)(name, mask));
92
+ return resolveFileMime(node.source || '', mime);
93
+ }
94
+ function isActiveContentMime(mime) {
95
+ return /^(?:text\/html|application\/xhtml\+xml|image\/svg\+xml)(?:;|$)/i.test(mime);
96
+ }
78
97
  // after this number of seconds, the browser should check the server to see if there's a newer version of the file
79
98
  const cacheControlDiskFiles = (0, config_1.defineConfig)(misc_1.CFG.cache_control_disk_files, 5);
80
99
  async function serveFile(ctx, filePath, mime, cached) {
81
100
  if (!filePath)
82
101
  return;
83
- mime ??= mimeCfg.compiled()((0, path_1.basename)(filePath));
84
- if (mime === undefined || mime === const_1.MIME_AUTO)
85
- mime = mime_types_1.default.lookup(filePath) || '';
102
+ mime = resolveFileMime(filePath, mime);
86
103
  if (mime)
87
104
  ctx.type = mime;
88
105
  if (ctx.method === 'OPTIONS') {
@@ -90,7 +107,7 @@ async function serveFile(ctx, filePath, mime, cached) {
90
107
  ctx.set({ Allow: 'OPTIONS, GET, HEAD' });
91
108
  return;
92
109
  }
93
- if (ctx.method !== 'GET')
110
+ if (!ctx.state.getOrHead)
94
111
  return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
95
112
  try {
96
113
  const stats = cached?.stats || await (0, util_1.promisify)(fs_1.stat)(filePath); // using fs's function instead of fs/promises, because only the former is supported by pkg
@@ -96,7 +96,7 @@ const serveSharedFiles = async (ctx, next) => {
96
96
  }
97
97
  return;
98
98
  }
99
- if (/^\/favicon.ico(\??.*)/.test(ctx.originalUrl) && adminApis_1.favicon.get() && ctx.method === 'GET') // originalUrl to not be subject to changes (vhosting plugin)
99
+ if (ctx.state.getOrHead && /^\/favicon.ico(\??.*)/.test(ctx.originalUrl) && adminApis_1.favicon.get()) // originalUrl to not be subject to changes (vhosting plugin)
100
100
  return (0, serveFile_1.serveFile)(ctx, adminApis_1.favicon.get());
101
101
  let node = await (0, vfs_1.urlToNode)(path, ctx);
102
102
  if (!node)
@@ -123,11 +123,11 @@ const serveSharedFiles = async (ctx, next) => {
123
123
  return ctx.status = cross_const_1.HTTP_SERVER_ERROR;
124
124
  }
125
125
  }
126
- if (path.endsWith('/') && !get) { // final slash needed on browsers to make resource urls working with html pages
127
- const found = await (0, vfs_1.getDefaultFile)(node, ctx);
128
- if (found && /\.html?/i.test((0, vfs_1.getNodeName)(node = found)))
129
- ctx.state.considerAsGui = true;
130
- }
126
+ if (path.endsWith('/') && !get) // final slash needed on browsers to make resource urls working with html pages
127
+ node = await (0, vfs_1.getDefaultFile)(node, ctx) || node;
128
+ // classify the resolved default so a file can override its folder's GUI status
129
+ if (node.gui_asset)
130
+ ctx.state.considerAsGui = true;
131
131
  if (get === 'icon')
132
132
  return (0, serveFile_1.serveFile)(ctx, node.icon || '|'); // pipe to cause not-found
133
133
  if (!(0, vfs_1.nodeIsFolder)(node))
@@ -19,6 +19,7 @@ const customHtml_1 = require("./customHtml");
19
19
  const lodash_1 = __importDefault(require("lodash"));
20
20
  const config_1 = require("./config");
21
21
  const lang_1 = require("./lang");
22
+ const adminLang_1 = require("./adminLang");
22
23
  const upload_1 = require("./upload");
23
24
  const icons_1 = require("./icons");
24
25
  const middlewares_1 = require("./middlewares");
@@ -40,10 +41,10 @@ function serveStatic(uri) {
40
41
  ctx.state.dontLog = true;
41
42
  if (ctx.method === 'OPTIONS') {
42
43
  ctx.status = const_1.HTTP_NO_CONTENT;
43
- ctx.set({ Allow: 'OPTIONS, GET' });
44
+ ctx.set({ Allow: 'OPTIONS, GET, HEAD' });
44
45
  return;
45
46
  }
46
- if (ctx.method !== 'GET')
47
+ if (!ctx.state.getOrHead)
47
48
  return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
48
49
  const serveApp = shouldServeApp(ctx);
49
50
  const fullPath = (0, path_1.join)(root, serveApp ? '/index.html' : ctx.path);
@@ -107,7 +108,8 @@ async function treatIndex(ctx, filesUri, body) {
107
108
  return !lodash_1.default.isEmpty(configs) && [name, configs];
108
109
  })));
109
110
  const timestamp = await getFaviconTimestamp();
110
- const lang = await (0, lang_1.getLangData)(ctx);
111
+ const adminLangs = isFrontend ? [] : await (0, adminLang_1.getAdminLangs)();
112
+ const lang = isFrontend ? await (0, lang_1.getLangData)(ctx) : await (0, adminLang_1.getAdminLangData)(ctx, adminLangs);
111
113
  let group = 0; // this represents the loading-group: all plugins with the same group can be loaded concurrently
112
114
  const loadScripts = (0, misc_1.onlyTruthy)((0, plugins_1.mapPlugins)((p, id) => {
113
115
  const js = p.frontend_js?.map(f => f.includes('//') ? f : pub + id + '/' + f);
@@ -148,7 +150,8 @@ async function treatIndex(ctx, filesUri, body) {
148
150
  customHtml: lodash_1.default.omit((0, customHtml_1.getAllSections)(), ['top', 'bottom', 'htmlHead', 'style']), // exclude the sections we already apply in this phase
149
151
  ...(0, misc_1.newObj)(misc_1.FRONTEND_OPTIONS, (v, k) => (0, config_1.getConfig)(k)),
150
152
  icons: Object.assign({}, ...(0, plugins_1.mapPlugins)(p => iconsToObj(p.icons, p.id + '/')), iconsToObj(icons_1.customizedIcons)), // name-to-uri
151
- lang
153
+ lang,
154
+ ...(isFrontend ? {} : { adminLangs, adminLang: adminLang_1.adminLang.get() })
152
155
  }, null, 4).replace(/<(\/script)/g, '<"+"$1') /*avoid breaking our script container*/}
153
156
  document.documentElement.setAttribute('ver', HFS.VERSION.split('-')[0])
154
157
  document.documentElement.setAttribute('browser', ${JSON.stringify((0, misc_1.shortenAgent)(ctx.get('user-agent')))})
package/src/update.js CHANGED
@@ -10,7 +10,7 @@ exports.getVersions = getVersions;
10
10
  exports.getUpdates = getUpdates;
11
11
  exports.localUpdateAvailable = localUpdateAvailable;
12
12
  exports.previousAvailable = previousAvailable;
13
- exports.updateSupported = updateSupported;
13
+ exports.updateNotSupported = updateNotSupported;
14
14
  exports.update = update;
15
15
  const github_1 = require("./github");
16
16
  const const_1 = require("./const");
@@ -29,6 +29,7 @@ const lodash_1 = __importDefault(require("lodash"));
29
29
  const argv_1 = require("./argv");
30
30
  const promises_2 = require("stream/promises");
31
31
  const updateChangelog_1 = require("./updateChangelog");
32
+ const fast_xml_parser_1 = require("fast-xml-parser");
32
33
  const updateToBeta = (0, config_1.defineConfig)(misc_1.CFG.update_to_beta, false);
33
34
  const autoCheckUpdate = (0, config_1.defineConfig)(misc_1.CFG.auto_check_update, true);
34
35
  const lastCheckUpdate = persistence_1.storedMap.singleSync('lastCheckUpdate', 0);
@@ -59,15 +60,16 @@ exports.checkForUpdates = (0, misc_1.debounceAsync)(async () => {
59
60
  async function getBestUpdate() {
60
61
  return (await getUpdates(true))[0];
61
62
  }
62
- const ReleaseKeys = ['prerelease', 'tag_name', 'name', 'body', 'assets', 'isNewer', 'versionScalar'];
63
- const ReleaseAssetKeys = ['name', 'browser_download_url'];
63
+ const releaseKeys = ['prerelease', 'tag_name', 'name', 'body', 'assets', 'isNewer', 'versionScalar'];
64
+ const releaseAssetKeys = ['name', 'browser_download_url'];
65
+ const assetPrefix = `hfs-${(0, misc_1.xlate)(process.platform, { win32: 'windows', darwin: 'mac' })}-${process.arch}`;
64
66
  const curV = config_1.currentVersion.scalar;
65
67
  function prepareRelease(r) {
66
68
  const v = (0, config_1.versionToScalar)(r.name);
67
- return Object.assign(lodash_1.default.pick(r, ReleaseKeys), {
69
+ return Object.assign(lodash_1.default.pick(r, releaseKeys), {
68
70
  versionScalar: v,
69
71
  isNewer: v > curV, // make easy to know what's newer
70
- assets: r.assets.map((a) => lodash_1.default.pick(a, ReleaseAssetKeys))
72
+ assets: r.assets.map((a) => lodash_1.default.pick(a, releaseAssetKeys))
71
73
  });
72
74
  }
73
75
  async function getVersions(filter, max = 30, stopAtCurrent = false) {
@@ -91,10 +93,19 @@ async function getVersions(filter, max = 30, stopAtCurrent = false) {
91
93
  async function getUpdates(strict = false) {
92
94
  console.log("Checking for updates");
93
95
  void (0, github_1.getProjectInfo)(); // also check for alerts and print them asap in the console
94
- const stable = prepareRelease(await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases/latest'));
95
96
  const includeBetas = updateToBeta.get() || const_1.RUNNING_BETA;
96
- // we don't consider betas before stable
97
- const betas = !includeBetas ? [] : await getVersions(x => x.prerelease && x.versionScalar > stable.versionScalar && (!strict || x.isNewer));
97
+ const latest = await (0, misc_1.httpStream)(`https://github.com/${const_1.HFS_REPO}/releases/latest`, { method: 'HEAD', noRedirect: true });
98
+ const stableTag = latest.headers.location?.split('/').at(-1);
99
+ if (!stableTag)
100
+ throw Error('Cannot determine latest stable version');
101
+ if ((0, config_1.versionToScalar)(stableTag) === curV && !includeBetas)
102
+ return [];
103
+ let stable = prepareRelease({ prerelease: false, tag_name: stableTag, name: stableTag, body: '', assets: [probableAsset(stableTag)], isNewer: false, versionScalar: 0 });
104
+ if (stable.isNewer || const_1.RUNNING_BETA && !strict)
105
+ stable = await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases/tags/' + stableTag).then(prepareRelease).catch(() => stable);
106
+ const betas = !includeBetas ? [] : await getVersions(x => x.prerelease && x.versionScalar > stable.versionScalar && (!strict || x.isNewer))
107
+ // preserve update detection when GitHub's REST quota is exhausted
108
+ .catch(() => getVersionsFromAtom(x => x.prerelease && x.versionScalar > stable.versionScalar && (!strict || x.isNewer)));
98
109
  if (stable.isNewer || const_1.RUNNING_BETA && !strict)
99
110
  betas.push(stable);
100
111
  if (stable.isNewer && stable.body) {
@@ -102,7 +113,28 @@ async function getUpdates(strict = false) {
102
113
  .catch(() => []); // missing history must not prevent installing an available update
103
114
  stable.body = (0, updateChangelog_1.updateChangelog)(stable, history, curV);
104
115
  }
105
- return betas;
116
+ return lodash_1.default.sortBy(betas, x => -x.versionScalar);
117
+ async function getVersionsFromAtom(filter) {
118
+ const feed = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false })
119
+ .parse(await (0, misc_1.httpString)(`https://github.com/${const_1.HFS_REPO}/releases.atom`))?.feed;
120
+ return lodash_1.default.sortBy(lodash_1.default.castArray(feed?.entry)
121
+ .map((x) => prepareRelease({
122
+ prerelease: /^v?\d+\.\d+\.\d+-/.test(x.title),
123
+ tag_name: x.id.split('/').at(-1),
124
+ name: x.title,
125
+ body: x.content?.['#text'] || '',
126
+ assets: [probableAsset(x.id.split('/').at(-1))],
127
+ isNewer: false,
128
+ versionScalar: 0,
129
+ }))
130
+ .filter(x => Number.isFinite(x.versionScalar) && !x.name.endsWith('-ignore')
131
+ && x.versionScalar !== curV && filter(x)), x => -x.versionScalar);
132
+ }
133
+ function probableAsset(tag) {
134
+ const name = `${assetPrefix}-${tag.replace(/^v/, '')}.zip`;
135
+ // release-assets.yml uses this predictable name, keeping degraded checks installable
136
+ return { name, browser_download_url: `https://github.com/${const_1.HFS_REPO}/releases/download/${tag}/${name}` };
137
+ }
106
138
  }
107
139
  const LOCAL_UPDATE = 'hfs-update.zip'; // update from file takes precedence over net
108
140
  const INSTALLED_FN = 'hfs-installed.zip';
@@ -113,13 +145,16 @@ function localUpdateAvailable() {
113
145
  function previousAvailable() {
114
146
  return (0, misc_1.exists)(PREVIOUS_FN);
115
147
  }
116
- function updateSupported() {
117
- return !process.env.DISABLE_UPDATE && (argv_1.argv.forceupdate || const_1.IS_BINARY);
148
+ function updateNotSupported() {
149
+ return process.env.DISABLE_UPDATE ? "Automatic updates are disabled"
150
+ : !(argv_1.argv.forceupdate || const_1.IS_BINARY) ? "Only binary versions support automatic updates"
151
+ : (0, promises_1.access)((0, path_1.dirname)(process.execPath), fs_1.constants.W_OK | fs_1.constants.X_OK)
152
+ .then(() => '', () => `Executable directory is not writable: ${(0, path_1.dirname)(process.execPath)}`);
118
153
  }
119
154
  async function update(tagOrUrl = '') {
120
- if (!updateSupported())
121
- throw process.env.DISABLE_UPDATE ? "Automatic updates are disabled"
122
- : "Only binary versions support automatic updates";
155
+ const error = await updateNotSupported();
156
+ if (error)
157
+ throw error;
123
158
  let url = tagOrUrl.includes('://') && tagOrUrl;
124
159
  if (tagOrUrl === const_1.PREVIOUS_TAG)
125
160
  await (0, promises_1.rename)(PREVIOUS_FN, LOCAL_UPDATE);
@@ -135,11 +170,9 @@ async function update(tagOrUrl = '') {
135
170
  });
136
171
  if (!update)
137
172
  throw "No update has been found";
138
- const plat = '-' + (0, misc_1.xlate)(process.platform, { win32: 'windows', darwin: 'mac' });
139
- const assetSearch = `${plat}-${process.arch}`;
140
- const asset = update.assets.find((x) => x.name.includes(assetSearch) && x.name.endsWith('.zip'));
173
+ const asset = update.assets.find((x) => x.name.includes(assetPrefix) && x.name.endsWith('.zip'));
141
174
  if (!asset)
142
- throw `Asset not found: ${assetSearch}`;
175
+ throw `Asset not found: ${assetPrefix}`;
143
176
  url = asset.browser_download_url;
144
177
  }
145
178
  if (url) {