hfs 0.53.0 → 0.54.0-alpha3

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 (65) hide show
  1. package/admin/assets/index-CcUio6cm.css +1 -0
  2. package/admin/assets/index-DCsMhd8s.js +807 -0
  3. package/admin/assets/{sha512-rpja4qx0.js → sha512-VvQJXb6I.js} +1 -1
  4. package/admin/index.html +4 -2
  5. package/frontend/assets/index-legacy-vQymU_EM.js +60 -0
  6. package/frontend/assets/{sha512-legacy-pz5im6D1.js → sha512-legacy-BD2cMJIT.js} +1 -1
  7. package/frontend/index.html +2 -2
  8. package/package.json +8 -6
  9. package/plugins/antibrute/plugin.js +1 -1
  10. package/plugins/download-counter/plugin.js +0 -15
  11. package/plugins/list-uploader/public/main.js +3 -2
  12. package/src/acme.js +2 -2
  13. package/src/adminApis.js +23 -3
  14. package/src/api.accounts.js +1 -1
  15. package/src/api.auth.js +6 -4
  16. package/src/api.get_file_list.js +18 -9
  17. package/src/api.monitor.js +1 -1
  18. package/src/api.net.js +4 -1
  19. package/src/api.plugins.js +2 -0
  20. package/src/api.vfs.js +6 -4
  21. package/src/apiMiddleware.js +4 -1
  22. package/src/auth.js +7 -7
  23. package/src/basicWeb.js +1 -1
  24. package/src/block.js +13 -1
  25. package/src/commands.js +8 -2
  26. package/src/const.js +13 -10
  27. package/src/cross-const.js +2 -1
  28. package/src/cross.js +25 -8
  29. package/src/debounceAsync.js +3 -5
  30. package/src/dirStream.js +138 -0
  31. package/src/events.js +18 -8
  32. package/src/frontEndApis.js +32 -15
  33. package/src/github.js +21 -3
  34. package/src/langs/embedded.js +4 -1
  35. package/src/langs/hfs-lang-fi.json +5 -3
  36. package/src/langs/hfs-lang-hu.json +27 -19
  37. package/src/langs/hfs-lang-it.json +2 -1
  38. package/src/langs/hfs-lang-th.json +166 -0
  39. package/src/langs/hfs-lang-tr.json +166 -0
  40. package/src/langs/hfs-lang-uk.json +170 -0
  41. package/src/langs/hfs-lang-vi.json +5 -5
  42. package/src/listen.js +12 -4
  43. package/src/log.js +1 -1
  44. package/src/makeQ.js +31 -0
  45. package/src/middlewares.js +11 -8
  46. package/src/misc.js +2 -1
  47. package/src/nat.js +4 -4
  48. package/src/perm.js +1 -1
  49. package/src/persistence.js +2 -1
  50. package/src/plugins.js +22 -3
  51. package/src/serveFile.js +16 -14
  52. package/src/serveGuiAndSharedFiles.js +9 -4
  53. package/src/serveGuiFiles.js +8 -6
  54. package/src/srp.js +2 -2
  55. package/src/update.js +48 -7
  56. package/src/upload.js +33 -18
  57. package/src/util-files.js +7 -29
  58. package/src/vfs.js +23 -11
  59. package/src/watchLoad.js +1 -1
  60. package/src/zip.js +4 -4
  61. package/README.md +0 -232
  62. package/admin/assets/index-Ly9xr1F6.js +0 -811
  63. package/admin/assets/index-gNE0rRfC.css +0 -1
  64. package/frontend/assets/index-legacy-5MEl9EM8.js +0 -60
  65. /package/frontend/assets/{polyfills-legacy-31b7nyOi.js → polyfills-legacy-DMrMt_pQ.js} +0 -0
@@ -1,4 +1,4 @@
1
- System.register(["./index-legacy-5MEl9EM8.js"],(function(h,t){"use strict";var i,s;return{setters:[function(h){i=h.g,s=h.c}],execute:function(){function t(h,t){for(var i=function(){var i=t[s];if("string"!=typeof i&&!Array.isArray(i)){var r=function(t){if("default"!==t&&!(t in h)){var s=Object.getOwnPropertyDescriptor(i,t);s&&Object.defineProperty(h,t,s.get?s:{enumerable:!0,get:function(){return i[t]}})}};for(var e in i)r(e)}},s=0;s<t.length;s++)i();return Object.freeze(Object.defineProperty(h,Symbol.toStringTag,{value:"Module"}))}var r={exports:{}};
1
+ System.register(["./index-legacy-vQymU_EM.js"],(function(h,t){"use strict";var i,s;return{setters:[function(h){i=h.g,s=h.c}],execute:function(){function t(h,t){for(var i=function(){var i=t[s];if("string"!=typeof i&&!Array.isArray(i)){var r=function(t){if("default"!==t&&!(t in h)){var s=Object.getOwnPropertyDescriptor(i,t);s&&Object.defineProperty(h,t,s.get?s:{enumerable:!0,get:function(){return i[t]}})}};for(var e in i)r(e)}},s=0;s<t.length;s++)i();return Object.freeze(Object.defineProperty(h,Symbol.toStringTag,{value:"Module"}))}var r={exports:{}};
2
2
  /*
3
3
  * [js-sha512]{@link https://github.com/emn178/js-sha512}
4
4
  *
@@ -8,7 +8,7 @@
8
8
  </head>
9
9
  <body>
10
10
  <div id="root">Wait for loading or <a href="?get=basic">use basic interface now</a></div>
11
- <script crossorigin id="vite-legacy-polyfill" src="/assets/polyfills-legacy-31b7nyOi.js"></script>
12
- <script crossorigin id="vite-legacy-entry" data-src="/assets/index-legacy-5MEl9EM8.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
11
+ <script crossorigin id="vite-legacy-polyfill" src="/assets/polyfills-legacy-DMrMt_pQ.js"></script>
12
+ <script crossorigin id="vite-legacy-entry" data-src="/assets/index-legacy-vQymU_EM.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
13
13
  </body>
14
14
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.53.0",
3
+ "version": "0.54.0-alpha3",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -59,6 +59,7 @@
59
59
  "central.json",
60
60
  "admin/**/*",
61
61
  "frontend/**/*",
62
+ "**/node_modules/fswin/x64/*",
62
63
  "**/node_modules/axios/dist/node/*"
63
64
  ],
64
65
  "targets": [
@@ -69,15 +70,16 @@
69
70
  ]
70
71
  },
71
72
  "dependencies": {
72
- "@koa/router": "^12.0.1",
73
- "@node-rs/crc32": "^1.6.0",
73
+ "@koa/router": "^13.0.1",
74
+ "@node-rs/crc32": "^1.10.3",
74
75
  "@rejetto/kvstorage": "^0.12.2",
75
- "acme-client": "^5.3.1",
76
- "buffer-crc32": "^0.2.13",
76
+ "acme-client": "^5.4.0",
77
+ "buffer-crc32": "^1.0.0",
77
78
  "fast-glob": "^3.2.7",
78
79
  "find-process": "^1.4.7",
79
80
  "formidable": "^3.5.1",
80
81
  "fs-x-attributes": "^1.0.2",
82
+ "fswin": "^3.24.829",
81
83
  "iconv-lite": "^0.6.3",
82
84
  "ip2location-nodejs": "^9.6.0",
83
85
  "koa": "^2.13.4",
@@ -92,7 +94,7 @@
92
94
  "qr-creator": "^1.0.0",
93
95
  "picomatch": "^3.0.1",
94
96
  "tssrp6a": "^3.0.0",
95
- "unzip-stream": "^0.3.1",
97
+ "unzip-stream": "^0.3.4",
96
98
  "valtio": "^1.10.3",
97
99
  "yaml": "^2.0.0-10"
98
100
  },
@@ -19,7 +19,7 @@ exports.init = api => {
19
19
  const { block } = api.require('./block')
20
20
  return {
21
21
  unload: api.events.multi({
22
- attemptingLogin: async ctx => {
22
+ attemptingLogin: async ctx => {
23
23
  const { ip } = ctx
24
24
  const now = new Date
25
25
  const rec = getOrSet(byIp, ip, () => ({ attempts: 0, next: now }))
@@ -16,21 +16,6 @@ exports.configDialog = {
16
16
 
17
17
  exports.init = async api => {
18
18
  const db = await api.openDb('counters.kv', { defaultPutDelay: 5_000, maxPutDelay: 30_000 })
19
-
20
- try { // load legacy file
21
- const countersFile = 'counters.yaml'
22
- const yaml = api.require('yaml')
23
- const { readFile, unlink } = api.require('fs/promises')
24
- const data = await readFile(countersFile, 'utf8')
25
- for (const [k,v] of Object.entries(yaml.parse(data)))
26
- db.put(uri2key(k), v)
27
- await unlink(countersFile)
28
- api.log("data converted")
29
- }
30
- catch(err) {
31
- if (err.code !== 'ENOENT')
32
- api.log(err)
33
- }
34
19
  return {
35
20
  frontend_js: 'main.js',
36
21
  frontend_css: 'style.css',
@@ -10,10 +10,11 @@
10
10
  const text = React.useMemo(() => {
11
11
  if (!data || data === true) return ''
12
12
  const { upload: x } = data
13
+ const shouldShowIpWithUser = display === 'ip+user' && x.ip || display === 'tooltip' && HFS.state.adminUrl
13
14
  return !x ? ''
14
15
  : display === 'user' ? x.username
15
- : display === 'ip' || !x.username ? x.ip
16
- : x.ip + ' (' + x.username + ')'
16
+ : display === 'ip' || !x.username && shouldShowIpWithUser ? x.ip
17
+ : shouldShowIpWithUser ? x.ip + ' (' + x.username + ')' : (x.username || ' ')
17
18
  }, [data])
18
19
  const iconOnly = display === 'tooltip'
19
20
  return text && HFS.h('span', { className: 'uploader', title: HFS.t`Uploader` + (iconOnly ? ' ' + text : '') },
package/src/acme.js CHANGED
@@ -113,7 +113,7 @@ exports.makeCert = (0, debounceAsync_1.debounceAsync)(async (domain, email, altN
113
113
  await promises_1.default.writeFile(KEY_FILE, res.key);
114
114
  listen_1.cert.set(CERT_FILE); // update config
115
115
  listen_1.privateKey.set(KEY_FILE);
116
- }, 0);
116
+ });
117
117
  const acmeDomain = (0, config_1.defineConfig)('acme_domain', '');
118
118
  const acmeEmail = (0, config_1.defineConfig)('acme_email', '');
119
119
  const acmeRenew = (0, config_1.defineConfig)('acme_renew', false); // handle config changes
@@ -133,4 +133,4 @@ const renewCert = (0, debounceAsync_1.debounceAsync)(async () => {
133
133
  return console.log("certificate still good");
134
134
  await (0, exports.makeCert)(domain, acmeEmail.get(), altNames)
135
135
  .catch(e => console.log("error renewing certificate: ", String(e.message || e)));
136
- }, 0, { retain: misc_1.DAY, retainFailure: misc_1.HOUR });
136
+ }, { retain: misc_1.DAY, retainFailure: misc_1.HOUR });
package/src/adminApis.js CHANGED
@@ -32,6 +32,8 @@ const geo_1 = require("./geo");
32
32
  const roots_1 = require("./roots");
33
33
  const SendList_1 = require("./SendList");
34
34
  const ddns_1 = require("./ddns");
35
+ const block_1 = require("./block");
36
+ const github_1 = require("./github");
35
37
  exports.adminApis = {
36
38
  ...api_vfs_1.default,
37
39
  ...api_accounts_1.default,
@@ -71,6 +73,10 @@ exports.adminApis = {
71
73
  async check_update() {
72
74
  return { options: await (0, update_1.getUpdates)() };
73
75
  },
76
+ async wait_project_info() {
77
+ await (0, github_1.getProjectInfo)();
78
+ return {};
79
+ },
74
80
  async ip_country({ ips }) {
75
81
  const res = await Promise.allSettled(ips.map(geo_1.ip2country));
76
82
  return {
@@ -107,7 +113,11 @@ exports.adminApis = {
107
113
  baseUrl: await (0, listen_1.getBaseUrlOrDefault)(),
108
114
  roots: roots_1.roots.get(),
109
115
  updatePossible: !await (0, update_1.updateSupported)() ? false : (await (0, update_1.localUpdateAvailable)()) ? 'local' : true,
116
+ autoCheckUpdateResult: update_1.autoCheckUpdateResult.get(),
117
+ alerts: github_1.alerts.get(),
110
118
  proxyDetected: (0, middlewares_1.getProxyDetected)(),
119
+ cloudflareDetected: middlewares_1.cloudflareDetected,
120
+ ram: process.memoryUsage.rss(),
111
121
  frpDetected: exports.localhostAdmin.get() && !(0, middlewares_1.getProxyDetected)()
112
122
  && (0, connections_1.getConnections)().every(misc_1.isLocalHost)
113
123
  && await frpDebounced(),
@@ -121,9 +131,19 @@ exports.adminApis = {
121
131
  await (0, promises_1.writeFile)(files.cert, cert);
122
132
  return files;
123
133
  },
134
+ async add_block({ merge, ip, expire, comment }) {
135
+ (0, misc_1.apiAssertTypes)({
136
+ string: { ip },
137
+ string_undefined: { comment, expire },
138
+ object_undefined: { merge },
139
+ });
140
+ const optionals = lodash_1.default.pickBy({ expire, comment }, v => v !== undefined); // passing undefined-s would override values in merge
141
+ (0, block_1.addBlock)({ ip, ...optionals }, merge);
142
+ return {};
143
+ }
124
144
  };
125
- for (const [k, was] of Object.entries(exports.adminApis))
126
- exports.adminApis[k] = (params, ctx) => {
145
+ for (const [k, was] of (0, misc_1.typedEntries)(exports.adminApis))
146
+ exports.adminApis[k] = ((params, ctx) => {
127
147
  if (!allowAdmin(ctx))
128
148
  return new apiMiddleware_1.ApiError(const_1.HTTP_FORBIDDEN);
129
149
  if (ctxAdminAccess(ctx))
@@ -132,7 +152,7 @@ for (const [k, was] of Object.entries(exports.adminApis))
132
152
  return ctx.headers.accept === 'text/event-stream'
133
153
  ? new SendList_1.SendListReadable({ doAtStart: x => x.error(const_1.HTTP_UNAUTHORIZED, true, props) })
134
154
  : new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, props);
135
- };
155
+ });
136
156
  exports.localhostAdmin = (0, config_1.defineConfig)('localhost_admin', true);
137
157
  exports.adminNet = (0, config_1.defineConfig)('admin_net', '', v => (0, misc_1.makeNetMatcher)(v, true));
138
158
  exports.favicon = (0, config_1.defineConfig)('favicon', '');
@@ -28,7 +28,7 @@ exports.default = {
28
28
  || new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND);
29
29
  },
30
30
  get_accounts() {
31
- return { list: Object.values(perm_1.accountsConfig.get()).map(prepareAccount) };
31
+ return { list: (0, misc_1.onlyTruthy)(Object.values(perm_1.accountsConfig.get()).map(prepareAccount)) };
32
32
  },
33
33
  get_admins() {
34
34
  return { list: lodash_1.default.filter(perm_1.accountsConfig.get(), perm_1.accountCanLoginAdmin).map(ac => ac.username) };
package/src/api.auth.js CHANGED
@@ -21,16 +21,16 @@ const loginSrp1 = async ({ username }, ctx) => {
21
21
  const account = (0, perm_1.getAccount)(username);
22
22
  if (!ctx.session)
23
23
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR);
24
- await events_1.default.emitAsync('attemptingLogin', ctx);
24
+ await events_1.default.emitAsync('attemptingLogin', { ctx, username });
25
25
  if (!account || !(0, perm_1.accountCanLogin)(account)) { // TODO simulate fake account to prevent knowing valid usernames
26
26
  ctx.logExtra({ u: username });
27
27
  ctx.state.dontLog = false; // log even if log_api is false
28
28
  return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED);
29
29
  }
30
30
  try {
31
- const { step1, ...rest } = await (0, auth_1.srpStep1)(account);
31
+ const { srpServer, ...rest } = await (0, auth_1.srpServerStep1)(account);
32
32
  const sid = Math.random();
33
- ongoingLogins[sid] = step1;
33
+ ongoingLogins[sid] = srpServer;
34
34
  setTimeout(() => delete ongoingLogins[sid], 60000);
35
35
  ctx.session.loggingIn = { username, sid }; // temporarily store until process is complete
36
36
  return rest;
@@ -81,8 +81,10 @@ const logout = async ({}, ctx) => {
81
81
  exports.logout = logout;
82
82
  const refresh_session = async ({}, ctx) => {
83
83
  var _a;
84
+ const username = (0, auth_1.getCurrentUsername)(ctx);
84
85
  return !ctx.session ? new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR) : {
85
- username: (0, auth_1.getCurrentUsername)(ctx),
86
+ username,
87
+ expandedUsername: (0, perm_1.expandUsername)(username),
86
88
  adminUrl: (0, adminApis_1.ctxAdminAccess)(ctx) ? ctx.state.revProxyPath + const_1.ADMIN_URI : undefined,
87
89
  canChangePassword: canChangePassword(ctx.state.account),
88
90
  exp: keepSessionAlive.get() ? new Date(Date.now() + middlewares_1.sessionDuration.compiled()) : undefined,
@@ -14,7 +14,8 @@ const connections_1 = require("./connections");
14
14
  const adminApis_1 = require("./adminApis");
15
15
  const upload_1 = require("./upload");
16
16
  const SendList_1 = require("./SendList");
17
- const get_file_list = async ({ uri = '/', offset, limit, search, c, onlyFolders, admin }, ctx) => {
17
+ const get_file_list = async ({ uri = '/', offset, limit, search, wild, c, onlyFolders, admin }, ctx) => {
18
+ var _a;
18
19
  const node = await (0, vfs_1.urlToNode)(uri, ctx);
19
20
  const list = ctx.get('accept') === 'text/event-stream' ? new SendList_1.SendListReadable() : undefined;
20
21
  if (!node)
@@ -29,17 +30,19 @@ const get_file_list = async ({ uri = '/', offset, limit, search, c, onlyFolders,
29
30
  return fail();
30
31
  offset = Number(offset);
31
32
  limit = Number(limit);
32
- const filter = (0, misc_1.pattern2filter)(String(search || ''));
33
+ search = String(search || '').toLocaleLowerCase();
34
+ const filter = wild === 'no' ? (s) => s.includes(search)
35
+ : (0, misc_1.pattern2filter)(search);
33
36
  const walker = (0, vfs_1.walkNode)(node, { ctx: admin ? undefined : ctx, onlyFolders, depth: search ? Infinity : 0 });
34
37
  const onDirEntryHandlers = (0, plugins_1.mapPlugins)(plug => plug.onDirEntry);
35
38
  const can_upload = admin || (0, vfs_1.hasPermission)(node, 'can_upload', ctx);
36
- const fakeChild = await (0, vfs_1.applyParentToChild)(undefined, node); // can we delete children
39
+ const fakeChild = await (0, vfs_1.applyParentToChild)({ source: 'x' }, node); // can we delete children
37
40
  const can_delete = admin || (0, vfs_1.hasPermission)(fakeChild, 'can_delete', ctx);
38
- const can_archive = admin || (0, vfs_1.hasPermission)(node, 'can_archive', ctx);
41
+ const can_archive = admin || (0, vfs_1.hasPermission)(fakeChild, 'can_archive', ctx);
39
42
  const can_comment = can_upload && (0, comments_1.areCommentsEnabled)();
40
43
  const can_overwrite = can_upload && (can_delete || !upload_1.dontOverwriteUploading.get());
41
- const comment = await (0, comments_1.getCommentFor)(node.source);
42
- const props = { can_archive, can_upload, can_delete, can_overwrite, can_comment, comment, accept: node.accept };
44
+ const comment = (_a = node.comment) !== null && _a !== void 0 ? _a : await (0, comments_1.getCommentFor)(node.source);
45
+ const props = { can_archive, can_upload, can_delete, can_overwrite, can_comment, comment, accept: node.accept, icon: getNodeIcon(node) };
43
46
  ctx.state.browsing = uri.replace(/\/{2,}/g, '/');
44
47
  (0, connections_1.updateConnectionForCtx)(ctx);
45
48
  if (!list)
@@ -94,14 +97,19 @@ const get_file_list = async ({ uri = '/', offset, limit, search, c, onlyFolders,
94
97
  break;
95
98
  }
96
99
  }
100
+ function getNodeIcon(node) {
101
+ var _a;
102
+ return ((_a = node.icon) === null || _a === void 0 ? void 0 : _a.includes('.')) || node.icon; // true = specific for this file, otherwise is a SYS_ICONS
103
+ }
97
104
  async function nodeToDirEntry(ctx, node) {
105
+ var _a;
98
106
  const { source, url } = node;
99
107
  const name = (0, vfs_1.getNodeName)(node);
100
108
  if (url)
101
109
  return name ? { n: name, url, target: node.target } : null;
102
110
  const isFolder = await (0, vfs_1.nodeIsDirectory)(node);
103
111
  try {
104
- const st = source ? await (0, promises_1.stat)(source) : undefined;
112
+ const st = source ? node.stats || await (0, promises_1.stat)(source) : undefined;
105
113
  const pl = node.can_list === misc_1.WHO_NO_ONE ? 'l'
106
114
  : !(0, vfs_1.hasPermission)(node, 'can_list', ctx) ? 'L'
107
115
  : '';
@@ -117,11 +125,12 @@ const get_file_list = async ({ uri = '/', offset, limit, search, c, onlyFolders,
117
125
  m: !st || Math.abs(+st.mtime - +st.ctime) < 1000 ? undefined : st.mtime,
118
126
  s: isFolder ? undefined : st === null || st === void 0 ? void 0 : st.size,
119
127
  p: (pr + pl + pd + pa) || undefined,
120
- comment: await (0, comments_1.getCommentFor)(source),
128
+ comment: (_a = node.comment) !== null && _a !== void 0 ? _a : await (0, comments_1.getCommentFor)(source),
129
+ icon: getNodeIcon(node),
121
130
  web: await (0, vfs_1.hasDefaultFile)(node, ctx) ? true : undefined,
122
131
  };
123
132
  }
124
- catch (_a) {
133
+ catch (_b) {
125
134
  return null;
126
135
  }
127
136
  function filesInsideCould(n = node) {
@@ -71,7 +71,7 @@ exports.default = {
71
71
  ...s.browsing ? { op: 'browsing', path: decodeURIComponent(s.browsing) }
72
72
  : s.uploadPath ? { op: 'upload', path: decodeURIComponent(s.uploadPath) }
73
73
  : {
74
- op: !s.considerAsGui && s.op || undefined,
74
+ op: !s.considerAsGui && (ctx.state.archive || ctx.state.vfsNode) ? 'download' : undefined,
75
75
  path: (0, misc_1.try_)(() => decodeURIComponent(ctx.originalUrl), () => ctx.originalUrl),
76
76
  },
77
77
  opProgress: lodash_1.default.isNumber(s.opProgress) ? lodash_1.default.round(s.opProgress, 3) : undefined,
package/src/api.net.js CHANGED
@@ -57,7 +57,10 @@ const apis = {
57
57
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, 'removeMapping failed: ' + String(e));
58
58
  }
59
59
  if (external) // must use the object form of 'public' to work around a bug of the library
60
- await nat_1.upnpClient.createMapping({ private: internal || internalPort, public: { host: '', port: external }, description: 'hfs', ttl: 0 });
60
+ await nat_1.upnpClient.createMapping({ private: internal || internalPort, public: { host: '', port: external }, description: 'hfs', ttl: 0 })
61
+ .catch(res => {
62
+ throw new apiMiddleware_1.ApiError(res.errorCode, res.errorCode === 718 ? "Port not available" : res.errorDescription);
63
+ });
61
64
  return {};
62
65
  },
63
66
  async self_check({ url }) {
@@ -43,6 +43,8 @@ const apis = {
43
43
  if (online.version !== disk.version) { // not just newer one, in case a version was retired
44
44
  online.id = disk.id; // id is installation-dependant, and online cannot know
45
45
  online.repo = serialize(disk).repo; // show the user the current repo we are getting this update from, not a possibly-changed future one
46
+ if (online.version < disk.version)
47
+ online.downgrade = true;
46
48
  list.add(online);
47
49
  }
48
50
  }
package/src/api.vfs.js CHANGED
@@ -21,14 +21,14 @@ async function urlToNodeOriginal(uri) {
21
21
  const n = await (0, vfs_1.urlToNode)(uri);
22
22
  return (n === null || n === void 0 ? void 0 : n.isTemp) ? n.original : n;
23
23
  }
24
- const ALLOWED_KEYS = ['name', 'source', 'masks', 'default', 'accept', 'rename', 'mime', 'url', 'target', ...misc_1.PERM_KEYS];
24
+ const ALLOWED_KEYS = ['name', 'source', 'masks', 'default', 'accept', 'rename', 'mime', 'url', 'target', 'comment', 'icon', ...misc_1.PERM_KEYS];
25
25
  const apis = {
26
26
  async get_vfs() {
27
27
  return { root: await recur() };
28
28
  async function recur(node = vfs_1.vfs) {
29
29
  var _a, _b, _c;
30
30
  const { source } = node;
31
- const stats = !source ? undefined : await (0, promises_1.stat)(source).catch(() => undefined);
31
+ const stats = !source ? undefined : (node.stats || await (0, promises_1.stat)(source).catch(() => undefined));
32
32
  const isDir = !(0, vfs_1.nodeIsLink)(node) && (!source || ((_a = stats === null || stats === void 0 ? void 0 : stats.isDirectory()) !== null && _a !== void 0 ? _a : ((_b = node.children) === null || _b === void 0 ? void 0 : _b.length) > 0));
33
33
  const copyStats = stats ? lodash_1.default.pick(stats, ['size', 'ctime', 'mtime'])
34
34
  : { size: source ? -1 : undefined };
@@ -188,14 +188,16 @@ const apis = {
188
188
  try {
189
189
  const matching = (0, misc_1.makeMatcher)(fileMask);
190
190
  path = (0, misc_1.isWindowsDrive)(path) ? path + '\\' : (0, path_1.resolve)(path || '/');
191
- for await (const [name, isDir] of (0, misc_1.dirStream)(path)) {
191
+ for await (const entry of (0, misc_1.dirStream)(path)) {
192
192
  if (ctx.req.aborted)
193
193
  return;
194
+ const { path: name } = entry;
195
+ const isDir = entry.isDirectory();
194
196
  if (!isDir)
195
197
  if (!files || fileMask && !matching(name))
196
198
  continue;
197
199
  try {
198
- const stats = await (0, promises_1.stat)((0, path_1.join)(path, name));
200
+ const stats = entry.stats || await (0, promises_1.stat)((0, path_1.join)(path, name));
199
201
  list.add({
200
202
  n: name,
201
203
  s: stats.size,
@@ -10,6 +10,7 @@ const stream_1 = require("stream");
10
10
  const misc_1 = require("./misc");
11
11
  const const_1 = require("./const");
12
12
  const config_1 = require("./config");
13
+ const plugins_1 = require("./plugins");
13
14
  class ApiError extends Error {
14
15
  constructor(status, message) {
15
16
  super(typeof message === 'string' ? message : message && message instanceof Error ? message.message : JSON.stringify(message));
@@ -32,7 +33,9 @@ function apiMiddleware(apis) {
32
33
  || apiName.startsWith('get_'); // "get_" apis are safe because they make no change
33
34
  if (!safe)
34
35
  return send(const_1.HTTP_FOOL, "missing header x-hfs-anti-csrf=1");
35
- const apiFun = apis.hasOwnProperty(apiName) && apis[apiName];
36
+ const customApiRest = apiName.startsWith(const_1.PLUGIN_CUSTOM_REST_PREFIX) && apiName.slice(const_1.PLUGIN_CUSTOM_REST_PREFIX.length);
37
+ const apiFun = customApiRest && (0, plugins_1.firstPlugin)(pl => { var _a; return (_a = pl.getData().customRest) === null || _a === void 0 ? void 0 : _a[customApiRest]; })
38
+ || apis.hasOwnProperty(apiName) && apis[apiName];
36
39
  if (!apiFun)
37
40
  return send(const_1.HTTP_NOT_FOUND, 'invalid api');
38
41
  // we don't rely on SameSite cookie option because it's https-only
package/src/auth.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.invalidateSessionBefore = exports.setLoggedIn = exports.getCurrentUsername = exports.srpCheck = exports.srpStep1 = void 0;
6
+ exports.invalidateSessionBefore = exports.setLoggedIn = exports.getCurrentUsername = exports.srpCheck = exports.srpServerStep1 = void 0;
7
7
  const perm_1 = require("./perm");
8
8
  const cross_const_1 = require("./cross-const");
9
9
  const tssrp6a_1 = require("tssrp6a");
@@ -12,17 +12,17 @@ const cross_1 = require("./cross");
12
12
  const node_crypto_1 = require("node:crypto");
13
13
  const events_1 = __importDefault(require("./events"));
14
14
  const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
15
- async function srpStep1(account) {
15
+ async function srpServerStep1(account) {
16
16
  if (!account.srp)
17
17
  throw cross_const_1.HTTP_NOT_ACCEPTABLE;
18
18
  const [salt, verifier] = account.srp.split('|');
19
19
  if (!salt || !verifier)
20
20
  throw Error("malformed account");
21
21
  const srpSession = new tssrp6a_1.SRPServerSession(srp6aNimbusRoutines);
22
- const step1 = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier));
23
- return { step1, salt, pubKey: String(step1.B) }; // cast to string cause bigint can't be jsonized
22
+ const srpServer = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier));
23
+ return { srpServer, salt, pubKey: String(srpServer.B) }; // cast to string cause bigint can't be jsonized
24
24
  }
25
- exports.srpStep1 = srpStep1;
25
+ exports.srpServerStep1 = srpServerStep1;
26
26
  const cache = {};
27
27
  async function srpCheck(username, password) {
28
28
  const account = (0, perm_1.getAccount)(username);
@@ -30,10 +30,10 @@ async function srpCheck(username, password) {
30
30
  return;
31
31
  const k = (0, node_crypto_1.createHash)('sha256').update(username + password + account.srp).digest("hex");
32
32
  const good = await (0, cross_1.getOrSet)(cache, k, async () => {
33
- const { step1, salt, pubKey } = await srpStep1(account);
33
+ const { srpServer, salt, pubKey } = await srpServerStep1(account);
34
34
  const client = await (0, srp_1.srpClientPart)(username, password, salt, pubKey);
35
35
  setTimeout(() => delete cache[k], 60000);
36
- return step1.step2(client.A, client.M1).then(() => 1, () => 0);
36
+ return srpServer.step2(client.A, client.M1).then(() => 1, () => 0);
37
37
  });
38
38
  return good ? account : undefined;
39
39
  }
package/src/basicWeb.js CHANGED
@@ -46,7 +46,7 @@ function basicWeb(ctx, node) {
46
46
  stream.push(`<title>${adminApis_1.title.get()}</title><body>`);
47
47
  stream.push((0, customHtml_1.getSection)('basicHeader'));
48
48
  const u = (0, auth_1.getCurrentUsername)(ctx);
49
- const links = u ? { [`//LOGOUT%00:@${ctx.get('host')}/?get=logout`]: `Logout (${u})` } : { '/?get=login': "Login" };
49
+ const links = u ? { [`//LOGOUT%00:@${ctx.host}/?get=logout`]: `Logout (${u})` } : { '/?get=login': "Login" };
50
50
  stream.push(lodash_1.default.map(links, (v, k) => a(k, v)).join(' ') + '\n<ul>\n');
51
51
  if (ctx.state.originalPath.length > 1)
52
52
  stream.push('<li>' + a('..' + force, '..') + '\n');
package/src/block.js CHANGED
@@ -1,10 +1,14 @@
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
+ };
3
6
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.applyBlock = exports.block = void 0;
7
+ exports.addBlock = exports.applyBlock = exports.block = void 0;
5
8
  const config_1 = require("./config");
6
9
  const connections_1 = require("./connections");
7
10
  const misc_1 = require("./misc");
11
+ const lodash_1 = __importDefault(require("lodash"));
8
12
  exports.block = (0, config_1.defineConfig)('block', [], rules => {
9
13
  const now = new Date();
10
14
  const ret = !Array.isArray(rules) ? []
@@ -31,3 +35,11 @@ setInterval(() => {
31
35
  console.log("blocking rules:", n, "expired");
32
36
  exports.block.set(next);
33
37
  }, misc_1.MINUTE / 2);
38
+ function addBlock(rule, merge) {
39
+ exports.block.set(was => {
40
+ const foundIdx = merge ? lodash_1.default.findIndex(was, merge) : -1;
41
+ return foundIdx < 0 ? [...was, { ...merge, ...rule }]
42
+ : was.map((x, i) => i === foundIdx ? { ...x, ...rule, ip: `${x.ip}|${rule.ip}` } : x);
43
+ });
44
+ }
45
+ exports.addBlock = addBlock;
package/src/commands.js CHANGED
@@ -33,8 +33,11 @@ if (!const_1.argv.updating)
33
33
  function parseCommandLine(line) {
34
34
  if (!line)
35
35
  return;
36
- const [name, ...params] = line.trim().split(/ +/);
37
- const cmd = commands[name];
36
+ let [name, ...params] = line.trim().split(/ +/);
37
+ name = aliases[name] || name;
38
+ let cmd = commands[name];
39
+ if (cmd.alias)
40
+ cmd = commands[cmd.alias];
38
41
  if (!cmd)
39
42
  return console.error("cannot understand entered command, try 'help'");
40
43
  if (cmd.cb.length > params.length)
@@ -45,6 +48,9 @@ function parseCommandLine(line) {
45
48
  console.error("command failed:", err.message || err);
46
49
  });
47
50
  }
51
+ const aliases = {
52
+ exit: 'quit',
53
+ };
48
54
  const commands = {
49
55
  help: {
50
56
  params: '',
package/src/const.js CHANGED
@@ -37,7 +37,7 @@ const os_1 = require("os");
37
37
  const fs_1 = require("fs");
38
38
  const path_1 = require("path");
39
39
  __exportStar(require("./cross-const"), exports);
40
- exports.API_VERSION = 8.891;
40
+ exports.API_VERSION = 9.2;
41
41
  exports.COMPATIBLE_API_VERSION = 1; // while changes in the api are not breaking, this number stays the same, otherwise it is made equal to API_VERSION
42
42
  exports.HFS_REPO = 'rejetto/hfs';
43
43
  exports.argv = (0, minimist_1.default)(process.argv.slice(2));
@@ -45,15 +45,15 @@ exports.DEV = process.env.DEV || exports.argv.dev ? 'DEV' : '';
45
45
  exports.ORIGINAL_CWD = process.cwd();
46
46
  exports.HFS_STARTED = new Date();
47
47
  const PKG_PATH = (0, path_1.join)(__dirname, '..', 'package.json');
48
- exports.BUILD_TIMESTAMP = "2024-07-19T14:05:43.478Z";
48
+ exports.BUILD_TIMESTAMP = "2024-09-28T14:50:14.403Z";
49
49
  const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
50
50
  exports.VERSION = pkg.version;
51
51
  exports.RUNNING_BETA = exports.VERSION.includes('-');
52
52
  exports.HFS_REPO_BRANCH = exports.RUNNING_BETA ? exports.VERSION.split('.')[1] : 'main';
53
53
  exports.IS_WINDOWS = process.platform === 'win32';
54
54
  exports.IS_MAC = process.platform === 'darwin';
55
- exports.IS_BINARY = !(0, path_1.basename)(process.execPath).includes('node'); // this won't be node if pkg was used
56
- exports.APP_PATH = (0, path_1.dirname)(exports.IS_BINARY ? process.execPath : __dirname);
55
+ exports.IS_BINARY = !/node|bun/.test((0, path_1.basename)(process.execPath)); // this won't be node if pkg was used
56
+ exports.APP_PATH = (0, path_1.dirname)(exports.IS_BINARY ? process.execPath : __dirname); // __dirname's parent can be compared with cwd
57
57
  exports.MIME_AUTO = 'auto';
58
58
  // we want this to be the first stuff to be printed, then we print it in this module, that is executed at the beginning
59
59
  if (exports.DEV)
@@ -66,10 +66,10 @@ console.log('started', exports.HFS_STARTED.toLocaleString(), exports.DEV);
66
66
  console.log('version', exports.VERSION || '-');
67
67
  console.log('build', exports.BUILD_TIMESTAMP || '-');
68
68
  const winExe = exports.IS_WINDOWS && process.execPath.match(/(?<!node)\.exe$/i);
69
- if (exports.argv.cwd)
70
- process.chdir(exports.argv.cwd);
71
- else if (!winExe) { // still considering whether to use this behavior with Windows users, who may be less accustomed to it
72
- const dir = (0, path_1.join)((0, os_1.homedir)(), '.hfs');
69
+ // still considering whether to use ".hfs" with Windows users, who may be less accustomed to it
70
+ const useHomeDir = !winExe || process.execPath.includes(process.env.ProgramFiles || '|'); // you can't write in program-files
71
+ const dir = exports.argv.cwd || useHomeDir && (0, path_1.join)((0, os_1.homedir)(), '.hfs');
72
+ if (dir) {
73
73
  try {
74
74
  (0, fs_1.mkdirSync)(dir);
75
75
  }
@@ -79,9 +79,12 @@ else if (!winExe) { // still considering whether to use this behavior with Windo
79
79
  }
80
80
  process.chdir(dir);
81
81
  }
82
- console.log('cwd', process.cwd());
82
+ console.log('working directory', process.cwd());
83
83
  if (exports.APP_PATH !== process.cwd())
84
84
  console.log('app', exports.APP_PATH);
85
85
  console.log('node', process.version);
86
- console.log('platform', process.platform, process.arch);
86
+ const bun = globalThis.Bun;
87
+ if (bun)
88
+ console.log('bun', bun.version);
89
+ console.log('platform', process.platform, exports.IS_BINARY ? 'binary' : (0, path_1.basename)(process.execPath));
87
90
  console.log('pid', process.pid);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HTTP_MESSAGES = exports.HTTP_SERVICE_UNAVAILABLE = exports.HTTP_SERVER_ERROR = exports.HTTP_TOO_MANY_REQUESTS = exports.HTTP_FAILED_DEPENDENCY = exports.HTTP_FOOL = exports.HTTP_RANGE_NOT_SATISFIABLE = exports.HTTP_PAYLOAD_TOO_LARGE = exports.HTTP_PRECONDITION_FAILED = exports.HTTP_CONFLICT = exports.HTTP_NOT_ACCEPTABLE = exports.HTTP_METHOD_NOT_ALLOWED = exports.HTTP_NOT_FOUND = exports.HTTP_FORBIDDEN = exports.HTTP_UNAUTHORIZED = exports.HTTP_BAD_REQUEST = exports.HTTP_NOT_MODIFIED = exports.HTTP_TEMPORARY_REDIRECT = exports.HTTP_MOVED_PERMANENTLY = exports.HTTP_PARTIAL_CONTENT = exports.HTTP_NO_CONTENT = exports.HTTP_OK = exports.NBSP = exports.PORT_DISABLED = exports.PLUGINS_PUB_URI = exports.API_URI = exports.ADMIN_URI = exports.FRONTEND_URI = exports.SPECIAL_URI = void 0;
3
+ exports.HTTP_MESSAGES = exports.HTTP_SERVICE_UNAVAILABLE = exports.HTTP_SERVER_ERROR = exports.HTTP_TOO_MANY_REQUESTS = exports.HTTP_FAILED_DEPENDENCY = exports.HTTP_FOOL = exports.HTTP_RANGE_NOT_SATISFIABLE = exports.HTTP_PAYLOAD_TOO_LARGE = exports.HTTP_PRECONDITION_FAILED = exports.HTTP_CONFLICT = exports.HTTP_NOT_ACCEPTABLE = exports.HTTP_METHOD_NOT_ALLOWED = exports.HTTP_NOT_FOUND = exports.HTTP_FORBIDDEN = exports.HTTP_UNAUTHORIZED = exports.HTTP_BAD_REQUEST = exports.HTTP_NOT_MODIFIED = exports.HTTP_TEMPORARY_REDIRECT = exports.HTTP_MOVED_PERMANENTLY = exports.HTTP_PARTIAL_CONTENT = exports.HTTP_NO_CONTENT = exports.HTTP_OK = exports.PLUGIN_CUSTOM_REST_PREFIX = exports.NBSP = exports.PORT_DISABLED = exports.PLUGINS_PUB_URI = exports.API_URI = exports.ADMIN_URI = exports.FRONTEND_URI = exports.SPECIAL_URI = void 0;
4
4
  exports.SPECIAL_URI = '/~/';
5
5
  exports.FRONTEND_URI = exports.SPECIAL_URI + 'frontend/';
6
6
  exports.ADMIN_URI = exports.SPECIAL_URI + 'admin/';
@@ -8,6 +8,7 @@ exports.API_URI = exports.SPECIAL_URI + 'api/';
8
8
  exports.PLUGINS_PUB_URI = exports.SPECIAL_URI + 'plugins/';
9
9
  exports.PORT_DISABLED = -1;
10
10
  exports.NBSP = '\xA0';
11
+ exports.PLUGIN_CUSTOM_REST_PREFIX = '_';
11
12
  exports.HTTP_OK = 200;
12
13
  exports.HTTP_NO_CONTENT = 204;
13
14
  exports.HTTP_PARTIAL_CONTENT = 206;