hfs 3.3.1 → 3.3.2

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.
package/central.json CHANGED
@@ -52,6 +52,9 @@
52
52
  "byVersion": {
53
53
  "^(0\\..*|3\\.[01]\\..*|3\\.2\\.0.*)$": {
54
54
  "alert": "This version is dangerous – please update"
55
+ },
56
+ "^3\\.2\\.[1-9]": {
57
+ "alert": "Please update. Your installation may be at risk if:\n - shared folders contain hidden subfolders: their contents can be downloaded if the folder name is known;\n - untrusted users can download folders as ZIP: they can make HFS unavailable;\n - you use WebDAV, LDAP, or `?login=` links: passwords can be exposed during login;\n - multiple users manage files in the same folders: one user can alter another's files"
55
58
  }
56
59
  }
57
60
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "3.3.1",
3
+ "version": "3.3.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "hfs",
9
- "version": "3.3.1",
9
+ "version": "3.3.2",
10
10
  "license": "GPL-3.0",
11
11
  "dependencies": {
12
12
  "@gregoranders/csv": "^0.0.13",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "3.3.1",
3
+ "version": "3.3.2",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
package/src/api.auth.js CHANGED
@@ -46,11 +46,11 @@ exports.authApis = {
46
46
  const account = await (0, auth_1.clearTextLogin)(ctx, username, password, 'api');
47
47
  if (!account)
48
48
  return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED);
49
- await (0, auth_1.setLoggedIn)(ctx, account.username);
49
+ await (0, auth_1.setLoggedIn)(ctx, account.username, ctx.state.params ? 'body' : 'url');
50
50
  }
51
51
  catch (e) {
52
52
  events_1.default.emit('failedLogin', { ctx, username, via: 'api' });
53
- return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, String(e));
53
+ return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, e);
54
54
  }
55
55
  return {
56
56
  redirect: ctx.state.account?.redirect,
@@ -117,7 +117,7 @@ exports.authApis = {
117
117
  try {
118
118
  const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
119
119
  .catch(() => { throw ''; }); // falsy value for later
120
- await (0, auth_1.setLoggedIn)(ctx, username);
120
+ await (0, auth_1.setLoggedIn)(ctx, username, 'srp');
121
121
  return {
122
122
  proof: String(M2),
123
123
  redirect: ctx.state.account?.redirect,
@@ -128,7 +128,7 @@ exports.authApis = {
128
128
  ctx.logExtra({ u: username });
129
129
  ctx.state.dontLog = false; // log even if log_api is false
130
130
  events_1.default.emit('failedLogin', { ctx, username });
131
- return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, e ? String(e) : undefined);
131
+ return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, e);
132
132
  }
133
133
  finally {
134
134
  delete ongoingLogins[sid];
package/src/api.vfs.js CHANGED
@@ -168,8 +168,12 @@ exports.default = {
168
168
  async resolve_path({ path, closestFolder }) {
169
169
  path = (0, path_1.resolve)(path);
170
170
  if (closestFolder)
171
- while (path && !await (0, misc_1.isDirectory)(path))
172
- path = (0, path_1.dirname)(path);
171
+ while (path && !await (0, misc_1.isDirectory)(path)) {
172
+ const parent = (0, path_1.dirname)(path);
173
+ if (parent === path)
174
+ break; // an unavailable root has no other parent to try
175
+ path = parent;
176
+ }
173
177
  return { path, isFolder: await (0, misc_1.isDirectory)(path) };
174
178
  },
175
179
  async mkdir({ path }) {
@@ -191,7 +195,7 @@ exports.default = {
191
195
  }
192
196
  return list.close();
193
197
  }
194
- const sendPropsAsap = (0, util_os_1.getDiskSpace)(path).then(x => x && list.props(x));
198
+ const sendPropsAsap = (0, util_os_1.getDiskSpace)(path).then(x => x && list.props(x)).catch(() => { }); // listing can fail before we await the disk-space query
195
199
  try {
196
200
  const matching = (0, misc_1.makeMatcher)(fileMask);
197
201
  path = (0, misc_1.isWindowsDrive)(path) ? path + '\\' : (0, path_1.resolve)(path || '/');
@@ -215,7 +219,7 @@ exports.default = {
215
219
  }
216
220
  catch { } // just ignore entries we can't stat
217
221
  });
218
- await sendPropsAsap.catch(() => { });
222
+ await sendPropsAsap;
219
223
  list.close();
220
224
  }
221
225
  catch (e) {
package/src/auth.js CHANGED
@@ -90,7 +90,7 @@ async function clearTextLogin(ctx, u, p, via) {
90
90
  return a;
91
91
  }
92
92
  // centralized log-in state
93
- async function setLoggedIn(ctx, username) {
93
+ async function setLoggedIn(ctx, username, via) {
94
94
  const s = ctx.session;
95
95
  if (!s)
96
96
  return ctx.throw(cross_const_1.HTTP_SERVER_ERROR, 'session');
@@ -106,7 +106,7 @@ async function setLoggedIn(ctx, username) {
106
106
  const a = ctx.state.account = (0, perm_1.getAccount)(username);
107
107
  if (!a)
108
108
  return;
109
- const result = await events_1.default.emitAsync('finalizingLogin', { ctx, username: a.username, inputs: { ...ctx.state.params, ...ctx.query } });
109
+ const result = await events_1.default.emitAsync('finalizingLogin', { ctx, username: a.username, via, inputs: { ...ctx.state.params, ...ctx.query } });
110
110
  const error = result?.find(x => x && lodash_1.default.isString(x)) || result?.isDefaultPrevented() && "Login denied";
111
111
  if (error) {
112
112
  // restore the session identity: the candidate account was exposed only for plugin checks
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.4;
52
+ exports.API_VERSION = 13.5;
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)
@@ -114,6 +114,7 @@ const prepareState = async (ctx, next) => {
114
114
  // calculate these once and for all
115
115
  ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
116
116
  // explicit credentials and existing sessions must take precedence, so a matching IP cannot override a chosen account
117
+ let via;
117
118
  let a = await urlLogin() || await getHttpAccount() || !s?.username && autoLogin();
118
119
  const loggedInNotBySession = a;
119
120
  ctx.state.account = a ||= (0, perm_1.getAccount)(s?.username, false); // with least precedence, we consider session
@@ -123,12 +124,12 @@ const prepareState = async (ctx, next) => {
123
124
  else if (loggedInNotBySession) {
124
125
  try {
125
126
  if (a.username)
126
- await (0, auth_1.setLoggedIn)(ctx, a.username);
127
+ await (0, auth_1.setLoggedIn)(ctx, a.username, via);
127
128
  }
128
129
  catch (e) {
129
130
  events_1.default.emit('failedLogin', { ctx, username: a.username, via: ctx.query.login ? 'url' : ctx.get('authorization') ? 'header' : undefined });
130
131
  ctx.status = const_1.HTTP_UNAUTHORIZED;
131
- ctx.body = String(e);
132
+ ctx.body = e instanceof Error ? e.message : String(e);
132
133
  return;
133
134
  }
134
135
  ctx.headers['x-username'] = a.username; // give an easier way to determine if the login was successful
@@ -137,6 +138,7 @@ const prepareState = async (ctx, next) => {
137
138
  (0, connections_1.updateConnectionForCtx)(ctx);
138
139
  await next();
139
140
  function urlLogin() {
141
+ via = 'url';
140
142
  const { login } = ctx.query;
141
143
  if (!login)
142
144
  return;
@@ -145,6 +147,7 @@ const prepareState = async (ctx, next) => {
145
147
  return u && (0, auth_1.clearTextLogin)(ctx, u, p, 'url');
146
148
  }
147
149
  function getHttpAccount() {
150
+ via = 'header';
148
151
  const b64 = allowAuthorizationHeader.get() && ctx.get('authorization')?.split(' ')[1];
149
152
  if (!b64)
150
153
  return;
@@ -157,6 +160,7 @@ const prepareState = async (ctx, next) => {
157
160
  catch { }
158
161
  }
159
162
  function autoLogin() {
163
+ via = 'net';
160
164
  // keep the mask direct so group inheritance cannot make identity depend on account order
161
165
  return Object.values(perm_1.accounts.get()).find(a => (0, perm_1.accountCanLogin)(a) && a.auto_login_net && (0, misc_1.netMatches)(ctx.ip, a.auto_login_net, true));
162
166
  }
package/src/update.js CHANGED
@@ -28,6 +28,7 @@ const persistence_1 = require("./persistence");
28
28
  const lodash_1 = __importDefault(require("lodash"));
29
29
  const argv_1 = require("./argv");
30
30
  const promises_2 = require("stream/promises");
31
+ const updateChangelog_1 = require("./updateChangelog");
31
32
  const updateToBeta = (0, config_1.defineConfig)(misc_1.CFG.update_to_beta, false);
32
33
  const autoCheckUpdate = (0, config_1.defineConfig)(misc_1.CFG.auto_check_update, true);
33
34
  const lastCheckUpdate = persistence_1.storedMap.singleSync('lastCheckUpdate', 0);
@@ -69,14 +70,17 @@ function prepareRelease(r) {
69
70
  assets: r.assets.map((a) => lodash_1.default.pick(a, ReleaseAssetKeys))
70
71
  });
71
72
  }
72
- async function getVersions(filter, max = 30) {
73
+ async function getVersions(filter, max = 30, stopAtCurrent = false) {
73
74
  const ret = [];
74
75
  for await (const x of (0, github_1.apiGithubPaginated)(`repos/${const_1.HFS_REPO}/releases`)) {
75
76
  if (x.name.endsWith('-ignore'))
76
77
  continue;
77
78
  const rel = prepareRelease(x);
78
- if (rel.versionScalar === curV)
79
+ if (rel.versionScalar === curV) {
80
+ if (stopAtCurrent)
81
+ break; // stop at the installed release, not at older maintenance releases published more recently
79
82
  continue;
83
+ }
80
84
  if (!filter || filter(rel))
81
85
  ret.push(rel);
82
86
  if (ret.length >= max)
@@ -93,6 +97,11 @@ async function getUpdates(strict = false) {
93
97
  const betas = !includeBetas ? [] : await getVersions(x => x.prerelease && x.versionScalar > stable.versionScalar && (!strict || x.isNewer));
94
98
  if (stable.isNewer || const_1.RUNNING_BETA && !strict)
95
99
  betas.push(stable);
100
+ if (stable.isNewer && stable.body) {
101
+ const history = await getVersions(r => !r.prerelease && r.isNewer && r.versionScalar <= stable.versionScalar, Infinity, true)
102
+ .catch(() => []); // missing history must not prevent installing an available update
103
+ stable.body = (0, updateChangelog_1.updateChangelog)(stable, history, curV);
104
+ }
96
105
  return betas;
97
106
  }
98
107
  const LOCAL_UPDATE = 'hfs-update.zip'; // update from file takes precedence over net
@@ -0,0 +1,31 @@
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.updateChangelog = updateChangelog;
7
+ const lodash_1 = __importDefault(require("lodash"));
8
+ function updateChangelog(destination, releases, currentVersion) {
9
+ const newer = lodash_1.default.sortBy(lodash_1.default.uniqBy([destination, ...releases], 'tag_name')
10
+ .filter(r => !r.prerelease && r.versionScalar > currentVersion && r.versionScalar <= destination.versionScalar), r => -r.versionScalar);
11
+ const level = Math.max(...newer.map(releaseLevel));
12
+ const seen = new Set();
13
+ const sections = newer.filter(r => releaseLevel(r) === level).map(r => {
14
+ let body = r.body.trim();
15
+ if (level === 0) {
16
+ // deduplicate complete entries, preserving continuation lines and nested lists
17
+ body = body.replace(/\r\n/g, '\n').split(/\n{2,}|\n(?=[*+-] )/).filter(entry => {
18
+ const key = entry.trim().replace(/^[*+-] /, '');
19
+ if (!key || seen.has(key))
20
+ return false;
21
+ seen.add(key);
22
+ return true;
23
+ }).join('\n\n');
24
+ }
25
+ return { tag: r.tag_name, body };
26
+ }).filter(r => r.body);
27
+ return sections.map(r => r.tag === destination.tag_name ? r.body : `**${r.tag}**\n\n${r.body}`).join('\n\n') || destination.body;
28
+ function releaseLevel(r) {
29
+ return r.versionScalar % 1E6 === 0 ? 2 : r.versionScalar % 1E3 === 0 ? 1 : 0;
30
+ }
31
+ }
package/src/upload.js CHANGED
@@ -125,11 +125,14 @@ function uploadWriter(base, baseUri, filename, ctx) {
125
125
  if (firstCreated) {
126
126
  setUploadMeta(dir, ctx);
127
127
  // recursive mkdir returns the first created directory; existing ancestors must not receive ownership
128
- const existingParent = (0, path_1.dirname)((0, path_1.resolve)(firstCreated));
128
+ const firstCreatedPath = (0, path_1.toNamespacedPath)((0, path_1.resolve)(firstCreated)); // Node 24 mkdir can return a namespace-prefixed Windows path
129
129
  let source = (0, path_1.resolve)(dir);
130
130
  let uri = path_1.posix.dirname(vfsUri);
131
- while (source !== existingParent) {
131
+ while (true) {
132
132
  createdFolders.push({ uri, source });
133
+ // stop before the existing parent: namespaced drive and UNC roots can differ in their trailing separator
134
+ if ((0, path_1.toNamespacedPath)(source) === firstCreatedPath)
135
+ break;
133
136
  source = (0, path_1.dirname)(source);
134
137
  uri = path_1.posix.dirname(uri);
135
138
  }
package/src/util-os.js CHANGED
@@ -23,8 +23,12 @@ const const_1 = require("./const");
23
23
  const promises_1 = require("node:fs/promises");
24
24
  const DF_TIMEOUT = 2000;
25
25
  function getDiskSpaceSync(path) {
26
- while (path && !(0, misc_1.isWindowsDrive)(path) && !(0, fs_1.existsSync)(path))
27
- path = (0, path_1.dirname)(path);
26
+ while (path && !(0, misc_1.isWindowsDrive)(path) && !(0, fs_1.existsSync)(path)) {
27
+ const parent = (0, path_1.dirname)(path);
28
+ if (parent === path)
29
+ break; // an unavailable root has no other parent to try
30
+ path = parent;
31
+ }
28
32
  const res = (0, fs_1.statfsSync)(path);
29
33
  return { free: res.bavail * res.bsize, total: res.blocks * res.bsize, name: path };
30
34
  }
@@ -35,8 +39,12 @@ function cmdEscape(par) {
35
39
  return `"${par.replaceAll('"', '\\"')}"`;
36
40
  }
37
41
  async function getDiskSpace(path) {
38
- while (path && !(0, misc_1.isWindowsDrive)(path) && !await (0, misc_1.exists)(path))
39
- path = (0, path_1.dirname)(path);
42
+ while (path && !(0, misc_1.isWindowsDrive)(path) && !await (0, misc_1.exists)(path)) {
43
+ const parent = (0, path_1.dirname)(path);
44
+ if (parent === path)
45
+ break; // an unavailable root has no other parent to try; this test covers UNC as well
46
+ path = parent;
47
+ }
40
48
  const res = await (0, promises_1.statfs)(path);
41
49
  return { free: res.bavail * res.bsize, total: res.blocks * res.bsize, name: path };
42
50
  }