hfs 0.26.7 → 0.26.9

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.
@@ -1,4 +1,4 @@
1
- import{c as SF}from"./index.1151988f.js";function OF(sF,hF){for(var eF=0;eF<hF.length;eF++){const tF=hF[eF];if(typeof tF!="string"&&!Array.isArray(tF)){for(const w in tF)if(w!=="default"&&!(w in sF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(sF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(sF,Symbol.toStringTag,{value:"Module"}))}var yF={exports:{}};/*
1
+ import{c as SF}from"./index.27a78796.js";function OF(sF,hF){for(var eF=0;eF<hF.length;eF++){const tF=hF[eF];if(typeof tF!="string"&&!Array.isArray(tF)){for(const w in tF)if(w!=="default"&&!(w in sF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(sF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(sF,Symbol.toStringTag,{value:"Module"}))}var yF={exports:{}};/*
2
2
  * [js-sha512]{@link https://github.com/emn178/js-sha512}
3
3
  *
4
4
  * @version 0.8.0
@@ -6,7 +6,7 @@
6
6
  <link href="fontello.css" rel="stylesheet" />
7
7
  <script>SESSION = _HFS_SESSION_</script>
8
8
  <title>File Server</title>
9
- <script type="module" crossorigin src="/assets/index.1151988f.js"></script>
9
+ <script type="module" crossorigin src="/assets/index.27a78796.js"></script>
10
10
  <link rel="stylesheet" href="/assets/index.93366732.css">
11
11
  </head>
12
12
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.26.7",
3
+ "version": "0.26.9",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -24,6 +24,7 @@
24
24
  "build-shared": "npm run build --workspace=shared",
25
25
  "build-form": "npm run build --workspace=mui-grid-form",
26
26
  "server-for-test": "node dist/src --cwd . --config tests",
27
+ "server-for-test-dev": "cross-env DEV=1 nodemon --ignore tests/ --watch src -e ts,tsx --exec ts-node src -- --cwd . --config tests",
27
28
  "test": "mocha -r ts-node/register 'tests/**/*.ts'",
28
29
  "pub": "cd dist && npm publish",
29
30
  "dist": "npm run build-all && npm run dist-bin",
@@ -21,7 +21,7 @@ exports.init = api => ({
21
21
  middleware(ctx) {
22
22
  let toModify = ctx
23
23
  if (ctx.path.startsWith(api.const.SPECIAL_URI)) { // special uris should be excluded...
24
- toModify = ctx.request.query
24
+ toModify = ctx.params
25
25
  if (toModify.path === undefined) // ...unless they carry a path in the query. In that case we'll work that.
26
26
  return
27
27
  }
@@ -37,14 +37,14 @@ const apis = {
37
37
  changes.admin = undefined;
38
38
  else if (admin !== undefined && typeof admin !== 'boolean')
39
39
  return new apiMiddleware_1.ApiError(400, "invalid admin");
40
- return (0, perm_1.setAccount)(username, changes) ? {} : new apiMiddleware_1.ApiError(400);
40
+ const acc = (0, perm_1.setAccount)(username, changes);
41
+ return acc ? lodash_1.default.pick(acc, 'username') : new apiMiddleware_1.ApiError(400);
41
42
  },
42
43
  add_account({ username, ...rest }) {
43
44
  if ((0, perm_1.getAccount)(username))
44
45
  return new apiMiddleware_1.ApiError(const_1.FORBIDDEN);
45
- if (!(0, perm_1.addAccount)(username, rest))
46
- return new apiMiddleware_1.ApiError(400);
47
- return {};
46
+ const acc = (0, perm_1.addAccount)(username, rest);
47
+ return acc ? lodash_1.default.pick(acc, 'username') : new apiMiddleware_1.ApiError(400);
48
48
  },
49
49
  del_account({ username }) {
50
50
  return (0, perm_1.delAccount)(username) ? {} : new apiMiddleware_1.ApiError(400);
package/src/api.auth.js CHANGED
@@ -23,7 +23,7 @@ async function loggedIn(ctx, username) {
23
23
  ctx.cookies.set('csrf', '');
24
24
  return;
25
25
  }
26
- s.username = username;
26
+ s.username = (0, perm_1.normalizeUsername)(username);
27
27
  await (0, middlewares_1.prepareState)(ctx, async () => { }); // updating the state is necessary to send complete session data so that frontend shows admin button
28
28
  delete s.login;
29
29
  ctx.cookies.set('csrf', (0, misc_1.randomId)(), { signed: false, httpOnly: false });
@@ -34,7 +34,6 @@ function makeExp() {
34
34
  const login = async ({ username, password }, ctx) => {
35
35
  if (!username || !password) // some validation
36
36
  return new apiMiddleware_1.ApiError(400);
37
- username = username.toLocaleLowerCase(); // normalize username, to be case-insensitive
38
37
  const acc = (0, perm_1.getAccount)(username);
39
38
  if (!acc)
40
39
  return new apiMiddleware_1.ApiError(const_1.UNAUTHORIZED);
@@ -51,7 +50,6 @@ exports.login = login;
51
50
  const loginSrp1 = async ({ username }, ctx) => {
52
51
  if (!username)
53
52
  return new apiMiddleware_1.ApiError(400);
54
- username = username.toLocaleLowerCase();
55
53
  const account = (0, perm_1.getAccount)(username);
56
54
  if (!ctx.session)
57
55
  return new apiMiddleware_1.ApiError(500);
@@ -107,7 +105,7 @@ exports.loginSrp2 = loginSrp2;
107
105
  const logout = async ({}, ctx) => {
108
106
  if (!ctx.session)
109
107
  return new apiMiddleware_1.ApiError(500);
110
- loggedIn(ctx, false);
108
+ await loggedIn(ctx, false);
111
109
  // 401 is a convenient code for OK: the browser clears a possible http authentication (hopefully), and Admin automatically triggers login dialog
112
110
  return new apiMiddleware_1.ApiError(401);
113
111
  };
@@ -20,8 +20,7 @@ class ApiError extends Error {
20
20
  exports.ApiError = ApiError;
21
21
  function apiMiddleware(apis) {
22
22
  return async (ctx) => {
23
- const params = ctx.method === 'POST' ? await getJsonFromReq(ctx.req)
24
- : (0, misc_1.objSameKeys)(ctx.request.query, x => Array.isArray(x) ? x : (0, misc_1.tryJson)(x));
23
+ const { params } = ctx;
25
24
  console.debug('API', ctx.method, ctx.path, { ...params });
26
25
  if (!apis.hasOwnProperty(ctx.path)) {
27
26
  ctx.body = 'invalid api';
@@ -55,21 +54,6 @@ exports.apiMiddleware = apiMiddleware;
55
54
  function isAsyncGenerator(x) {
56
55
  return typeof (x === null || x === void 0 ? void 0 : x.next) === 'function';
57
56
  }
58
- async function getJsonFromReq(req) {
59
- return new Promise((resolve, reject) => {
60
- let data = '';
61
- req.on('data', chunk => data += chunk);
62
- req.on('error', reject);
63
- req.on('end', () => {
64
- try {
65
- resolve(data && JSON.parse(data));
66
- }
67
- catch (e) {
68
- reject(e);
69
- }
70
- });
71
- });
72
- }
73
57
  class SendListReadable extends stream_1.Readable {
74
58
  constructor({ addAtStart, doAtStart, bufferTime } = {}) {
75
59
  super({ objectMode: true, read() { } });
package/src/index.js CHANGED
@@ -33,6 +33,7 @@ exports.app.use(middlewares_1.someSecurity)
33
33
  .use((0, log_1.log)())
34
34
  .use(throttler_1.throttler)
35
35
  .use(middlewares_1.gzipper)
36
+ .use(middlewares_1.paramsDecoder)
36
37
  .use((0, plugins_1.pluginsMiddleware)())
37
38
  .use((0, koa_mount_1.default)(const_1.API_URI, (0, apiMiddleware_1.apiMiddleware)({ ...frontEndApis_1.frontEndApis, ...adminApis_1.adminApis })))
38
39
  .use(middlewares_1.serveGuiAndSharedFiles)
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.prepareState = exports.getProxyDetected = exports.someSecurity = exports.serveGuiAndSharedFiles = exports.sessions = exports.headRequests = exports.gzipper = void 0;
7
+ exports.paramsDecoder = exports.prepareState = exports.getProxyDetected = exports.someSecurity = exports.serveGuiAndSharedFiles = exports.sessions = exports.headRequests = exports.gzipper = void 0;
8
8
  const koa_compress_1 = __importDefault(require("koa-compress"));
9
9
  const koa_session_1 = __importDefault(require("koa-session"));
10
10
  const const_1 = require("./const");
@@ -82,7 +82,7 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
82
82
  return;
83
83
  const browserDetected = ctx.get('Upgrade-Insecure-Requests') || ctx.get('Sec-Fetch-Mode'); // ugh, heuristics
84
84
  if (!browserDetected) // we don't want to trigger basic authentication on browsers, it's meant for download managers only
85
- ctx.set('WWW-Authenticate', 'Basic'); // we support basic authentication
85
+ return ctx.set('WWW-Authenticate', 'Basic'); // we support basic authentication
86
86
  ctx.state.serveApp = true;
87
87
  return serveFrontendFiles(ctx, next);
88
88
  }
@@ -126,9 +126,9 @@ function getProxyDetected() {
126
126
  }
127
127
  exports.getProxyDetected = getProxyDetected;
128
128
  const prepareState = async (ctx, next) => {
129
- var _a;
129
+ var _a, _b;
130
130
  // calculate these once and for all
131
- ctx.state.account = (_a = await getHttpAccount(ctx)) !== null && _a !== void 0 ? _a : (0, perm_1.getAccount)((0, perm_1.getCurrentUsername)(ctx));
131
+ ctx.state.account = (_a = await getHttpAccount(ctx)) !== null && _a !== void 0 ? _a : (0, perm_1.getAccount)((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username, false);
132
132
  const conn = ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
133
133
  await next();
134
134
  if (conn)
@@ -142,7 +142,6 @@ async function getHttpAccount(ctx) {
142
142
  return account;
143
143
  }
144
144
  async function srpCheck(username, password) {
145
- username = username.toLocaleLowerCase();
146
145
  const account = (0, perm_1.getAccount)(username);
147
146
  if (!(account === null || account === void 0 ? void 0 : account.srp) || !password)
148
147
  return false;
@@ -152,3 +151,25 @@ async function srpCheck(username, password) {
152
151
  const clientRes2 = await clientRes1.step2(BigInt(salt), BigInt(pubKey));
153
152
  return await step1.step2(clientRes2.A, clientRes2.M1).then(() => true, () => false);
154
153
  }
154
+ // unify get/post parameters, with JSON decoding to not be limited to strings
155
+ const paramsDecoder = async (ctx, next) => {
156
+ ctx.params = ctx.method === 'POST' ? (0, misc_1.tryJson)(await getReqData(ctx.req))
157
+ : (0, misc_1.objSameKeys)(ctx.query, x => Array.isArray(x) ? x : (0, misc_1.tryJson)(x));
158
+ await next();
159
+ };
160
+ exports.paramsDecoder = paramsDecoder;
161
+ async function getReqData(req) {
162
+ return new Promise((resolve, reject) => {
163
+ let data = '';
164
+ req.on('data', chunk => data += chunk);
165
+ req.on('error', reject);
166
+ req.on('end', () => {
167
+ try {
168
+ resolve(data);
169
+ }
170
+ catch (e) {
171
+ reject(e);
172
+ }
173
+ });
174
+ });
175
+ }
package/src/perm.js CHANGED
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.anyAccountCanLoginAdmin = exports.accountCanLoginAdmin = exports.accountCanLogin = exports.accountHasPassword = exports.getFromAccount = exports.delAccount = exports.setAccount = exports.addAccount = exports.renameAccount = exports.updateAccount = exports.allowClearTextLogin = exports.saveSrpInfo = exports.getAccount = exports.getCurrentUsernameExpanded = exports.getCurrentUsername = exports.getAccounts = void 0;
7
+ exports.anyAccountCanLoginAdmin = exports.accountCanLoginAdmin = exports.accountCanLogin = exports.accountHasPassword = exports.getFromAccount = exports.delAccount = exports.setAccount = exports.addAccount = exports.renameAccount = exports.normalizeUsername = exports.updateAccount = exports.allowClearTextLogin = exports.saveSrpInfo = exports.getAccount = exports.getCurrentUsernameExpanded = exports.getCurrentUsername = exports.getAccounts = void 0;
8
8
  const lodash_1 = __importDefault(require("lodash"));
9
9
  const crypt_1 = require("./crypt");
10
10
  const misc_1 = require("./misc");
@@ -17,8 +17,8 @@ function getAccounts() {
17
17
  }
18
18
  exports.getAccounts = getAccounts;
19
19
  function getCurrentUsername(ctx) {
20
- var _a, _b;
21
- return ((_a = ctx.state.account) === null || _a === void 0 ? void 0 : _a.username) || ((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username) || '';
20
+ var _a;
21
+ return ((_a = ctx.state.account) === null || _a === void 0 ? void 0 : _a.username) || '';
22
22
  }
23
23
  exports.getCurrentUsername = getCurrentUsername;
24
24
  // provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
@@ -35,7 +35,9 @@ function getCurrentUsernameExpanded(ctx) {
35
35
  return ret;
36
36
  }
37
37
  exports.getCurrentUsernameExpanded = getCurrentUsernameExpanded;
38
- function getAccount(username) {
38
+ function getAccount(username, normalize = true) {
39
+ if (normalize)
40
+ username = normalizeUsername(username);
39
41
  return username ? accounts[username] : undefined;
40
42
  }
41
43
  exports.getAccount = getAccount;
@@ -77,15 +79,16 @@ accountsConfig.sub(async (v) => {
77
79
  const norm = normalizeUsername(k);
78
80
  if (!rec) // an empty object in yaml is stored as null
79
81
  rec = accounts[norm] = { username: norm };
80
- else
81
- (0, misc_1.objRenameKey)(accounts, k, norm);
82
+ else if ((0, misc_1.objRenameKey)(accounts, k, norm))
83
+ saveAccountsAsap();
82
84
  (0, misc_1.setHidden)(rec, { username: norm });
83
- await updateAccount(rec);
85
+ await updateAccount(rec); // work password fields
84
86
  }));
85
87
  });
86
88
  function normalizeUsername(username) {
87
89
  return username.toLocaleLowerCase();
88
90
  }
91
+ exports.normalizeUsername = normalizeUsername;
89
92
  function renameAccount(from, to) {
90
93
  from = normalizeUsername(from);
91
94
  to = normalizeUsername(to);
@@ -112,9 +115,11 @@ exports.renameAccount = renameAccount;
112
115
  // we consider all the following fields, when falsy, as equivalent to be missing. If this changes in the future, please adjust addAccount and setAccount
113
116
  const assignableProps = ['redirect', 'ignore_limits', 'belongs', 'admin'];
114
117
  function addAccount(username, props) {
115
- if (!username || accounts[username])
118
+ username = normalizeUsername(username);
119
+ if (!username || getAccount(username, false))
116
120
  return;
117
- const copy = (0, misc_1.setHidden)(lodash_1.default.pickBy(lodash_1.default.pick(props, assignableProps), Boolean), { username }); // have the field in the object but hidden so that stringification won't include it
121
+ const filteredProps = lodash_1.default.pickBy(lodash_1.default.pick(props, assignableProps), Boolean);
122
+ const copy = (0, misc_1.setHidden)(filteredProps, { username }); // have the field in the object but hidden so that stringification won't include it
118
123
  accountsConfig.set(accounts => Object.assign(accounts, { [username]: copy }));
119
124
  saveAccountsAsap().then();
120
125
  return copy;
@@ -132,13 +137,13 @@ function setAccount(username, changes) {
132
137
  if (changes.username)
133
138
  renameAccount(username, changes.username);
134
139
  saveAccountsAsap().then();
135
- return true;
140
+ return acc;
136
141
  }
137
142
  exports.setAccount = setAccount;
138
143
  function delAccount(username) {
139
144
  if (!getAccount(username))
140
145
  return false;
141
- accountsConfig.set(accounts => Object.assign(accounts, { [username]: undefined }));
146
+ accountsConfig.set(accounts => Object.assign(accounts, { [normalizeUsername(username)]: undefined }));
142
147
  saveAccountsAsap().then();
143
148
  return true;
144
149
  }
package/src/serveFile.js CHANGED
@@ -89,15 +89,16 @@ function getRange(ctx, totalSize) {
89
89
  return ctx.throw(400, 'bad range');
90
90
  const max = totalSize - 1;
91
91
  const start = bytes[0] ? Number(bytes[0]) : Math.max(0, totalSize - Number(bytes[1])); // a negative start is relative to the end
92
- const end = bytes[0] ? Number(bytes[1] || max) : max; // NaN in case we are asked for last N bytes without knowing max
93
- if (isNaN(end) || end > max || start > max) {
92
+ const end = bytes[0] ? Number(bytes[1] || max) : max;
93
+ // we don't support last-bytes without knowing max
94
+ if (isNaN(end) && isNaN(max) || end > max || start > max) {
94
95
  ctx.status = 416;
95
96
  ctx.set('Content-Range', `bytes ${totalSize}`);
96
97
  ctx.body = 'Requested Range Not Satisfiable';
97
98
  return;
98
99
  }
99
100
  ctx.status = 206;
100
- ctx.set('Content-Range', `bytes ${start}-${end}/${isNaN(totalSize) ? '*' : totalSize}`);
101
+ ctx.set('Content-Range', `bytes ${start}-${isNaN(end) ? '' : end}/${isNaN(totalSize) ? '*' : totalSize}`);
101
102
  ctx.response.length = end - start + 1;
102
103
  return { start, end };
103
104
  }
package/src/vfs.js CHANGED
@@ -38,11 +38,14 @@ function inheritFromParent(parent, child) {
38
38
  }
39
39
  async function urlToNode(url, ctx, parent = exports.vfs) {
40
40
  var _a;
41
- let i = url.indexOf('/', 1);
42
- const name = decodeURIComponent(url.slice(url[0] === '/' ? 1 : 0, i < 0 ? undefined : i));
41
+ let initialSlashes = 0;
42
+ while (url[initialSlashes] === '/')
43
+ initialSlashes++;
44
+ let nextSlash = url.indexOf('/', initialSlashes);
45
+ const name = decodeURIComponent(url.slice(initialSlashes, nextSlash < 0 ? undefined : nextSlash));
43
46
  if (!name)
44
47
  return parent;
45
- const rest = i < 0 ? '' : url.slice(i + 1, url.endsWith('/') ? -1 : undefined);
48
+ const rest = nextSlash < 0 ? '' : url.slice(nextSlash + 1, url.endsWith('/') ? -1 : undefined);
46
49
  if ((0, misc_1.dirTraversal)(name) || /[\/]/.test(name)) {
47
50
  if (ctx)
48
51
  ctx.status = 418;
package/src/zip.js CHANGED
@@ -57,8 +57,11 @@ async function zipStreamFromFolder(node, ctx) {
57
57
  });
58
58
  const zip = new QuickZipStream_1.QuickZipStream(mappedWalker);
59
59
  const time = 1000 * zipSeconds.get();
60
- ctx.response.length = await zip.calculateSize(time);
61
- const range = (0, serveFile_1.getRange)(ctx, ctx.response.length);
60
+ const size = await zip.calculateSize(time);
61
+ ctx.response.length = size;
62
+ const range = (0, serveFile_1.getRange)(ctx, size); // keep var size as ctx.response.length won't preserve a NaN
63
+ if (ctx.status >= 400)
64
+ return;
62
65
  if (range)
63
66
  zip.applyRange(range.start, range.end);
64
67
  ctx.body = zip;