hfs 3.4.0-alpha1 → 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 (109) hide show
  1. package/admin/assets/AccountsPage-CoiPofEv.js +4 -0
  2. package/admin/assets/{ConfigFilePage-D5TZZyMR.js → ConfigFilePage-jUH2feqi.js} +1 -1
  3. package/admin/assets/{CustomHtmlPage-DV2-ufWZ.js → CustomHtmlPage-Pl_AJZrl.js} +1 -1
  4. package/admin/assets/{InternetPage-DT3wSQYQ.js → InternetPage-DIcAwhFk.js} +1 -1
  5. package/admin/assets/{LangPage-DbXCv7wR.js → LangPage-BwVY8QRD.js} +1 -1
  6. package/admin/assets/{LogsPage-DmAxUZpu.js → LogsPage-CcjS12cP.js} +1 -1
  7. package/admin/assets/{MonitorPage-D7ydOeq_.js → MonitorPage-DMVFXyMe.js} +1 -1
  8. package/admin/assets/OptionsPage-BB40ax9G.js +3 -0
  9. package/admin/assets/{PluginsPage-BYzfwuvg.js → PluginsPage-CC2dINcb.js} +1 -1
  10. package/admin/assets/{icons-CfBG9vvB.js → icons-C_ux1zLA.js} +1 -1
  11. package/admin/assets/{index-CVCnYMoB.js → index-BSSQOMG_.js} +4 -4
  12. package/admin/assets/{routing-C4l8rnk_.js → routing-Be72yFmd.js} +1 -1
  13. package/admin/index.html +1 -1
  14. package/frontend/assets/{dist-legacy-CKczq_fx.js → dist-legacy-D7Dg5w3L.js} +1 -1
  15. package/frontend/assets/index-legacy-CnazC3Dj.js +1 -0
  16. package/frontend/assets/{lib-legacy-D2tyILlj.js → lib-legacy-BxFHpdER.js} +1 -1
  17. package/frontend/assets/{sha512-legacy-CFcDeiiP.js → sha512-legacy-C8xoQBnP.js} +1 -1
  18. package/frontend/index.html +1 -1
  19. package/npm-shrinkwrap.json +66 -66
  20. package/package.json +1 -1
  21. package/plugins/antibrute/plugin.js +72 -18
  22. package/src/admin-langs/embedded.js +2 -2
  23. package/src/admin-langs/hfs-admin-lang-ar.json +34 -26
  24. package/src/admin-langs/hfs-admin-lang-bg.json +35 -27
  25. package/src/admin-langs/hfs-admin-lang-de.json +37 -29
  26. package/src/admin-langs/hfs-admin-lang-el.json +38 -30
  27. package/src/admin-langs/hfs-admin-lang-en.json +8 -0
  28. package/src/admin-langs/hfs-admin-lang-es.json +37 -29
  29. package/src/admin-langs/hfs-admin-lang-fi.json +100 -92
  30. package/src/admin-langs/hfs-admin-lang-fr.json +39 -31
  31. package/src/admin-langs/hfs-admin-lang-hu.json +37 -29
  32. package/src/admin-langs/hfs-admin-lang-it.json +35 -27
  33. package/src/admin-langs/hfs-admin-lang-ja.json +36 -28
  34. package/src/admin-langs/hfs-admin-lang-ko.json +39 -31
  35. package/src/admin-langs/hfs-admin-lang-lt.json +35 -27
  36. package/src/admin-langs/hfs-admin-lang-ms.json +36 -28
  37. package/src/admin-langs/hfs-admin-lang-nl.json +36 -28
  38. package/src/admin-langs/hfs-admin-lang-pl.json +36 -28
  39. package/src/admin-langs/hfs-admin-lang-pt-br.json +36 -28
  40. package/src/admin-langs/hfs-admin-lang-ro.json +37 -29
  41. package/src/admin-langs/hfs-admin-lang-ru.json +35 -27
  42. package/src/admin-langs/hfs-admin-lang-sr-latn.json +35 -27
  43. package/src/admin-langs/hfs-admin-lang-sr.json +39 -31
  44. package/src/admin-langs/hfs-admin-lang-th.json +35 -27
  45. package/src/admin-langs/hfs-admin-lang-tr.json +38 -30
  46. package/src/admin-langs/hfs-admin-lang-uk.json +35 -27
  47. package/src/admin-langs/hfs-admin-lang-vi.json +35 -27
  48. package/src/admin-langs/hfs-admin-lang-zh-hant.json +1149 -0
  49. package/src/admin-langs/hfs-admin-lang-zh.json +728 -720
  50. package/src/adminApis.js +2 -1
  51. package/src/adminLang.js +8 -7
  52. package/src/api.accounts.js +9 -4
  53. package/src/api.auth.js +11 -3
  54. package/src/api.vfs.js +3 -2
  55. package/src/auth.js +25 -14
  56. package/src/consoleLog.js +4 -0
  57. package/src/const.js +1 -1
  58. package/src/cross-const.js +1 -1
  59. package/src/cross.js +1 -1
  60. package/src/customHtml.js +3 -2
  61. package/src/deletePermission.js +120 -0
  62. package/src/events.js +6 -0
  63. package/src/index.js +3 -2
  64. package/src/lang.js +6 -1
  65. package/src/langs/embedded.js +2 -2
  66. package/src/langs/hfs-lang-ar.json +2 -1
  67. package/src/langs/hfs-lang-bg.json +2 -1
  68. package/src/langs/hfs-lang-de.json +2 -1
  69. package/src/langs/hfs-lang-el.json +2 -1
  70. package/src/langs/hfs-lang-en.json +2 -1
  71. package/src/langs/hfs-lang-es.json +2 -1
  72. package/src/langs/hfs-lang-fi.json +2 -1
  73. package/src/langs/hfs-lang-fr.json +2 -1
  74. package/src/langs/hfs-lang-hu.json +2 -1
  75. package/src/langs/hfs-lang-it.json +2 -1
  76. package/src/langs/hfs-lang-ja.json +2 -1
  77. package/src/langs/hfs-lang-ko.json +2 -1
  78. package/src/langs/hfs-lang-lt.json +2 -1
  79. package/src/langs/hfs-lang-ms.json +2 -1
  80. package/src/langs/hfs-lang-nl.json +2 -1
  81. package/src/langs/hfs-lang-pl.json +2 -1
  82. package/src/langs/hfs-lang-pt-br.json +2 -1
  83. package/src/langs/hfs-lang-ro.json +2 -1
  84. package/src/langs/hfs-lang-ru.json +2 -1
  85. package/src/langs/hfs-lang-sr-latn.json +2 -1
  86. package/src/langs/hfs-lang-sr.json +2 -1
  87. package/src/langs/hfs-lang-th.json +2 -1
  88. package/src/langs/hfs-lang-tr.json +2 -1
  89. package/src/langs/hfs-lang-uk.json +2 -1
  90. package/src/langs/hfs-lang-vi.json +2 -1
  91. package/src/langs/hfs-lang-zh-hant.json +189 -0
  92. package/src/langs/hfs-lang-zh.json +96 -98
  93. package/src/listen.js +8 -4
  94. package/src/middlewares.js +31 -28
  95. package/src/perm.js +26 -4
  96. package/src/plugins.js +2 -2
  97. package/src/serveFile.js +5 -9
  98. package/src/serveGuiAndSharedFiles.js +9 -8
  99. package/src/serveGuiFiles.js +2 -2
  100. package/src/sessionStamp.js +53 -0
  101. package/src/upload.js +1 -1
  102. package/src/uploadOwners.js +4 -2
  103. package/src/vfs.js +80 -33
  104. package/src/webdav.js +2 -2
  105. package/admin/assets/AccountsPage-C6J2hRcY.js +0 -4
  106. package/admin/assets/OptionsPage-DpN3ajRm.js +0 -3
  107. package/frontend/assets/index-legacy-ZYae30AY.js +0 -1
  108. package/src/admin-langs/hfs-admin-lang-zh-tw.json +0 -1141
  109. package/src/langs/hfs-lang-zh-tw.json +0 -187
package/src/adminApis.js CHANGED
@@ -38,6 +38,7 @@ const ddns_1 = require("./ddns");
38
38
  const block_1 = require("./block");
39
39
  const github_1 = require("./github");
40
40
  const acme_1 = require("./acme");
41
+ const first_1 = require("./first");
41
42
  exports.adminApis = {
42
43
  ...api_vfs_1.default,
43
44
  ...api_accounts_1.default,
@@ -133,7 +134,7 @@ exports.adminApis = {
133
134
  return {};
134
135
  },
135
136
  quit() {
136
- setTimeout(() => process.exit());
137
+ setTimeout(first_1.quit); // let the API response finish before starting ordered cleanup
137
138
  return {};
138
139
  },
139
140
  async get_status(_params, ctx) {
package/src/adminLang.js CHANGED
@@ -22,7 +22,7 @@ async function getAdminLangs() {
22
22
  return lodash_1.default.uniq([...Object.keys(embedded_1.default), ...files.map(name => (0, lang_1.file2code)(name, true))
23
23
  .filter(code => (0, lang_1.normalizeLangCode)(code) === code)]);
24
24
  }
25
- exports.adminLang = (0, config_1.defineConfig)(misc_1.CFG.admin_lang, '', lang_1.normalizeLangCode);
25
+ exports.adminLang = (0, config_1.defineConfig)(misc_1.CFG.admin_lang, '', v => (0, lang_1.langCodeForLookup)((0, lang_1.normalizeLangCode)(v)));
26
26
  const cache = (0, expiringCache_1.expiringCache)(3_000);
27
27
  function getAdminLangData(ctx, langs) {
28
28
  const configured = exports.adminLang.compiled();
@@ -39,12 +39,13 @@ function invalidateAdminLang(code) {
39
39
  function browserAdminLang(ctx, langs) {
40
40
  const accepted = ctx?.get('Accept-Language') || '';
41
41
  for (const raw of accepted.split(',')) {
42
- const code = (0, lang_1.normalizeLangCode)(raw);
43
- if (langs.includes(code))
44
- return code;
45
- const base = code.split('-')[0] || '';
46
- if (langs.includes(base))
47
- return base;
42
+ let code = (0, lang_1.normalizeLangCode)(raw);
43
+ while (code) {
44
+ const candidate = (0, lang_1.langCodeForLookup)(code);
45
+ if (langs.includes(candidate))
46
+ return candidate;
47
+ code = code.substring(0, code.lastIndexOf('-'));
48
+ }
48
49
  }
49
50
  return const_1.EMBEDDED_LANGUAGE;
50
51
  }
@@ -9,6 +9,7 @@ const perm_1 = require("./perm");
9
9
  const lodash_1 = __importDefault(require("lodash"));
10
10
  const const_1 = require("./const");
11
11
  const auth_1 = require("./auth");
12
+ const sessionStamp_1 = require("./sessionStamp");
12
13
  const misc_1 = require("./misc");
13
14
  const api_vfs_1 = require("./api.vfs");
14
15
  function serializeAccount(ac) {
@@ -23,7 +24,7 @@ function serializeAccount(ac) {
23
24
  adminActualAccess: (0, perm_1.accountCanLoginAdmin)(ac),
24
25
  canLogin: hasLogin ? (0, perm_1.accountCanLogin)(ac) : undefined,
25
26
  canChangePassword: (0, perm_1.accountCanChangePassword)(ac),
26
- invalidated: auth_1.invalidateSessionBefore.get(ac.username),
27
+ invalidated: Number((0, sessionStamp_1.getSessionStamp)(ac.username)?.split('.')[0]) || undefined,
27
28
  directMembers: Object.values(perm_1.accounts.get()).filter(a => a.belongs?.includes(ac.username)).map(x => x.username),
28
29
  members: (0, misc_1.with_)(Object.values(perm_1.accounts.get()), accounts => {
29
30
  const ret = [];
@@ -60,11 +61,13 @@ exports.default = {
60
61
  if (!acc)
61
62
  return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST);
62
63
  await (0, perm_1.updateAccount)(acc, (0, api_vfs_1.pickProps)(changes, ALLOWED_KEYS));
63
- if (changes.username && ctx.session?.username === (0, perm_1.normalizeUsername)(username)) // update session if necessary
64
+ if (ctx.session?.username === (0, perm_1.normalizeUsername)(username)) { // preserve only the session making its own account change
64
65
  ctx.session.username = acc.username;
66
+ ctx.session.stamp = (0, sessionStamp_1.getSessionStamp)(acc.username);
67
+ }
65
68
  return lodash_1.default.pick(acc, 'username');
66
69
  },
67
- async add_account({ overwrite, username, ...rest }) {
70
+ async add_account({ overwrite, username, ...rest }, ctx) {
68
71
  (0, misc_1.apiAssertTypes)({ string: { username } });
69
72
  const existing = (0, perm_1.getAccount)(username);
70
73
  rest = (0, api_vfs_1.pickProps)(rest, ALLOWED_KEYS);
@@ -72,6 +75,8 @@ exports.default = {
72
75
  if (!overwrite)
73
76
  return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT);
74
77
  await (0, perm_1.updateAccount)(existing, rest);
78
+ if (ctx.session?.username === existing.username)
79
+ ctx.session.stamp = (0, sessionStamp_1.getSessionStamp)(existing.username);
75
80
  return lodash_1.default.pick(existing, 'username');
76
81
  }
77
82
  const acc = await (0, perm_1.addAccount)(username, rest);
@@ -87,7 +92,7 @@ exports.default = {
87
92
  },
88
93
  invalidate_sessions({ username }) {
89
94
  (0, misc_1.apiAssertTypes)({ string: { username } });
90
- auth_1.invalidateSessionBefore.set((0, perm_1.normalizeUsername)(username), Date.now());
95
+ (0, perm_1.invalidateAccountSessions)(username);
91
96
  return {};
92
97
  },
93
98
  };
package/src/api.auth.js CHANGED
@@ -16,6 +16,7 @@ const events_1 = __importDefault(require("./events"));
16
16
  const misc_1 = require("./misc");
17
17
  const uploadOwners_1 = require("./uploadOwners");
18
18
  const node_crypto_1 = require("node:crypto");
19
+ const sessionStamp_1 = require("./sessionStamp");
19
20
  const ongoingLogins = {}; // store data that doesn't fit session object
20
21
  const keepSessionAlive = (0, config_1.defineConfig)(misc_1.CFG.keep_session_alive, true);
21
22
  const fakeSrpSecret = (0, node_crypto_1.randomBytes)(32);
@@ -66,7 +67,7 @@ exports.authApis = {
66
67
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR);
67
68
  if (account?.plugin?.auth) // tell client to do clear-text login, before firing attemptingLogin, before triggering anti-brute
68
69
  return new apiMiddleware_1.ApiError(const_1.HTTP_METHOD_NOT_ALLOWED);
69
- if ((await events_1.default.emitAsync('attemptingLogin', { ctx, username }))?.isDefaultPrevented())
70
+ if ((await events_1.default.emitAsync('attemptingLogin', { ctx, username, via: 'srp' }))?.isDefaultPrevented())
70
71
  return;
71
72
  if (account && !(0, perm_1.accountCanLogin)(account)) {
72
73
  ctx.logExtra({ u: username });
@@ -76,13 +77,14 @@ exports.authApis = {
76
77
  if (account && (0, middlewares_1.failAllowNet)(ctx, account))
77
78
  return unauthorized();
78
79
  try { // unknown users complete step 1 so only a full failed login can reveal and penalize the attempt
80
+ const stamp = account && (0, sessionStamp_1.getSessionStamp)(account.username);
79
81
  const { srpServer, ...rest } = await (0, auth_1.srpServerStep1)(account || fakeSrpAccount(username));
80
82
  // keep the public handshake identifier independent of predictable application PRNG state
81
83
  const sid = (0, node_crypto_1.randomUUID)();
82
84
  ongoingLogins[sid] = srpServer;
83
85
  setTimeout(() => delete ongoingLogins[sid], 60_000); // client must complete api sequence (loginSrp2) within 1 minute or will be discarded to avoid memory leaks
84
86
  // 60-second expiry bounded 5k challenges to ~37 MB in a local saturation test, so an additional cap isn't justified
85
- ctx.session.loggingIn = { username, sid }; // temporarily store until process is complete
87
+ ctx.session.loggingIn = { username, sid, stamp }; // temporarily store until process is complete
86
88
  return rest;
87
89
  }
88
90
  catch (code) {
@@ -109,7 +111,7 @@ exports.authApis = {
109
111
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR);
110
112
  if (!ctx.session.loggingIn)
111
113
  return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT);
112
- const { username, sid } = ctx.session.loggingIn;
114
+ const { username, sid, stamp } = ctx.session.loggingIn;
113
115
  delete ctx.session.loggingIn;
114
116
  const step1 = ongoingLogins[sid];
115
117
  if (!step1)
@@ -117,6 +119,10 @@ exports.authApis = {
117
119
  try {
118
120
  const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
119
121
  .catch(() => { throw ''; }); // falsy value for later
122
+ // an old-password handshake must not mint a session after credentials have been revoked
123
+ const account = (0, perm_1.getAccount)(username);
124
+ if (!account || !stamp || stamp !== (0, sessionStamp_1.getSessionStamp)(account.username))
125
+ throw '';
120
126
  await (0, auth_1.setLoggedIn)(ctx, username, 'srp');
121
127
  return {
122
128
  proof: String(M2),
@@ -150,6 +156,8 @@ exports.authApis = {
150
156
  if (!salt || !verifier)
151
157
  return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'missing parameters');
152
158
  await (0, perm_1.updateAccount)(a, a => (0, perm_1.saveSrpInfo)(a, salt, verifier));
159
+ if (ctx.session?.username === a.username)
160
+ ctx.session.stamp = (0, sessionStamp_1.getSessionStamp)(a.username);
153
161
  delete a.require_password_change;
154
162
  return {};
155
163
  }
package/src/api.vfs.js CHANGED
@@ -36,8 +36,9 @@ exports.default = {
36
36
  delete copyStats.mtime;
37
37
  const inherited = node.parent && (0, vfs_1.permsFromParent)(node.parent, {});
38
38
  const byMasks = node.original && lodash_1.default.pickBy(node, (v, k) => v !== node.original[k] // something is changing me...
39
- && !(inherited && k in inherited) // ...and it's not inheritance...
40
- && misc_1.PERM_KEYS.includes(k)); // ...must be masks. Please limit this to perms
39
+ && (k === 'gui_asset'
40
+ ? v !== node.parent?.gui_asset // gui_asset inherits outside the permission helpers
41
+ : !(inherited && k in inherited) && misc_1.PERM_KEYS.includes(k))); // ...must be masks
41
42
  return {
42
43
  ...copyStats,
43
44
  ...node.original || node,
package/src/auth.js CHANGED
@@ -36,7 +36,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.invalidateSessionBefore = void 0;
40
39
  exports.srpServerStep1 = srpServerStep1;
41
40
  exports.srpCheck = srpCheck;
42
41
  exports.getCurrentUsername = getCurrentUsername;
@@ -51,6 +50,7 @@ const cross_1 = require("./cross");
51
50
  const expiringCache_1 = require("./expiringCache");
52
51
  const node_crypto_1 = require("node:crypto");
53
52
  const events_1 = __importDefault(require("./events"));
53
+ const sessionStamp_1 = require("./sessionStamp");
54
54
  const srp6aNimbusRoutines = new srp.SRPRoutines(new srp.SRPParameters());
55
55
  async function srpServerStep1(account) {
56
56
  if (!account.srp)
@@ -67,13 +67,17 @@ async function srpCheck(username, password) {
67
67
  const account = (0, perm_1.getAccount)(username);
68
68
  if (!account?.srp || !password)
69
69
  return;
70
- const k = (0, node_crypto_1.createHash)('sha256').update(username + password + account.srp).digest("hex");
70
+ const verifier = account.srp;
71
+ const sessionStamp = (0, sessionStamp_1.getSessionStamp)(account.username);
72
+ const k = (0, node_crypto_1.createHash)('sha256').update(username + password + verifier).digest("hex");
71
73
  const good = await cache.try(k, async () => {
72
74
  const { srpServer, salt, pubKey } = await srpServerStep1(account);
73
75
  const client = await (0, srp_1.srpClientPart)(srp, username, password, salt, pubKey);
74
76
  return srpServer.step2(client.A, client.M1).then(() => true, () => false);
75
77
  });
76
- return good ? account : undefined;
78
+ // verification can finish after a password change or account replacement
79
+ return good && (0, perm_1.getAccount)(username) === account && account.srp === verifier && (0, sessionStamp_1.getSessionStamp)(account.username) === sessionStamp
80
+ ? account : undefined;
77
81
  }
78
82
  function getCurrentUsername(ctx) {
79
83
  return ctx.state.account?.username || '';
@@ -81,13 +85,19 @@ function getCurrentUsername(ctx) {
81
85
  async function clearTextLogin(ctx, u, p, via) {
82
86
  if (!p)
83
87
  return;
84
- if ((await events_1.default.emitAsync('attemptingLogin', { ctx, username: u, via }))?.isDefaultPrevented())
85
- return;
86
- const plugins = await events_1.default.emitAsync('clearTextLogin', { ctx, username: u, password: p, via }); // provide clear password to plugins
87
- const a = plugins?.some(x => x === true) ? (0, perm_1.getAccount)(u) : await srpCheck(u, p);
88
- if (!a && u)
89
- events_1.default.emit('failedLogin', { ctx, username: u, via });
90
- return a;
88
+ let account;
89
+ try {
90
+ if ((await events_1.default.emitAsync('attemptingLogin', { ctx, username: u, via }))?.isDefaultPrevented())
91
+ return;
92
+ const plugins = await events_1.default.emitAsync('clearTextLogin', { ctx, username: u, password: p, via }); // provide clear password to plugins
93
+ account = plugins?.some(x => x === true) ? (0, perm_1.getAccount)(u) : await srpCheck(u, p);
94
+ if (!account && u)
95
+ events_1.default.emit('failedLogin', { ctx, username: u, via });
96
+ return account;
97
+ }
98
+ finally {
99
+ events_1.default.emit('loginAttemptFinished', { ctx, username: u, via, success: Boolean(account) });
100
+ }
91
101
  }
92
102
  // centralized log-in state
93
103
  async function setLoggedIn(ctx, username, via) {
@@ -106,8 +116,11 @@ async function setLoggedIn(ctx, username, via) {
106
116
  const a = ctx.state.account = (0, perm_1.getAccount)(username);
107
117
  if (!a)
108
118
  return;
119
+ const sessionStamp = (0, sessionStamp_1.getSessionStamp)(a.username);
109
120
  const result = await events_1.default.emitAsync('finalizingLogin', { ctx, username: a.username, via, inputs: { ...ctx.state.params, ...ctx.query } });
110
- const error = result?.find(x => x && lodash_1.default.isString(x)) || result?.isDefaultPrevented() && "Login denied";
121
+ // async plugin checks must not let a revoked login acquire the new session stamp
122
+ const error = result?.find(x => x && lodash_1.default.isString(x)) || (result?.isDefaultPrevented()
123
+ || !sessionStamp || (0, perm_1.getAccount)(username) !== a || (0, sessionStamp_1.getSessionStamp)(a.username) !== sessionStamp) && "Login denied";
111
124
  if (error) {
112
125
  // restore the session identity: the candidate account was exposed only for plugin checks
113
126
  ctx.state.account = (0, perm_1.getAccount)(s.username, false);
@@ -116,13 +129,11 @@ async function setLoggedIn(ctx, username, via) {
116
129
  }
117
130
  const normalized = (0, perm_1.normalizeUsername)(username);
118
131
  s.username = normalized;
119
- s.ts = Date.now();
120
132
  s.ip = ctx.ip;
133
+ s.stamp = sessionStamp;
121
134
  const k = cross_const_1.ALLOW_SESSION_IP_CHANGE;
122
135
  s[k] = k in ctx.query || Boolean(ctx.state.params?.[k]) || undefined; // login APIs will get ctx.state.params, others can rely on ctx.query
123
136
  if (!a.expire && a.days_to_live)
124
137
  (0, perm_1.updateAccount)(a, { expire: new Date(Date.now() + a.days_to_live * cross_1.DAY) });
125
138
  await events_1.default.emitAsync('login', ctx);
126
139
  }
127
- // since session are currently stored in cookies, we need to store this information
128
- exports.invalidateSessionBefore = new Map();
package/src/consoleLog.js CHANGED
@@ -17,6 +17,7 @@ f?.on('error', err => {
17
17
  console.error("Cannot write console file", argv_1.argv.consoleFile, String(err));
18
18
  });
19
19
  let terminalOutputBroken = false;
20
+ let lastTerminalDate = '';
20
21
  for (const stream of [process.stdout, process.stderr])
21
22
  stream.on('error', err => {
22
23
  if (!isBrokenTerminalOutput(err))
@@ -40,6 +41,9 @@ for (const k of ['log', 'warn', 'error', 'debug']) {
40
41
  events_1.default.emit('console', rec);
41
42
  f?.write(`${(0, cross_1.formatTimestamp)(ts)} [${k}] ${msg}\n`);
42
43
  if (!terminalOutputBroken) {
44
+ const date = (0, cross_1.formatDate)(ts);
45
+ if (date !== lastTerminalDate)
46
+ console.log.original(lastTerminalDate = date);
43
47
  try {
44
48
  return original((0, cross_1.formatTime)(ts), ...args);
45
49
  } // bundled nodejs doesn't have locales (and apparently uses en-US)
package/src/const.js CHANGED
@@ -49,7 +49,7 @@ const path_1 = require("path");
49
49
  const cross_1 = require("./cross");
50
50
  const argv_1 = require("./argv");
51
51
  __exportStar(require("./cross-const"), exports);
52
- exports.API_VERSION = 13.6;
52
+ exports.API_VERSION = 13.71;
53
53
  exports.COMPATIBLE_API_VERSION = 1; // the day we break with the past, we'll update this
54
54
  // you can add arguments with this file, currently used for the update process on mac/linux.
55
55
  // we are using homedir because it's the only stable path both the old and new process can agree on (open() doesn't preserve cwd)
@@ -43,7 +43,7 @@ exports.CFG = constMap([
43
43
  'smart_unc_folder_detection', 'sort_by', 'sort_numerics', 'split_uploads',
44
44
  'suspend_plugins', 'theme', 'tile_size', 'title', 'title_with_path', 'track_ips',
45
45
  'update_to_beta', 'upnp_enabled', 'version', 'vfs', 'webdav_initial_auth',
46
- 'zip_calculate_size_for_seconds', 'https_port'
46
+ 'zip_calculate_size_for_seconds', 'https_port', 'require_upload_approval_for_active_content',
47
47
  ]);
48
48
  function constMap(a) {
49
49
  return Object.fromEntries(a.map(x => [x, x]));
package/src/cross.js CHANGED
@@ -143,7 +143,7 @@ exports.defaultPerms = {
143
143
  };
144
144
  exports.PERM_KEYS = typedKeys(exports.defaultPerms);
145
145
  exports.VFS_STORED_KEYS = ['name', 'source', 'masks', 'default', 'accept', 'rename',
146
- 'mime', 'url', 'target', 'comment', 'icon', 'order', 'see_without_probing', 'children', ...exports.PERM_KEYS];
146
+ 'mime', 'url', 'target', 'comment', 'icon', 'order', 'gui_asset', 'see_without_probing', 'children', ...exports.PERM_KEYS];
147
147
  function isWhoObject(v) {
148
148
  return v !== null && typeof v === 'object' && !Array.isArray(v);
149
149
  }
package/src/customHtml.js CHANGED
@@ -14,8 +14,9 @@ const config_1 = require("./config");
14
14
  const plugins_1 = require("./plugins");
15
15
  const lodash_1 = __importDefault(require("lodash"));
16
16
  const FILE = 'custom.html';
17
- exports.customHtmlSections = ['style', 'script', 'beforeHeader', 'afterHeader', 'afterMenuBar', 'afterList',
18
- 'footer', 'top', 'bottom', 'afterEntryName', 'beforeLogin', 'unauthorized', 'htmlHead', 'userPanelAfterInfo'];
17
+ exports.customHtmlSections = ['style', 'script', 'beforeHeader', 'afterHeader', 'afterMenuBar',
18
+ 'footer', 'top', 'bottom', 'afterEntryName', 'beforeLogin', 'unauthorized', 'htmlHead', 'userPanelAfterInfo',
19
+ 'afterList', 'beforeOptions', 'afterOptions'];
19
20
  exports.customHtml = watchLoadCustomHtml();
20
21
  exports.disableCustomHtml = (0, config_1.defineConfig)(misc_1.CFG.disable_custom_html, false);
21
22
  function watchLoadCustomHtml(folder = '') {
@@ -0,0 +1,120 @@
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.checkDeletePermission = checkDeletePermission;
7
+ const promises_1 = __importDefault(require("node:fs/promises"));
8
+ const node_path_1 = require("node:path");
9
+ const cross_1 = require("./cross");
10
+ const const_1 = require("./const");
11
+ const events_1 = __importDefault(require("./events"));
12
+ const uploadOwners_1 = require("./uploadOwners");
13
+ const vfs_1 = require("./vfs");
14
+ // unlike a listing, deletion affects hidden entries and must fail if any directory cannot be read
15
+ async function checkDeletePermission(node, ctx) {
16
+ if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_delete', ctx))
17
+ return false;
18
+ if (!node.source || !(await promises_1.default.lstat(node.source)).isDirectory())
19
+ return true;
20
+ return recur(node, []);
21
+ async function recur(parent, scopes) {
22
+ // explicit VFS entries are not deletable, even when their source is several levels below this directory
23
+ for (const child of explicitNodes(parent)) {
24
+ if (!child.source)
25
+ continue;
26
+ const path = (0, node_path_1.relative)((0, node_path_1.resolve)(parent.source), (0, node_path_1.resolve)(child.source));
27
+ if (path === '..' || path.startsWith('..' + node_path_1.sep) || (0, node_path_1.isAbsolute)(path))
28
+ continue;
29
+ try {
30
+ await promises_1.default.lstat(child.source);
31
+ }
32
+ catch (error) {
33
+ if (error.code === 'ENOENT')
34
+ continue;
35
+ throw error;
36
+ }
37
+ ctx.status = const_1.HTTP_FORBIDDEN;
38
+ await reportBlocked(child);
39
+ return false;
40
+ }
41
+ const inherited = (0, vfs_1.permsFromParent)(parent, {}) || {};
42
+ const keys = new Set(['can_delete']);
43
+ for (const key of keys) {
44
+ addReferences(parent[key] ?? cross_1.defaultPerms[key]);
45
+ addReferences(inherited[key] ?? cross_1.defaultPerms[key]);
46
+ }
47
+ // skip scanning descendants only when inherited permissions guarantee deletion and no deeper rule or plugin can override them
48
+ if (!scopes.some(scope => hasRules(scope.node, keys)) && !hasRules(parent, keys)
49
+ && !cross_1.PERM_KEYS.some(key => (0, cross_1.isWhoObject)(inherited[key]))
50
+ && !events_1.default.hasOtherListeners('checkVfsPermission', uploadOwners_1.grantUploadOwnerDelete)
51
+ && !(0, vfs_1.statusCodeForMissingPerm)(inherited, 'can_delete', ctx, false, false))
52
+ return true;
53
+ // only literal path prefixes can be exhausted by inheritMasks; other patterns may match again deeper down
54
+ const persistentMasks = Object.fromEntries(Object.entries(parent.masks || {}).filter(([mask, mods]) => mods && (cross_1.PERM_KEYS.some(key => mods[key] !== undefined) || hasRules(mods, new Set(cross_1.PERM_KEYS)))
55
+ && (!mask.includes('/') || /[!*?\[\]{}()|\\]/.test(mask.split('/')[0]))));
56
+ if (Object.keys(persistentMasks).length)
57
+ scopes = [...scopes, { node: { masks: persistentMasks }, path: '' }];
58
+ for await (const entry of await promises_1.default.opendir(parent.source)) {
59
+ if (ctx.isAborted())
60
+ throw Error("Deletion cancelled");
61
+ const source = (0, node_path_1.join)(parent.source, entry.name);
62
+ const name = (0, vfs_1.getVirtualName)(entry.name, parent, source);
63
+ const explicit = (0, vfs_1.getChildBySource)(parent, source);
64
+ // permission masks use the target type, but deletion must never recurse through the link
65
+ const isFolder = entry.isSymbolicLink()
66
+ ? await promises_1.default.stat(source).then(stats => stats.isDirectory(), error => {
67
+ if (error.code === 'ENOENT')
68
+ return false;
69
+ throw error;
70
+ }) : entry.isDirectory();
71
+ const child = {
72
+ ...explicit, original: explicit, source, isFolder,
73
+ rename: explicit?.rename || (0, vfs_1.renameUnderPath)(parent.rename, entry.name),
74
+ };
75
+ const nextScopes = scopes.map(scope => ({ node: scope.node, path: scope.path + name }));
76
+ for (const scope of [...nextScopes].reverse()) {
77
+ (0, vfs_1.parentMaskApplier)(scope.node, true)(child, scope.path);
78
+ (0, vfs_1.parentMaskApplier)(scope.node)(child, name);
79
+ }
80
+ const evaluated = (0, vfs_1.applyParentToChild)(child, parent, name);
81
+ if ((0, vfs_1.statusCodeForMissingPerm)(evaluated, 'can_delete', ctx)) {
82
+ await reportBlocked(evaluated);
83
+ return false;
84
+ }
85
+ // dirents do not follow symlinks, including dangling links
86
+ if (entry.isDirectory() && !await recur(evaluated, nextScopes.map(scope => ({ ...scope, path: scope.path + '/' }))))
87
+ return false;
88
+ }
89
+ return true;
90
+ function addReferences(who) {
91
+ if ((0, cross_1.isWhoObject)(who)) {
92
+ if (who.this !== undefined)
93
+ addReferences(who.this);
94
+ if (who.children !== undefined)
95
+ addReferences(who.children);
96
+ }
97
+ else if (typeof who === 'string' && cross_1.PERM_KEYS.includes(who))
98
+ keys.add(who);
99
+ }
100
+ }
101
+ async function reportBlocked(blocked) {
102
+ // a deletion error must not disclose names hidden by access permissions
103
+ let visible = blocked;
104
+ for (let ancestor = blocked; ancestor && ancestor !== node; ancestor = ancestor.parent)
105
+ if (!(0, vfs_1.hasPermission)(ancestor, 'can_see', ctx) || !await (0, vfs_1.urlToNode)(ancestor.vfsPath, ctx)
106
+ || ancestor.parent && !(0, vfs_1.hasPermission)(ancestor.parent, 'can_list', ctx))
107
+ visible = ancestor.parent || node;
108
+ ctx.body = { uri: '/' + visible.vfsPath };
109
+ }
110
+ function* explicitNodes(parent) {
111
+ for (const child of parent.children || []) {
112
+ const evaluated = (0, vfs_1.applyParentToChild)(child, parent);
113
+ yield evaluated;
114
+ yield* explicitNodes(evaluated);
115
+ }
116
+ }
117
+ }
118
+ function hasRules(node, keys) {
119
+ return Boolean(node.children?.length || Object.values(node.masks || {}).some(mods => mods && (Array.from(keys).some(key => mods[key] !== undefined) || hasRules(mods, keys))));
120
+ }
package/src/events.js CHANGED
@@ -56,6 +56,12 @@ class BetterEventEmitter {
56
56
  anyListener(event) {
57
57
  return Boolean(this.listeners.get(event)?.size);
58
58
  }
59
+ hasOtherListeners(event, mine) {
60
+ for (const x of this.listeners.get(event) || [])
61
+ if (x !== mine)
62
+ return true;
63
+ return false;
64
+ }
59
65
  emit(event, ...args) {
60
66
  let cbs = this.listeners.get(event);
61
67
  if (!cbs?.size)
package/src/index.js CHANGED
@@ -48,15 +48,16 @@ exports.app = new koa_1.default({ keys });
48
48
  exports.app.use(middlewares_1.sessionMiddleware)
49
49
  .use(selfCheck_1.selfCheckMiddleware)
50
50
  .use(acme_1.acmeMiddleware)
51
- .use(middlewares_1.prepareState)
51
+ .use(middlewares_1.prepareConnection)
52
+ .use(middlewares_1.blockFilter) // denied clients must not reach credential verification or its session/account side effects
52
53
  .use(geo_1.geoFilter)
54
+ .use(middlewares_1.prepareState)
53
55
  .use(ips_1.trackIpsMw)
54
56
  .use(middlewares_1.gzipper)
55
57
  .use(roots_1.rootsHostGuard)
56
58
  .use(log_1.logMw)
57
59
  .use(middlewares_1.someSecurity)
58
60
  .use(middlewares_1.paramsDecoder) // must be done before plugins, so they can manipulate params
59
- .use(middlewares_1.headRequests)
60
61
  .use(roots_1.rootsMiddleware)
61
62
  .use(throttler_1.throttler)
62
63
  .use(plugins_1.pluginsMiddleware)
package/src/lang.js CHANGED
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.normalizeLangCode = normalizeLangCode;
7
+ exports.langCodeForLookup = langCodeForLookup;
7
8
  exports.code2file = code2file;
8
9
  exports.file2code = file2code;
9
10
  exports.getLangData = getLangData;
@@ -22,6 +23,9 @@ function normalizeLangCode(code) {
22
23
  code = code.toLowerCase().split(';', 1)[0].trim();
23
24
  return /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/.test(code) ? code : '';
24
25
  }
26
+ function langCodeForLookup(code) {
27
+ return ['zh-tw', 'zh-hk', 'zh-mo'].includes(code) ? 'zh-hant' : code;
28
+ }
25
29
  function code2file(code, admin = false) {
26
30
  return (admin ? 'hfs-admin-lang-' : 'hfs-lang-') + code.toLowerCase() + SUFFIX;
27
31
  }
@@ -39,7 +43,7 @@ async function getLangData(ctxOrLangCsv) {
39
43
  }
40
44
  const csv = ctxOrLangCsv.toLowerCase();
41
45
  return cache.try(csv, async () => {
42
- const langs = csv.split(',').map(normalizeLangCode).filter(Boolean);
46
+ const langs = csv.split(',').map(normalizeLangCode).map(langCodeForLookup).filter(Boolean);
43
47
  if (!langs.includes(const_1.EMBEDDED_LANGUAGE))
44
48
  langs.push(const_1.EMBEDDED_LANGUAGE);
45
49
  const ret = {};
@@ -76,6 +80,7 @@ async function getLangData(ctxOrLangCsv) {
76
80
  let forceLangData;
77
81
  let undo;
78
82
  (0, config_1.defineConfig)(misc_1.CFG.force_lang, '', v => {
83
+ v = langCodeForLookup(v);
79
84
  undo?.();
80
85
  if (!v)
81
86
  return forceLangData = undefined;
@@ -12,7 +12,7 @@ const hfs_lang_sr_json_1 = __importDefault(require("./hfs-lang-sr.json"));
12
12
  const hfs_lang_sr_latn_json_1 = __importDefault(require("./hfs-lang-sr-latn.json"));
13
13
  const hfs_lang_ko_json_1 = __importDefault(require("./hfs-lang-ko.json"));
14
14
  const hfs_lang_ms_json_1 = __importDefault(require("./hfs-lang-ms.json"));
15
- const hfs_lang_zh_tw_json_1 = __importDefault(require("./hfs-lang-zh-tw.json"));
15
+ const hfs_lang_zh_hant_json_1 = __importDefault(require("./hfs-lang-zh-hant.json"));
16
16
  const hfs_lang_fr_json_1 = __importDefault(require("./hfs-lang-fr.json"));
17
17
  const hfs_lang_pl_json_1 = __importDefault(require("./hfs-lang-pl.json"));
18
18
  const hfs_lang_pt_br_json_1 = __importDefault(require("./hfs-lang-pt-br.json"));
@@ -30,4 +30,4 @@ const hfs_lang_uk_json_1 = __importDefault(require("./hfs-lang-uk.json"));
30
30
  const hfs_lang_ar_json_1 = __importDefault(require("./hfs-lang-ar.json"));
31
31
  const hfs_lang_lt_json_1 = __importDefault(require("./hfs-lang-lt.json"));
32
32
  const hfs_lang_bg_json_1 = __importDefault(require("./hfs-lang-bg.json"));
33
- exports.default = { en: hfs_lang_en_json_1.default, it: hfs_lang_it_json_1.default, zh: hfs_lang_zh_json_1.default, ru: hfs_lang_ru_json_1.default, sr: hfs_lang_sr_json_1.default, 'sr-latn': hfs_lang_sr_latn_json_1.default, ko: hfs_lang_ko_json_1.default, ms: hfs_lang_ms_json_1.default, 'zh-tw': hfs_lang_zh_tw_json_1.default, fr: hfs_lang_fr_json_1.default, pl: hfs_lang_pl_json_1.default, 'pt-br': hfs_lang_pt_br_json_1.default, vi: hfs_lang_vi_json_1.default, es: hfs_lang_es_json_1.default, nl: hfs_lang_nl_json_1.default, el: hfs_lang_el_json_1.default, de: hfs_lang_de_json_1.default, fi: hfs_lang_fi_json_1.default, hu: hfs_lang_hu_json_1.default, ja: hfs_lang_ja_json_1.default, ro: hfs_lang_ro_json_1.default, tr: hfs_lang_tr_json_1.default, th: hfs_lang_th_json_1.default, uk: hfs_lang_uk_json_1.default, ar: hfs_lang_ar_json_1.default, lt: hfs_lang_lt_json_1.default, bg: hfs_lang_bg_json_1.default };
33
+ exports.default = { en: hfs_lang_en_json_1.default, it: hfs_lang_it_json_1.default, zh: hfs_lang_zh_json_1.default, ru: hfs_lang_ru_json_1.default, sr: hfs_lang_sr_json_1.default, 'sr-latn': hfs_lang_sr_latn_json_1.default, ko: hfs_lang_ko_json_1.default, ms: hfs_lang_ms_json_1.default, 'zh-hant': hfs_lang_zh_hant_json_1.default, fr: hfs_lang_fr_json_1.default, pl: hfs_lang_pl_json_1.default, 'pt-br': hfs_lang_pt_br_json_1.default, vi: hfs_lang_vi_json_1.default, es: hfs_lang_es_json_1.default, nl: hfs_lang_nl_json_1.default, el: hfs_lang_el_json_1.default, de: hfs_lang_de_json_1.default, fi: hfs_lang_fi_json_1.default, hu: hfs_lang_hu_json_1.default, ja: hfs_lang_ja_json_1.default, ro: hfs_lang_ro_json_1.default, tr: hfs_lang_tr_json_1.default, th: hfs_lang_th_json_1.default, uk: hfs_lang_uk_json_1.default, ar: hfs_lang_ar_json_1.default, lt: hfs_lang_lt_json_1.default, bg: hfs_lang_bg_json_1.default };
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "author": "Hussein M. Alghazal (hmg3099)",
3
3
  "version": 1.0,
4
- "hfs_version": "0.57.5",
5
4
  "translate": {
6
5
  "Select": "اختر",
7
6
  "n_files": "{n,plural,one{# file} other{# files}}",
@@ -111,6 +110,8 @@
111
110
  "Timestamp": "الطابع الزمني",
112
111
  "Show": "عرض",
113
112
  "Loading failed": "فشل التحميل",
113
+ "Download limit reached": "تم بلوغ حد التنزيلات",
114
+ "Retry": "إعادة المحاولة",
114
115
  "Rename": "إعادة تسمية",
115
116
  "Tiles mode:": "وضع الشبكة:",
116
117
  "off": "إيقاف",
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "author": "Ангел Трънгалов",
3
3
  "version": 1.0,
4
- "hfs_version": "0.57.5",
5
4
  "translate": {
6
5
  "Select": "Избери",
7
6
  "n_files": "{n,plural,one{# файл} other{# файлове}}",
@@ -109,6 +108,8 @@
109
108
  "Timestamp": "Печат на време",
110
109
  "Show": "Покажи",
111
110
  "Loading failed": "Зареждането неуспешно",
111
+ "Download limit reached": "Достигнат е лимитът за изтегляния",
112
+ "Retry": "Опитай отново",
112
113
  "Rename": "Преименувай",
113
114
  "Tiles mode:": "Режим плочки:",
114
115
  "off": "изкл.",
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "author": "Uwe Wennmann",
3
3
  "version": 0.1,
4
- "hfs_version": "0.57.5",
5
4
  "translate": {
6
5
  "Select": "Auswählen",
7
6
  "n_files": "{n,plural,one{# Datei} other{# Dateien}}",
@@ -111,6 +110,8 @@
111
110
  "Timestamp": "Zeitstempel",
112
111
  "Show": "Anzeigen",
113
112
  "Loading failed": "Laden fehlgeschlagen",
113
+ "Download limit reached": "Download-Limit erreicht",
114
+ "Retry": "Erneut versuchen",
114
115
  "Rename": "Umbenennen",
115
116
  "Tiles mode:": "Kachel-Modus:",
116
117
  "off": "aus",
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "author": "Konstantinos (konmaz)",
3
3
  "version": 1.0,
4
- "hfs_version": "0.57.5",
5
4
  "translate": {
6
5
  "Select": "Επιλογή",
7
6
  "n_files": "{n,plural,one{# αρχείο} other{# αρχεία}}",
@@ -111,6 +110,8 @@
111
110
  "Timestamp": "Χρονική σήμανση",
112
111
  "Show": "Εμφάνιση",
113
112
  "Loading failed": "Η φόρτωση απέτυχε",
113
+ "Download limit reached": "Συμπληρώθηκε το όριο λήψεων",
114
+ "Retry": "Δοκιμή ξανά",
114
115
  "Rename": "Μετονομασία",
115
116
  "Tiles mode:": "Λειτουργία πλακιδίων:",
116
117
  "off": "ανενεργό",
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "author": "NAME OF THE AUTHOR(S) OF THIS TRANSLATION",
3
3
  "version": 1.0,
4
- "hfs_version": "0.57.5",
5
4
  "translate": {
6
5
  "Select": "Select",
7
6
  "n_files": "{n,plural,one{# file} other{# files}}",
@@ -112,6 +111,8 @@
112
111
  "Timestamp": "Timestamp",
113
112
  "Show": "Show",
114
113
  "Loading failed": "Loading failed",
114
+ "Download limit reached": "Download limit reached",
115
+ "Retry": "Retry",
115
116
  "Rename": "Rename",
116
117
  "Tiles mode:": "Tiles mode:",
117
118
  "off": "off",
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "author": "Eidy Estupiñan Varona, Arturo Fernández Rivas",
3
3
  "version": 1.3,
4
- "hfs_version": "0.57.5",
5
4
  "translate": {
6
5
  "Select": "Seleccionar",
7
6
  "n_files": "{n,plural,one{# archivo} other {# archivos}}",
@@ -111,6 +110,8 @@
111
110
  "Timestamp": "Fecha - Hora",
112
111
  "Show": "Visualizar",
113
112
  "Loading failed": "Carga fallida",
113
+ "Download limit reached": "Límite de descargas alcanzado",
114
+ "Retry": "Reintentar",
114
115
  "Rename": "Renombrar",
115
116
  "Tiles mode:": "Cantidad elementos",
116
117
  "off": "Apagado",