hfs 0.27.2 → 0.29.0

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.
@@ -0,0 +1 @@
1
+ @charset "UTF-8";:root{--bg: #fff;--text: #555;--faint-contrast: #0002;--mild-contrast: #0005;--good-contrast: #000a;--button-bg: #68a;--button-text: #fff;--focus-color: #468}:root .theme-dark{--bg: #000;--text: #999;--faint-contrast: #fff2;--mild-contrast: #fff5;--good-contrast: #fffa;--button-bg: #345;--button-text: #999}:root .theme-dark body{color-scheme:dark}:root .theme-dark a{color:#8ac}:root .theme-dark .dialog-closer{background:#633}:root .theme-dark .dialog-icon{color:#ccc}:root .theme-dark .dialog-icon .icon{color:#aaa;margin-left:-1px;font-size:95%}:root .theme-dark .dialog-backdrop{background:rgba(51,51,51,.7333333333)}:root .theme-dark .error-msg{color:#b88;background-color:#623}body{background:var(--bg);margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body,button,select,input{font-size:12pt}#root{max-width:50em;margin:auto;min-height:100vh;display:flex;flex-direction:column}body,input{color:var(--text)}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}input,select{padding:.3em .4em;border-radius:.5em;background:var(--bg);border-color:var(--mild-contrast);color:var(--good-contrast);max-width:100%;box-sizing:border-box}input[type=checkbox]{margin:0 1.3em 0 .8em;transform:scale(1.7);accent-color:var(--button-bg)}.hidden{display:none!important}.icon{font-size:1.2em}.icon.mirror:before{transform:scaleX(-1)}a{text-decoration:none;color:#57a}button{background-color:var(--button-bg);color:var(--button-text);padding:.5em 1em;border:transparent;text-decoration:none;border-radius:.3em;vertical-align:middle;cursor:pointer}button.toggled{opacity:.6}button:focus-visible,.breadcrumb:focus-visible{outline:3px solid var(--focus-color)}input:focus-visible,select:focus-visible,ul a:focus-visible{border-radius:.3em;border-color:transparent;outline:2px solid var(--focus-color)}.error-msg{background-color:#faa;color:#833;padding:.5em 1em}header{position:sticky;top:0;background:var(--bg);padding:.2em;z-index:1}.ani-working{animation:1s blink infinite}@keyframes blink{0%{opacity:1}50%{opacity:.2}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.spinner,.icon.spinner:before{animation:1.5s spin infinite linear}.icon.emoji.spinner{display:inline-block}.breadcrumb{padding:.1em .6em .2em;line-height:1.8em;border-radius:.7em;background-color:var(--button-bg);color:var(--button-text);border-top:1px solid #666;margin-right:-.1em}.breadcrumb:nth-child(-n+3) .icon{padding:0 .2em}#folder-stats{font-size:90%;margin:.4em 0 0 .5em;float:right}#folder-stats .icon{margin-right:.3em}header input{width:100%;margin:.2em auto;box-sizing:border-box}#filter-bar{display:flex;gap:.3em;margin:.5em 0}#filter-bar input{flex:1}#filter-bar button{padding:0 .5em}ul.dir{flex:1;padding:0;margin:0;clear:both}ul.dir li{display:block;list-style-type:none;margin-bottom:.3em;padding:.3em;border-top:1px solid var(--button-bg)}ul.dir li a{word-break:break-word;padding-right:.3em}ul.dir li a .icon{margin-right:.3em}ul.dir li a.container-folder:hover{text-decoration:underline}ul.dir li .entry-props{float:right;font-size:90%;margin-left:12px;margin-top:.2em}ul.dir li .entry-props .icon{margin:0 .3em}ul.dir li .entry-props .entry-size{display:inline-block}#menu-panel{margin-bottom:.2em}#menu-bar{display:flex;justify-content:space-evenly;flex-wrap:wrap}#menu-bar>*{flex:auto;margin:.1em}#menu-bar button{padding-left:0;padding-right:0}#menu-bar>a>button{width:100%}#searched{margin:.2em}#user-panel{display:flex;flex-direction:column;gap:1em}#user-panel a>button{width:100%}button label{cursor:inherit;margin-left:.5em}.dialog-backdrop.working{font-size:5em;animation:1s fade-in}.dialog-content{padding:.2em}.dialog{--color: var(--button-bg)}#paging{display:flex;position:sticky;bottom:0;background:var(--bg);gap:.5em;overflow-x:auto}#paging>button{flex:1;background:var(--button-bg);text-align:center}.upload-progress:before{content:" \2013 "}.upload-progress{min-width:4em;display:inline-block;margin-left:.5em}.upload-list td:nth-child(1){width:0}.upload-list td:nth-child(2){text-align:right;width:0;white-space:nowrap;padding-left:.5em}.upload-list td:nth-child(3){padding:.2em .5em;word-break:break-word}*{scrollbar-width:thin;scrollbar-color:var(--button-bg) var(--faint-contrast)}*::-webkit-scrollbar{width:12px}*::-webkit-scrollbar-track{background:var(--faint-contrast)}*::-webkit-scrollbar-thumb{background-color:var(--button-bg);border-radius:20px;border:1px solid var(--faint-contrast)}@media (max-width: 42em){body,button,select{font-size:14pt}#menu-bar button label{display:none}#filter-bar{margin:.2em 0}#filter-bar label{display:none}#filter-bar button{width:17.6vw;height:2.3em}.breadcrumb{word-break:break-all}.breadcrumb .icon{font-size:24px}}.dialog-backdrop{position:fixed;inset:0;background:#888a;display:flex;justify-content:center;align-items:center;z-index:1000}.dialog{background:#fff;background:var(--bg);padding:max(.5em,1vw);border-radius:1em;position:relative;margin:0 3vw;overflow:hidden;max-height:calc(100vh - 2em)}.dialog-icon{color:#fff;background-color:var(--color);position:absolute;top:0;width:1.8em;height:1.7em;text-align:center;border-radius:.8em 0}.dialog-title{margin-top:-.4em;font-size:110%}.dialog-icon~.dialog-title{text-align:center}.dialog-closer{border-radius:0 .8em;right:0;padding:0;background-color:#c99}.dialog-icon~.dialog-content{margin-top:2em}.dialog-type{left:0;top:0;overflow:hidden;line-height:1.7em}.dialog-content{overflow:auto;max-height:calc(100vh - 4.5em)}.dialog-content p{white-space:pre-wrap;margin:.5em 0}.dialog-confirm .dialog-content button{margin-top:1em}.dialog-alert-info{--color: #282 }.dialog-alert-warning{--color: #c91 }.dialog-alert-error{--color: #822}@media (max-width: 50em){.dialog-closer{font-size:120%}.dialog-icon~.dialog-content{margin-top:2em}}
@@ -1,4 +1,4 @@
1
- import{c as SF}from"./index-509bb1d6.js";function OF(iF,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 iF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(iF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(iF,Symbol.toStringTag,{value:"Module"}))}var EF={},UF={get exports(){return EF},set exports(iF){EF=iF}};/*
1
+ import{c as SF}from"./index-72e96bb2.js";function OF(iF,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 iF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(iF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(iF,Symbol.toStringTag,{value:"Module"}))}var EF={},UF={get exports(){return EF},set exports(iF){EF=iF}};/*
2
2
  * [js-sha512]{@link https://github.com/emn178/js-sha512}
3
3
  *
4
4
  * @version 0.8.0
@@ -6,8 +6,8 @@
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-aea7654e.js"></script>
10
- <link rel="stylesheet" href="/assets/index-6e178dfd.css">
9
+ <script type="module" crossorigin src="/assets/index-72e96bb2.js"></script>
10
+ <link rel="stylesheet" href="/assets/index-cbcc6ac5.css">
11
11
  </head>
12
12
  <body>
13
13
  <div hidden>_HFS_PLUGINS_</div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.27.2",
3
+ "version": "0.29.0",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -64,6 +64,7 @@
64
64
  "cidr-tools": "^4.3.0",
65
65
  "fast-glob": "^3.2.7",
66
66
  "find-process": "^1.4.7",
67
+ "formidable": "^2.1.1",
67
68
  "koa": "^2.13.4",
68
69
  "koa-compress": "^5.1.0",
69
70
  "koa-mount": "^4.0.0",
@@ -73,12 +74,13 @@
73
74
  "minimist": "^1.2.6",
74
75
  "open": "^8.4.0",
75
76
  "tssrp6a": "^3.0.0",
76
- "yaml": "^2.0.0-10",
77
- "unzip-stream": "^0.3.1"
77
+ "unzip-stream": "^0.3.1",
78
+ "yaml": "^2.0.0-10"
78
79
  },
79
80
  "devDependencies": {
80
81
  "@types/archiver": "^5.1.1",
81
82
  "@types/basic-auth": "^1.1.3",
83
+ "@types/formidable": "^2.0.5",
82
84
  "@types/koa": "^2.13.4",
83
85
  "@types/koa__router": "^8.0.11",
84
86
  "@types/koa-compress": "^4.0.3",
@@ -90,16 +92,16 @@
90
92
  "@types/minimist": "^1.2.2",
91
93
  "@types/mocha": "^9.0.0",
92
94
  "@types/node": "^16.11.12",
95
+ "@types/tough-cookie": "^4.0.2",
93
96
  "@types/unzipper": "^0.10.5",
94
97
  "axios": "^0.24.0",
95
98
  "axios-cookiejar-support": "^4.0.1",
96
- "tough-cookie": "^4.0.0",
97
- "@types/tough-cookie": "^4.0.2",
98
99
  "koa-better-http-proxy": "^0.2.9",
99
100
  "mocha": "^9.1.3",
100
101
  "nm-prune": "^5.0.0",
101
102
  "nodemon": "^2.0.15",
102
103
  "pkg": "^5.7.0",
104
+ "tough-cookie": "^4.0.0",
103
105
  "ts-node": "^10.4.0"
104
106
  }
105
107
  }
@@ -24,7 +24,7 @@ exports.init = api => {
24
24
  let toModify = ctx
25
25
  if (ctx.path.startsWith(api.const.SPECIAL_URI)) { // special uris should be excluded...
26
26
  toModify = ctx.params
27
- if (toModify.path === undefined) // ...unless they carry a path in the query. In that case we'll work that.
27
+ if (toModify?.path === undefined) // ...unless they carry a path in the query. In that case we'll work that.
28
28
  return
29
29
  }
30
30
  const hosts = api.getConfig('hosts')
@@ -1,28 +1,5 @@
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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || function (mod) {
20
- if (mod && mod.__esModule) return mod;
21
- var result = {};
22
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
23
- __setModuleDefault(result, mod);
24
- return result;
25
- };
26
3
  var __importDefault = (this && this.__importDefault) || function (mod) {
27
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
28
5
  };
@@ -35,7 +12,7 @@ const assert_1 = __importDefault(require("assert"));
35
12
  const ZIP64_SIZE_LIMIT = 0xffffffff;
36
13
  const ZIP64_NUMBER_LIMIT = 0xffff;
37
14
  let crc32function;
38
- Promise.resolve().then(() => __importStar(require('@node-rs/crc32'))).then(lib => crc32function = lib.crc32, () => {
15
+ import('@node-rs/crc32').then(lib => crc32function = lib.crc32, () => {
39
16
  console.log('using generic lib for crc32');
40
17
  crc32function = buffer_crc32_1.unsigned;
41
18
  });
package/src/adminApis.js CHANGED
@@ -149,11 +149,9 @@ exports.adminApis = {
149
149
  }
150
150
  },
151
151
  };
152
- for (const k in exports.adminApis) {
153
- const was = exports.adminApis[k];
152
+ for (const [k, was] of Object.entries(exports.adminApis))
154
153
  exports.adminApis[k] = (params, ctx) => ctxAdminAccess(ctx) ? was(params, ctx)
155
- : new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED);
156
- }
154
+ : new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, { any: (0, perm_1.anyAccountCanLoginAdmin)() });
157
155
  exports.localhostAdmin = (0, config_1.defineConfig)('localhost_admin', true);
158
156
  function ctxAdminAccess(ctx) {
159
157
  return !ctx.state.proxiedFor // we consider localhost_admin only if no proxy is detected
@@ -42,7 +42,7 @@ const apis = {
42
42
  },
43
43
  add_account({ username, ...rest }) {
44
44
  if ((0, perm_1.getAccount)(username))
45
- return new apiMiddleware_1.ApiError(const_1.HTTP_FORBIDDEN);
45
+ return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT);
46
46
  const acc = (0, perm_1.addAccount)(username, rest);
47
47
  return acc ? lodash_1.default.pick(acc, 'username') : new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST);
48
48
  },
package/src/api.auth.js CHANGED
@@ -72,6 +72,8 @@ async function srpStep1(account) {
72
72
  if (!account.srp)
73
73
  throw const_1.HTTP_NOT_ACCEPTABLE;
74
74
  const [salt, verifier] = account.srp.split('|');
75
+ if (!salt || !verifier)
76
+ throw Error("malformed account");
75
77
  const srpSession = new tssrp6a_1.SRPServerSession(srp6aNimbusRoutines);
76
78
  const step1 = await srpSession.step1(account.username, BigInt(salt), BigInt(verifier));
77
79
  return { step1, salt, pubKey: String(step1.B) }; // cast to string cause bigint can't be jsonized
@@ -85,6 +87,8 @@ const loginSrp2 = async ({ pubKey, proof }, ctx) => {
85
87
  return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT);
86
88
  const { username, sid } = ctx.session.login;
87
89
  const step1 = ongoingLogins[sid];
90
+ if (!step1)
91
+ return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND);
88
92
  try {
89
93
  const M2 = await step1.step2(BigInt(pubKey), BigInt(proof));
90
94
  await loggedIn(ctx, username);
package/src/api.vfs.js CHANGED
@@ -10,10 +10,9 @@ const promises_1 = require("fs/promises");
10
10
  const apiMiddleware_1 = require("./apiMiddleware");
11
11
  const path_1 = require("path");
12
12
  const misc_1 = require("./misc");
13
- const child_process_1 = require("child_process");
14
- const util_1 = require("util");
15
13
  const const_1 = require("./const");
16
14
  const micromatch_1 = require("micromatch");
15
+ const util_os_1 = require("./util-os");
17
16
  // to manipulate the tree we need the original node
18
17
  async function urlToNodeOriginal(uri) {
19
18
  const n = await (0, vfs_1.urlToNode)(uri);
@@ -68,7 +67,7 @@ const apis = {
68
67
  if (!n)
69
68
  return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'invalid under');
70
69
  if (n.isTemp || !await (0, vfs_1.nodeIsDirectory)(n))
71
- return new apiMiddleware_1.ApiError(const_1.HTTP_FORBIDDEN, 'invalid under');
70
+ return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_ACCEPTABLE, 'invalid under');
72
71
  if ((0, misc_1.isWindowsDrive)(source))
73
72
  source += '\\'; // slash must be included, otherwise it will refer to the cwd of that drive
74
73
  const a = n.children || (n.children = []);
@@ -91,7 +90,7 @@ const apis = {
91
90
  const parent = (0, path_1.dirname)(uri);
92
91
  const parentNode = await urlToNodeOriginal(parent);
93
92
  if (!parentNode)
94
- return const_1.HTTP_FORBIDDEN;
93
+ return const_1.HTTP_NOT_ACCEPTABLE;
95
94
  const { children } = parentNode;
96
95
  if (!children) // shouldn't happen
97
96
  return const_1.HTTP_SERVER_ERROR;
@@ -115,7 +114,7 @@ const apis = {
115
114
  async *ls({ path, files = true, fileMask }, ctx) {
116
115
  if (!path && const_1.IS_WINDOWS) {
117
116
  try {
118
- for (const n of await getDrives())
117
+ for (const n of await (0, util_os_1.getDrives)())
119
118
  yield { add: { n, k: 'd' } };
120
119
  }
121
120
  catch (error) {
@@ -161,7 +160,3 @@ function pickProps(o, keys) {
161
160
  ret[k] = o[k] === null || o[k] === '' ? undefined : o[k];
162
161
  return ret;
163
162
  }
164
- async function getDrives() {
165
- const { stdout } = await (0, util_1.promisify)(child_process_1.exec)('wmic logicaldisk get name');
166
- return stdout.split('\n').slice(1).map(x => x.trim()).filter(Boolean);
167
- }
@@ -13,7 +13,7 @@ const const_1 = require("./const");
13
13
  const lodash_1 = __importDefault(require("lodash"));
14
14
  class ApiError extends Error {
15
15
  constructor(status, message) {
16
- super(typeof message === 'string' ? message : message === null || message === void 0 ? void 0 : message.message);
16
+ super(typeof message === 'string' ? message : message && message instanceof Error ? message.message : JSON.stringify(message));
17
17
  this.status = status;
18
18
  }
19
19
  }
@@ -22,21 +22,25 @@ function apiMiddleware(apis) {
22
22
  return async (ctx) => {
23
23
  const { params } = ctx;
24
24
  console.debug('API', ctx.method, ctx.path, { ...params });
25
- if (!apis.hasOwnProperty(ctx.path)) {
25
+ const apiFun = apis.hasOwnProperty(ctx.path) && apis[ctx.path];
26
+ if (!apiFun) {
26
27
  ctx.body = 'invalid api';
27
28
  return ctx.status = const_1.HTTP_NOT_FOUND;
28
29
  }
30
+ ctx.params = ctx.method === 'POST' ? (0, misc_1.tryJson)(await (0, misc_1.stream2string)(ctx.req))
31
+ : (0, misc_1.objSameKeys)(ctx.query, x => Array.isArray(x) ? x : (0, misc_1.tryJson)(x));
32
+ console.debug('API', ctx.method, ctx.path, { ...ctx.params });
29
33
  const csrf = ctx.cookies.get('csrf');
30
34
  // we don't rely on SameSite cookie option because it's https-only
31
- let res = csrf && csrf !== params.csrf ? new ApiError(const_1.HTTP_UNAUTHORIZED, 'csrf')
32
- : await apis[ctx.path](params || {}, ctx);
35
+ let res = csrf && csrf !== ctx.params.csrf ? new ApiError(const_1.HTTP_UNAUTHORIZED, 'csrf')
36
+ : await apiFun(ctx.params || {}, ctx);
33
37
  if (isAsyncGenerator(res))
34
38
  res = (0, misc_1.asyncGeneratorToReadable)(res);
35
39
  if (res instanceof stream_1.Readable) { // Readable, we'll go SSE-mode
36
40
  res.pipe((0, sse_1.default)(ctx));
37
- const stillRes = res; // satisfy ts
41
+ const resAsReadable = res; // satisfy ts
38
42
  ctx.req.on('close', () => // by closing the generated stream, creator of the stream will know the request is over without having to access anything else
39
- stillRes.destroy());
43
+ resAsReadable.destroy());
40
44
  return;
41
45
  }
42
46
  if (res instanceof ApiError) {
package/src/config.js CHANGED
@@ -147,7 +147,8 @@ function setConfig1(k, newV, saveChanges = true) {
147
147
  var _a;
148
148
  if (lodash_1.default.isPlainObject(newV))
149
149
  newV = lodash_1.default.pickBy(newV, x => x !== undefined);
150
- if ((0, misc_1.same)(newV, (_a = configProps[k]) === null || _a === void 0 ? void 0 : _a.defaultValue))
150
+ const def = (_a = configProps[k]) === null || _a === void 0 ? void 0 : _a.defaultValue;
151
+ if ((0, misc_1.same)(newV !== null && newV !== void 0 ? newV : null, def !== null && def !== void 0 ? def : null))
151
152
  newV = undefined;
152
153
  if (started && (0, misc_1.same)(newV, state[k]))
153
154
  return; // no change
package/src/const.js CHANGED
@@ -27,7 +27,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
27
27
  return (mod && mod.__esModule) ? mod : { "default": mod };
28
28
  };
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.APP_PATH = exports.IS_WINDOWS = exports.HTTP_SERVER_ERROR = exports.HTTP_FOOL = exports.HTTP_RANGE_NOT_SATISFIABLE = 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_PARTIAL_CONTENT = exports.HTTP_NO_CONTENT = exports.HTTP_OK = exports.PLUGINS_PUB_URI = exports.API_URI = exports.ADMIN_URI = exports.FRONTEND_URI = exports.SPECIAL_URI = exports.COMPATIBLE_API_VERSION = exports.API_VERSION = exports.SESSION_DURATION = exports.DAY = exports.VERSION = exports.BUILD_TIMESTAMP = exports.HFS_STARTED = exports.ORIGINAL_CWD = exports.DEV = exports.argv = void 0;
30
+ exports.APP_PATH = exports.IS_WINDOWS = exports.HTTP_SERVER_ERROR = exports.HTTP_FOOL = exports.HTTP_RANGE_NOT_SATISFIABLE = exports.HTTP_PAYLOAD_TOO_LARGE = 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_PARTIAL_CONTENT = exports.HTTP_NO_CONTENT = exports.HTTP_OK = exports.PLUGINS_PUB_URI = exports.API_URI = exports.ADMIN_URI = exports.FRONTEND_URI = exports.SPECIAL_URI = exports.COMPATIBLE_API_VERSION = exports.API_VERSION = exports.SESSION_DURATION = exports.DAY = exports.VERSION = exports.BUILD_TIMESTAMP = exports.HFS_STARTED = exports.ORIGINAL_CWD = exports.DEV = exports.argv = void 0;
31
31
  const minimist_1 = __importDefault(require("minimist"));
32
32
  const fs = __importStar(require("fs"));
33
33
  const os_1 = require("os");
@@ -62,6 +62,7 @@ exports.HTTP_NOT_FOUND = 404;
62
62
  exports.HTTP_METHOD_NOT_ALLOWED = 405;
63
63
  exports.HTTP_NOT_ACCEPTABLE = 406;
64
64
  exports.HTTP_CONFLICT = 409;
65
+ exports.HTTP_PAYLOAD_TOO_LARGE = 413;
65
66
  exports.HTTP_RANGE_NOT_SATISFIABLE = 416;
66
67
  exports.HTTP_FOOL = 418;
67
68
  exports.HTTP_SERVER_ERROR = 500;
@@ -23,16 +23,37 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  __setModuleDefault(result, mod);
24
24
  return result;
25
25
  };
26
+ var __importDefault = (this && this.__importDefault) || function (mod) {
27
+ return (mod && mod.__esModule) ? mod : { "default": mod };
28
+ };
26
29
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.frontEndApis = void 0;
30
+ exports.notifyClient = exports.frontEndApis = void 0;
31
+ const apiMiddleware_1 = require("./apiMiddleware");
28
32
  const api_file_list_1 = require("./api.file_list");
29
33
  const api_auth = __importStar(require("./api.auth"));
30
34
  const config_1 = require("./config");
35
+ const events_1 = __importDefault(require("./events"));
31
36
  const customHeader = (0, config_1.defineConfig)('custom_header');
32
37
  exports.frontEndApis = {
33
38
  file_list: api_file_list_1.file_list,
34
39
  ...api_auth,
35
40
  config() {
36
41
  return Object.fromEntries([customHeader].map(x => [x.key(), x.get()]));
42
+ },
43
+ get_notifications({ channel }, ctx) {
44
+ const list = new apiMiddleware_1.SendListReadable();
45
+ list.ready(); // on chrome109 EventSource doesn't emit 'open' until something is sent
46
+ return list.events(ctx, {
47
+ [NOTIFICATION_PREFIX + channel](name, data) {
48
+ list.custom({ name, data });
49
+ }
50
+ });
37
51
  }
38
52
  };
53
+ function notifyClient(ctx, name, data) {
54
+ const { notificationChannel } = ctx.query;
55
+ if (notificationChannel)
56
+ events_1.default.emit(NOTIFICATION_PREFIX + notificationChannel, name, data);
57
+ }
58
+ exports.notifyClient = notifyClient;
59
+ const NOTIFICATION_PREFIX = 'notificationChannel:';
package/src/github.js CHANGED
@@ -28,6 +28,8 @@ async function downloadPlugin(repo, branch = '', overwrite) {
28
28
  if (!branch)
29
29
  branch = rec.default_branch;
30
30
  const short = repo.split('/')[1]; // second part, repo without the owner
31
+ if (!short)
32
+ return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, "bad repo");
31
33
  const folder2repo = getFolder2repo();
32
34
  const folder = overwrite ? lodash_1.default.findKey(folder2repo, x => x === repo) // use existing folder
33
35
  : short in folder2repo ? repo.replace('/', '-') // longer form only if another plugin is using short form
package/src/index.js CHANGED
@@ -23,6 +23,7 @@ const config_1 = require("./config");
23
23
  const assert_1 = require("assert");
24
24
  const lodash_1 = __importDefault(require("lodash"));
25
25
  const misc_1 = require("./misc");
26
+ //import body from 'koa-better-body'
26
27
  (0, assert_1.ok)(lodash_1.default.intersection(Object.keys(frontEndApis_1.frontEndApis), Object.keys(adminApis_1.adminApis)).length === 0); // they share same endpoints
27
28
  const keys = ((_a = process.env.COOKIE_SIGN_KEYS) === null || _a === void 0 ? void 0 : _a.split(',')) || [(0, misc_1.randomId)(30)];
28
29
  exports.app = new koa_1.default({ keys });
@@ -33,9 +34,9 @@ exports.app.use(middlewares_1.someSecurity)
33
34
  .use((0, log_1.log)())
34
35
  .use(throttler_1.throttler)
35
36
  .use(middlewares_1.gzipper)
36
- .use(middlewares_1.paramsDecoder)
37
37
  .use((0, plugins_1.pluginsMiddleware)())
38
38
  .use((0, koa_mount_1.default)(const_1.API_URI, (0, apiMiddleware_1.apiMiddleware)({ ...frontEndApis_1.frontEndApis, ...adminApis_1.adminApis })))
39
+ //.use(body({ multipart: false }))
39
40
  .use(middlewares_1.serveGuiAndSharedFiles)
40
41
  .on('error', errorHandler);
41
42
  function errorHandler(err) {
package/src/listen.js CHANGED
@@ -132,6 +132,8 @@ function startServer(srv, { port, host }) {
132
132
  try {
133
133
  if (port < 0 || !host && !await testIpV4()) // !host means ipV4+6, and if v4 port alone is busy we won't be notified of the failure, so we'll first test it on its own
134
134
  return resolve(0);
135
+ // from a few tests, this seems enough to support the expect-100 http/1.1 mechanism, at least with curl -T, not used by chrome|firefox anyway
136
+ srv.on('checkContinue', (req, res) => srv.emit('request', req, res));
135
137
  port = await listen(host);
136
138
  if (port)
137
139
  console.log(srv.name, "serving on", host || "any network", ':', port);
@@ -223,9 +225,10 @@ function printUrls(port, proto) {
223
225
  if (!nets || ignore.test(name))
224
226
  continue;
225
227
  lodash_1.default.remove(nets, 'internal');
226
- if (!nets.length)
228
+ const first = nets[0];
229
+ if (!first)
227
230
  continue;
228
- const best = lodash_1.default.find(nets, { family: 'IPv4' }) || nets[0];
231
+ const best = lodash_1.default.find(nets, { family: 'IPv4' }) || first;
229
232
  const appendPort = port === (proto === 'https' ? 443 : 80) ? '' : ':' + port;
230
233
  let { address } = best;
231
234
  if (address.includes(':'))
package/src/log.js CHANGED
@@ -81,7 +81,7 @@ const logRotation = (0, config_1.defineConfig)('log_rotation', 'weekly');
81
81
  function log() {
82
82
  const debounce = lodash_1.default.debounce(cb => cb(), 1000);
83
83
  return async (ctx, next) => {
84
- var _a;
84
+ var _a, _b;
85
85
  await next();
86
86
  const isError = ctx.status >= 400;
87
87
  const logger = isError && accessErrorLog || accessLogger;
@@ -112,7 +112,7 @@ function log() {
112
112
  }
113
113
  }
114
114
  const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s\n'; // Apache's Common Log Format
115
- const date = a[2] + '/' + a[1] + '/' + a[3] + ':' + a[4] + ' ' + a[5].slice(3);
115
+ const date = a[2] + '/' + a[1] + '/' + a[3] + ':' + a[4] + ' ' + ((_b = a[5]) === null || _b === void 0 ? void 0 : _b.slice(3));
116
116
  const user = (0, perm_1.getCurrentUsername)(ctx);
117
117
  events_1.default.emit(logger.name, Object.assign(lodash_1.default.pick(ctx, ['ip', 'method', 'status', 'length']), { user, ts: now, uri: ctx.path }));
118
118
  console.debug(ctx.status, ctx.method, ctx.path);
@@ -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.paramsDecoder = exports.prepareState = exports.getProxyDetected = exports.someSecurity = exports.serveGuiAndSharedFiles = exports.sessions = exports.headRequests = exports.gzipper = void 0;
7
+ 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");
@@ -23,8 +23,9 @@ const basic_auth_1 = __importDefault(require("basic-auth"));
23
23
  const tssrp6a_1 = require("tssrp6a");
24
24
  const api_auth_1 = require("./api.auth");
25
25
  const path_1 = require("path");
26
- const fs_1 = require("fs");
27
26
  const promises_1 = require("stream/promises");
27
+ const formidable_1 = __importDefault(require("formidable"));
28
+ const upload_1 = require("./upload");
28
29
  exports.gzipper = (0, koa_compress_1.default)({
29
30
  threshold: 2048,
30
31
  gzip: { flush: require('zlib').constants.Z_SYNC_FLUSH },
@@ -75,11 +76,27 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
75
76
  const folder = await (0, vfs_1.urlToNode)((0, path_1.dirname)(decPath), ctx, vfs_1.vfs, v => rest = v + '/' + rest);
76
77
  if (!folder)
77
78
  return ctx.status = const_1.HTTP_NOT_FOUND;
78
- return await receiveUpload(folder, rest, ctx.req, ctx);
79
+ const dest = (0, upload_1.uploadWriter)(folder, rest, ctx);
80
+ if (dest) {
81
+ await (0, promises_1.pipeline)(ctx.req, dest);
82
+ ctx.body = {};
83
+ }
84
+ return;
79
85
  }
80
86
  const node = await (0, vfs_1.urlToNode)(path, ctx);
81
87
  if (!node)
82
88
  return ctx.status = const_1.HTTP_NOT_FOUND;
89
+ if (ctx.method === 'POST') { // curl -F upload=@file url/
90
+ ctx.body = {};
91
+ const form = (0, formidable_1.default)({
92
+ maxFileSize: Infinity,
93
+ //@ts-ignore wrong in the .d.ts file
94
+ fileWriteStreamHandler: f => (0, upload_1.uploadWriter)(node, f.originalFilename, ctx)
95
+ });
96
+ form.parse(ctx.req);
97
+ await (0, stream_1.once)(form, 'end').catch(() => { });
98
+ return;
99
+ }
83
100
  const canRead = (0, vfs_1.hasPermission)(node, 'can_read', ctx);
84
101
  const isFolder = await (0, vfs_1.nodeIsDirectory)(node);
85
102
  if (isFolder && !path.endsWith('/'))
@@ -110,15 +127,6 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
110
127
  return serveFrontendFiles(ctx, next);
111
128
  };
112
129
  exports.serveGuiAndSharedFiles = serveGuiAndSharedFiles;
113
- async function receiveUpload(base, path, stream, ctx) {
114
- if (!base.source || !(0, vfs_1.hasPermission)(base, 'can_upload', ctx))
115
- return ctx.status = base.can_upload === false ? const_1.HTTP_FORBIDDEN : const_1.HTTP_UNAUTHORIZED;
116
- path = (0, path_1.join)(base.source, path);
117
- await (0, misc_1.prepareFolder)(path);
118
- const dest = (0, fs_1.createWriteStream)(path);
119
- await (0, promises_1.pipeline)(stream, dest);
120
- ctx.body = '{}';
121
- }
122
130
  let proxyDetected = false;
123
131
  const someSecurity = async (ctx, next) => {
124
132
  ctx.request.ip = (0, connections_1.normalizeIp)(ctx.ip);
@@ -128,14 +136,14 @@ const someSecurity = async (ctx, next) => {
128
136
  if (const_1.DEV && proxy && [process.env.FRONTEND_PROXY, process.env.ADMIN_PROXY].includes(ctx.get('X-Forwarded-port')))
129
137
  proxy = '';
130
138
  if ((0, misc_1.dirTraversal)(decodeURI(ctx.path)))
131
- return ctx.status = const_1.HTTP_FOOL;
139
+ return ctx.status = 418;
132
140
  if ((0, block_1.applyBlock)(ctx.socket, ctx.ip))
133
141
  return;
134
142
  proxyDetected || (proxyDetected = proxy > '');
135
143
  ctx.state.proxiedFor = proxy;
136
144
  }
137
145
  catch (_a) {
138
- return ctx.status = const_1.HTTP_FOOL;
146
+ return ctx.status = 418;
139
147
  }
140
148
  return next();
141
149
  };
@@ -171,25 +179,3 @@ async function srpCheck(username, password) {
171
179
  const clientRes2 = await clientRes1.step2(BigInt(salt), BigInt(pubKey));
172
180
  return await step1.step2(clientRes2.A, clientRes2.M1).then(() => true, () => false);
173
181
  }
174
- // unify get/post parameters, with JSON decoding to not be limited to strings
175
- const paramsDecoder = async (ctx, next) => {
176
- ctx.params = ctx.method === 'POST' ? (0, misc_1.tryJson)(await stream2string(ctx.req))
177
- : (0, misc_1.objSameKeys)(ctx.query, x => Array.isArray(x) ? x : (0, misc_1.tryJson)(x));
178
- await next();
179
- };
180
- exports.paramsDecoder = paramsDecoder;
181
- async function stream2string(stream) {
182
- return new Promise((resolve, reject) => {
183
- let data = '';
184
- stream.on('data', chunk => data += chunk);
185
- stream.on('error', reject);
186
- stream.on('end', () => {
187
- try {
188
- resolve(data);
189
- }
190
- catch (e) {
191
- reject(e);
192
- }
193
- });
194
- });
195
- }
package/src/misc.js CHANGED
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
18
18
  return (mod && mod.__esModule) ? mod : { "default": mod };
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.tryJson = exports.same = exports.isLocalHost = exports.with_ = exports.typedKeys = exports.objRenameKey = exports.onOff = exports.pendingPromise = exports.onlyTruthy = exports.truthy = exports.pattern2filter = exports.onFirstEvent = exports.onProcessExit = exports.randomId = exports.getOrSet = exports.wantArray = exports.wait = exports.objSameKeys = exports.setHidden = exports.prefix = exports.enforceFinal = exports.debounceAsync = void 0;
21
+ exports.try_ = exports.stream2string = exports.tryJson = exports.same = exports.isLocalHost = exports.with_ = exports.typedKeys = exports.objRenameKey = exports.onOff = exports.pendingPromise = exports.onlyTruthy = exports.truthy = exports.pattern2filter = exports.onFirstEvent = exports.onProcessExit = exports.randomId = exports.getOrSet = exports.wantArray = exports.wait = exports.objSameKeys = exports.setHidden = exports.prefix = exports.enforceFinal = exports.debounceAsync = void 0;
22
22
  const path_1 = require("path");
23
23
  const lodash_1 = __importDefault(require("lodash"));
24
24
  const assert_1 = __importDefault(require("assert"));
@@ -158,3 +158,28 @@ function tryJson(s) {
158
158
  catch (_a) { }
159
159
  }
160
160
  exports.tryJson = tryJson;
161
+ async function stream2string(stream) {
162
+ return new Promise((resolve, reject) => {
163
+ let data = '';
164
+ stream.on('data', chunk => data += chunk);
165
+ stream.on('error', reject);
166
+ stream.on('end', () => {
167
+ try {
168
+ resolve(data);
169
+ }
170
+ catch (e) {
171
+ reject(e);
172
+ }
173
+ });
174
+ });
175
+ }
176
+ exports.stream2string = stream2string;
177
+ function try_(cb, onException) {
178
+ try {
179
+ return cb();
180
+ }
181
+ catch (e) {
182
+ return onException === null || onException === void 0 ? void 0 : onException(e);
183
+ }
184
+ }
185
+ exports.try_ = try_;
package/src/plugins.js CHANGED
@@ -92,9 +92,8 @@ function pluginsMiddleware() {
92
92
  var _a;
93
93
  const after = [];
94
94
  // run middleware plugins
95
- for (const id in plugins)
95
+ for (const [id, pl] of Object.entries(plugins))
96
96
  try {
97
- const pl = plugins[id];
98
97
  const res = await ((_a = pl.middleware) === null || _a === void 0 ? void 0 : _a.call(pl, ctx));
99
98
  if (res === true)
100
99
  ctx.pluginStopped = true;
@@ -225,7 +224,7 @@ async function rescan() {
225
224
  try {
226
225
  const alreadyRunning = plugins[id];
227
226
  console.log(alreadyRunning ? "reloading plugin" : "loading plugin", id);
228
- const { init, ...data } = await Promise.resolve().then(() => __importStar(require(module)));
227
+ const { init, ...data } = await import(module);
229
228
  delete data.default;
230
229
  deleteModule(require.resolve(module)); // avoid caching at next import
231
230
  calculateBadApi(data);
@@ -277,8 +276,7 @@ async function rescan() {
277
276
  }
278
277
  });
279
278
  }
280
- for (const id in foundDisabled) {
281
- const p = foundDisabled[id];
279
+ for (const [id, p] of Object.entries(foundDisabled)) {
282
280
  const a = availablePlugins[id];
283
281
  if ((0, misc_1.same)(a, p))
284
282
  continue;
@@ -293,9 +291,9 @@ async function rescan() {
293
291
  delete availablePlugins[id];
294
292
  events_1.default.emit('pluginUninstalled', id);
295
293
  }
296
- for (const id in plugins)
294
+ for (const [id, p] of Object.entries(plugins))
297
295
  if (!found.includes(id))
298
- await plugins[id].unload();
296
+ await p.unload();
299
297
  }
300
298
  exports.rescan = rescan;
301
299
  function deleteModule(id) {
package/src/serveFile.js CHANGED
@@ -81,8 +81,10 @@ function getRange(ctx, totalSize) {
81
81
  ctx.response.length = totalSize;
82
82
  return;
83
83
  }
84
- const ranges = range.split('=')[1];
85
- if (ranges.includes(','))
84
+ const [unit, ranges] = range.split('=');
85
+ if (unit !== 'bytes')
86
+ return ctx.throw(const_1.HTTP_BAD_REQUEST, 'bad range unit');
87
+ if (ranges === null || ranges === void 0 ? void 0 : ranges.includes(','))
86
88
  return ctx.throw(const_1.HTTP_BAD_REQUEST, 'multi-range not supported');
87
89
  let bytes = ranges === null || ranges === void 0 ? void 0 : ranges.split('-');
88
90
  if (!(bytes === null || bytes === void 0 ? void 0 : bytes.length))