hfs 0.38.3 → 0.40.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{height:100dvh;--bg: #fff;--text: #555;--ghost-contrast: #8882;--faint-contrast: #8884;--mild-contrast: #8886;--good-contrast: #000a;--button-bg: #68a;--button-text: #eaeaea;--focus-color: #468;--separator: " \2013 "}:root .theme-dark{--bg: #000;--text: #999;--good-contrast: #fffa;--button-bg: #345;--button-text: #999;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}:root .theme-dark button.toggled{color:#eee}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:not([type=checkbox]),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;width:100%}input[type=checkbox]{transform:scale(1.7);accent-color:var(--button-bg)}label input[type=checkbox]{margin-right:.8em}select{text-align:center}.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{color:#fff;text-shadow:0 0 3px #fff}button:focus-visible,.breadcrumb:focus-visible{outline:3px solid var(--focus-color)}a>button{width:100%}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}.before-sliding{width:0!important;flex:0!important;margin:0!important;height:0!important;padding:0!important;overflow:hidden!important;transition:all .5s}.show-sliding{transition:all .5s;overflow:clip;flex:1;white-space:nowrap}.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,#filter-bar>span{font-size:90%}#folder-stats{margin:.4em 0 0 .5em;float:right}#folder-stats .icon{margin-right:.3em}#filter{flex:1;box-sizing:border-box}#filter-bar{display:flex;align-items:center;gap:.8em;margin:.2em 0 0;padding:2px 0 1px 11px;height:1.8em}#filter-bar input[type=checkbox]{margin-top:.5em}#filter-bar span:empty{display:none}ul.dir{flex:1;padding:0;margin:0;clear:both}ul.dir>p{text-align:center}ul.dir li{display:block;list-style-type:none;padding:.3em .3em .4em;border-bottom:1px solid var(--faint-contrast)}ul.dir li:nth-of-type(odd){background-color:var(--ghost-contrast)}ul.dir li input[type=checkbox]{margin:0 .8em}ul.dir li a:last-of-type{word-break:break-word;padding-right:.3em}ul.dir li a .icon{margin-right:.3em}ul.dir li a:hover{text-decoration:underline}ul.dir li .entry-props{float:right;font-size:90%;margin-left:4px;margin-top:.3em;font-variant-numeric:tabular-nums}ul.dir li .entry-props .entry-size-unit{margin-left:.3em}ul.dir li>div:last-of-type{clear:both}ul.dir li.page-separator{margin-top:1em;position:relative}ul.dir li.page-separator:before{content:attr(label);position:absolute;top:-1.8em;font-size:smaller;margin-left:calc(50% - 1em);opacity:.9}#menu-bar{display:flex;justify-content:space-evenly;flex-wrap:wrap}#menu-bar>*{flex:1;margin:.1em}#menu-bar button{padding-left:0;padding-right:0}#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-alert .dialog-content{text-align:center}.dialog-alert .dialog-content p{text-align:left;display:inline-block}.dialog{min-width:11em;--color: var(--button-bg)}#paging{position:sticky;bottom:0;display:flex;gap:.1em;background:var(--bg);padding:0 .2em .2em}#paging>button{z-index:1}#paging button{box-shadow:0 0 .3em .3em #0003}#paging #paging-middle{padding:0 .5em;margin:0 -.3em;display:flex;gap:.5em;flex:1;overflow-x:auto}#paging #paging-middle>button{flex:1;padding-top:0;padding-bottom:0}#paging button{background:var(--button-bg);text-align:center;white-space:nowrap;padding:.5em}.upload-progress:before{content:var(--separator)}.entry-size:after{content:var(--separator)}.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}.dialog-login form{display:flex;flex-direction:column;gap:1.2em}.dialog-login label{display:block;margin-bottom:.5em;margin-left:.1em}@media (min-width: 42em){body{scrollbar-width:thin;scrollbar-color:var(--button-bg) var(--ghost-contrast)}body::-webkit-scrollbar{width:12px}body::-webkit-scrollbar-track{background:var(--ghost-contrast)}body::-webkit-scrollbar-thumb{background-color:var(--button-bg);border-radius:20px;border:1px solid var(--ghost-contrast)}}@media (max-width: 42em){:root{--ghost-contrast: #8883}body,button,select{font-size:14pt}#menu-bar button label,#filter-bar button label{display:none}#filter-bar{margin-top:.4em}#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);display:flex;justify-content:center}.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%;position:absolute}.dialog-icon~.dialog-title{text-align:center}.dialog-closer{border-radius:0 .8em;right:0;padding:0;background-color:#c88}.dialog-icon~.dialog-content{margin-top:2em}.dialog-type{left:0;top:0;overflow:hidden;line-height:1.7em;opacity:.8}.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: 42em){.dialog-icon{font-size:120%}.dialog-icon~.dialog-content{margin-top:2.5em}.dialog-title{margin-top:-.2em}}.dialog-prompt label{display:block;margin-bottom:.5em;margin-left:.1em}
@@ -1,4 +1,4 @@
1
- import{c as SF}from"./index-33978702.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-31f0430a.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
@@ -1,7 +1,6 @@
1
1
  @font-face {
2
2
  font-family: 'fontello';
3
- src: url('fontello.woff2?1378305') format('woff2');
4
- font-weight: normal;
3
+ src: url('fontello.woff2?66723712') format('woff2'); font-weight: normal;
5
4
  font-style: normal;
6
5
  }
7
6
  /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
@@ -10,7 +9,7 @@
10
9
  @media screen and (-webkit-min-device-pixel-ratio:0) {
11
10
  @font-face {
12
11
  font-family: 'fontello';
13
- src: url('../font/fontello.svg?1378305#fontello') format('svg');
12
+ src: url('../font/fontello.svg?66723712#fontello') format('svg');
14
13
  }
15
14
  }
16
15
  */
@@ -50,11 +49,9 @@
50
49
  }
51
50
 
52
51
  .fa-cog:before { content: '\e800'; } /* '' */
53
- .fa-check-circled:before { content: '\e801'; } /* '' */
54
52
  .fa-doc:before { content: '\e802'; } /* '' */
55
53
  .fa-stop:before { content: '\e803'; } /* '' */
56
- .fa-download:before { content: '\e804'; } /* '' */
57
- .fa-upload:before { content: '\e805'; } /* '' */
54
+ .fa-play:before { content: '\e804'; } /* '' */
58
55
  .fa-cancel:before { content: '\e806'; } /* '' */
59
56
  .fa-edit:before { content: '\e807'; } /* '' */
60
57
  .fa-check:before { content: '\e808'; } /* '' */
@@ -62,18 +59,19 @@
62
59
  .fa-user:before { content: '\e80a'; } /* '' */
63
60
  .fa-home:before { content: '\e80b'; } /* '' */
64
61
  .fa-key:before { content: '\e80c'; } /* '' */
65
- .fa-to-start:before { content: '\e80e'; } /* '' */
62
+ .fa-trash:before { content: '\e80d'; } /* '' */
63
+ .fa-to_start:before { content: '\e80e'; } /* '' */
66
64
  .fa-retweet:before { content: '\e80f'; } /* '' */
67
- .fa-to-end:before { content: '\e810'; } /* '' */
68
- .fa-cancel-circled:before { content: '\e811'; } /* '' */
65
+ .fa-to_end:before { content: '\e810'; } /* '' */
69
66
  .fa-search:before { content: '\e813'; } /* '' */
70
67
  .fa-logout:before { content: '\e814'; } /* '' */
71
68
  .fa-spin6:before { content: '\e839'; } /* '' */
72
69
  .fa-crown:before { content: '\e844'; } /* '' */
70
+ .fa-download:before { content: '\f02e'; } /* '' */
71
+ .fa-upload:before { content: '\f02f'; } /* '' */
73
72
  .fa-filter:before { content: '\f0b0'; } /* '' */
74
73
  .fa-menu:before { content: '\f0c9'; } /* '' */
75
- .fa-quote-left:before { content: '\f10d'; } /* '' */
74
+ .fa-quote:before { content: '\f10d'; } /* '' */
76
75
  .fa-unlink:before { content: '\f127'; } /* '' */
77
76
  .fa-level-up:before { content: '\f148'; } /* '' */
78
- .fa-file-archive:before { content: '\f1c6'; } /* '' */
79
- .fa-trash:before { content: '\e80d'; } /* '' */
77
+ .fa-archive:before { content: '\f1c6'; } /* '' */
Binary file
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" />
6
6
  <link href="/fontello.css" rel="stylesheet" />
7
7
 
8
- <script type="module" crossorigin src="/assets/index-869b7be6.js"></script>
9
- <link rel="stylesheet" href="/assets/index-6d4e81f7.css">
8
+ <script type="module" crossorigin src="/assets/index-31f0430a.js"></script>
9
+ <link rel="stylesheet" href="/assets/index-ffbcd4c9.css">
10
10
  </head>
11
11
  <body>
12
12
  <noscript>You need to enable JavaScript to run this app.</noscript>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.38.3",
3
+ "version": "0.40.0",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -27,7 +27,7 @@ exports.init = api => {
27
27
  if (!ctx.path.startsWith(api.const.API_URI) || ctx.params.path === undefined) return
28
28
  let { referer } = ctx.headers
29
29
  referer &&= new URL(referer).pathname
30
- if (referer?.startsWith(api.const.ADMIN_URI)) return
30
+ if (referer?.startsWith(ctx.state.revProxyPath + api.const.ADMIN_URI)) return
31
31
  toModify = ctx.params
32
32
  }
33
33
  const hosts = api.getConfig('hosts')
package/src/api.auth.js CHANGED
@@ -117,7 +117,7 @@ exports.logout = logout;
117
117
  const refresh_session = async ({}, ctx) => {
118
118
  return !ctx.session ? new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR) : {
119
119
  username: (0, perm_1.getCurrentUsername)(ctx),
120
- adminUrl: (0, adminApis_1.ctxAdminAccess)(ctx) ? const_1.ADMIN_URI : undefined,
120
+ adminUrl: (0, adminApis_1.ctxAdminAccess)(ctx) ? ctx.state.revProxyPath + const_1.ADMIN_URI : undefined,
121
121
  ...makeExp(),
122
122
  };
123
123
  };
@@ -22,7 +22,9 @@ const file_list = async ({ path, offset, limit, search, omit, sse }, ctx) => {
22
22
  if ((0, misc_1.dirTraversal)(search))
23
23
  return fail(const_1.HTTP_FOOL);
24
24
  if (node.default)
25
- return (sse ? list.custom : lodash_1.default.identity)({ redirect: path }); // sse will wrap the object in a 'custom' message, otherwise we plainly return the object
25
+ return (sse ? list.custom : lodash_1.default.identity)({
26
+ redirect: path // tell the browser to access the folder (instead of using this api), so it will get the default file
27
+ });
26
28
  if (!await (0, vfs_1.nodeIsDirectory)(node))
27
29
  return fail(const_1.HTTP_METHOD_NOT_ALLOWED);
28
30
  offset = Number(offset);
package/src/api.vfs.js CHANGED
@@ -25,6 +25,7 @@ const apis = {
25
25
  defaultPerms: vfs_1.defaultPerms,
26
26
  };
27
27
  async function recur(node) {
28
+ var _a;
28
29
  const stats = Boolean(node.source) && await (0, promises_1.stat)(node.source).catch(e => false);
29
30
  const isDir = !node.source || stats && stats.isDirectory();
30
31
  const copyStats = stats ? lodash_1.default.pick(stats, ['size', 'ctime', 'mtime'])
@@ -35,7 +36,8 @@ const apis = {
35
36
  return {
36
37
  ...copyStats,
37
38
  ...node,
38
- website: isDir && node.source && await (0, promises_1.stat)((0, path_1.join)(node.source, 'index.html')).then(() => true, () => undefined)
39
+ website: Boolean((_a = node.children) === null || _a === void 0 ? void 0 : _a.find((0, vfs_1.isSameFilenameAs)('index.html')))
40
+ || isDir && node.source && await (0, promises_1.stat)((0, path_1.join)(node.source, 'index.html')).then(() => true, () => undefined)
39
41
  || undefined,
40
42
  name: isRoot ? undefined : (0, vfs_1.getNodeName)(node),
41
43
  type: isDir ? 'folder' : undefined,
@@ -58,6 +60,8 @@ const apis = {
58
60
  return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, 'item with same name already present in destination');
59
61
  const oldParent = await urlToNodeOriginal((0, path_1.dirname)(from));
60
62
  lodash_1.default.pull(oldParent.children, fromNode);
63
+ if (lodash_1.default.isEmpty(oldParent.children))
64
+ delete oldParent.children;
61
65
  (parentNode.children || (parentNode.children = [])).push(fromNode);
62
66
  await (0, vfs_1.saveVfs)();
63
67
  return {};
@@ -91,7 +95,8 @@ const apis = {
91
95
  if ((0, misc_1.isWindowsDrive)(source))
92
96
  source += '\\'; // slash must be included, otherwise it will refer to the cwd of that drive
93
97
  n.children || (n.children = []);
94
- if (n.children.find(x => x.source === source || (0, vfs_1.getNodeName)(x) === name))
98
+ const sameName = (0, vfs_1.isSameFilenameAs)(name);
99
+ if (n.children.find(x => source && source === x.source || sameName(x)))
95
100
  return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, 'already present');
96
101
  n.children.unshift({ source, name });
97
102
  await (0, vfs_1.saveVfs)();
@@ -31,8 +31,10 @@ function apiMiddleware(apis) {
31
31
  // we don't rely on SameSite cookie option because it's https-only
32
32
  let res;
33
33
  try {
34
- res = csrf && csrf !== ctx.params.csrf ? new ApiError(const_1.HTTP_UNAUTHORIZED, 'csrf')
35
- : await apiFun(ctx.params || {}, ctx);
34
+ if (params.path)
35
+ params.path = (0, misc_1.removeStarting)(ctx.state.revProxyPath, params.path);
36
+ res = csrf && csrf !== params.csrf ? new ApiError(const_1.HTTP_UNAUTHORIZED, 'csrf')
37
+ : await apiFun(params || {}, ctx);
36
38
  }
37
39
  catch (e) {
38
40
  res = e;
package/src/const.js CHANGED
@@ -38,7 +38,7 @@ exports.DEV = process.env.DEV || exports.argv.dev ? 'DEV' : '';
38
38
  exports.ORIGINAL_CWD = process.cwd();
39
39
  exports.HFS_STARTED = new Date();
40
40
  const PKG_PATH = (0, path_1.join)(__dirname, '..', 'package.json');
41
- exports.BUILD_TIMESTAMP = "2023-03-10T14:41:56.924Z";
41
+ exports.BUILD_TIMESTAMP = "2023-03-13T19:06:29.589Z";
42
42
  const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
43
43
  exports.VERSION = pkg.version;
44
44
  exports.DAY = 86400000;
package/src/log.js CHANGED
@@ -94,7 +94,7 @@ function log() {
94
94
  if (!stream)
95
95
  return;
96
96
  logger.last = now;
97
- if (rotate && last) { // rotation enabled and a fµile exists?
97
+ if (rotate && last) { // rotation enabled and a file exists?
98
98
  const passed = Number(now) - Number(last)
99
99
  - 3600000; // be pessimistic and count a possible DST change
100
100
  if (rotate === 'm' && (passed >= 31 * const_1.DAY || now.getMonth() !== last.getMonth())
@@ -79,8 +79,8 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
79
79
  return next();
80
80
  if (path.startsWith(const_2.FRONTEND_URI))
81
81
  return serveFrontendPrefixed(ctx, next);
82
- if (path + '/' === const_1.ADMIN_URI)
83
- return ctx.redirect(const_1.ADMIN_URI);
82
+ if (path.length === const_1.ADMIN_URI.length - 1 && const_1.ADMIN_URI.startsWith(path))
83
+ return ctx.redirect(ctx.state.revProxyPath + const_1.ADMIN_URI);
84
84
  if (path.startsWith(const_1.ADMIN_URI))
85
85
  return serveAdminPrefixed(ctx, next);
86
86
  if (ctx.method === 'PUT') { // curl -T file url/
@@ -97,7 +97,7 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
97
97
  return;
98
98
  }
99
99
  if (ctx.originalUrl === '/favicon.ico' && adminApis_1.favicon.get()) // originalUrl to not be subject to changes (vhosting plugin)
100
- return (0, serveFile_1.serveFile)(adminApis_1.favicon.get())(ctx, next);
100
+ return (0, serveFile_1.serveFile)(ctx, adminApis_1.favicon.get());
101
101
  const node = await (0, vfs_1.urlToNode)(path, ctx);
102
102
  if (!node)
103
103
  return ctx.status = const_1.HTTP_NOT_FOUND;
@@ -115,9 +115,9 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
115
115
  const canRead = (0, vfs_1.hasPermission)(node, 'can_read', ctx);
116
116
  const isFolder = await (0, vfs_1.nodeIsDirectory)(node);
117
117
  if (isFolder && !path.endsWith('/'))
118
- return ctx.redirect(ctx.originalUrl + '/');
118
+ return ctx.redirect(ctx.state.revProxyPath + ctx.originalUrl + '/');
119
119
  if (canRead && !isFolder)
120
- return node.source ? (0, serveFile_1.serveFileNode)(node)(ctx, next)
120
+ return node.source ? (0, serveFile_1.serveFileNode)(ctx, node)
121
121
  : next();
122
122
  if (!canRead) {
123
123
  ctx.status = (0, vfs_1.cantReadStatusCode)(node);
@@ -136,7 +136,7 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
136
136
  if (node.default) {
137
137
  const def = await (0, vfs_1.urlToNode)(path + node.default, ctx);
138
138
  return !def ? next()
139
- : (0, vfs_1.hasPermission)(def, 'can_read', ctx) ? (0, serveFile_1.serveFileNode)(def)(ctx, next)
139
+ : (0, vfs_1.hasPermission)(def, 'can_read', ctx) ? (0, serveFile_1.serveFileNode)(ctx, def)
140
140
  : ctx.status = (0, vfs_1.cantReadStatusCode)(def);
141
141
  }
142
142
  return serveFrontendFiles(ctx, next);
@@ -173,6 +173,7 @@ const prepareState = async (ctx, next) => {
173
173
  // calculate these once and for all
174
174
  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);
175
175
  const conn = ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
176
+ ctx.state.revProxyPath = ctx.get('x-forwarded-prefix');
176
177
  await next();
177
178
  if (conn)
178
179
  (0, connections_1.updateConnection)(conn, { ctx });
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.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.waitFor = exports.wait = exports.newObj = 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.waitFor = exports.wait = exports.newObj = exports.setHidden = exports.prefix = exports.removeStarting = 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"));
@@ -31,6 +31,10 @@ function enforceFinal(sub, s) {
31
31
  return s.endsWith(sub) ? s : s + sub;
32
32
  }
33
33
  exports.enforceFinal = enforceFinal;
34
+ function removeStarting(sub, s) {
35
+ return s.startsWith(sub) ? s.slice(sub.length) : s;
36
+ }
37
+ exports.removeStarting = removeStarting;
34
38
  function prefix(pre, v, post = '') {
35
39
  return v ? pre + v + post : '';
36
40
  }
package/src/plugins.js CHANGED
@@ -90,7 +90,7 @@ exports.getPluginConfigFields = getPluginConfigFields;
90
90
  function pluginsMiddleware() {
91
91
  return async (ctx, next) => {
92
92
  var _a;
93
- const after = [];
93
+ const after = {};
94
94
  // run middleware plugins
95
95
  for (const [id, pl] of Object.entries(plugins))
96
96
  try {
@@ -98,11 +98,10 @@ function pluginsMiddleware() {
98
98
  if (res === true)
99
99
  ctx.pluginStopped = true;
100
100
  if (typeof res === 'function')
101
- after.push(res);
101
+ after[id] = res;
102
102
  }
103
103
  catch (e) {
104
- console.log('error middleware plugin', id, String(e));
105
- console.debug(e);
104
+ printError(id, e);
106
105
  }
107
106
  // expose public plugins' files
108
107
  const { path } = ctx;
@@ -111,14 +110,23 @@ function pluginsMiddleware() {
111
110
  const a = path.substring(const_1.PLUGINS_PUB_URI.length).split('/');
112
111
  const name = a.shift();
113
112
  if (plugins.hasOwnProperty(name)) // do it only if the plugin is loaded
114
- await (0, serveFile_1.serveFile)(plugins[name].folder + '/public/' + a.join('/'), 'auto')(ctx, next);
113
+ await (0, serveFile_1.serveFile)(ctx, plugins[name].folder + '/public/' + a.join('/'), 'auto');
115
114
  return;
116
115
  }
117
116
  await next();
118
117
  }
119
- for (const f of after)
120
- await f();
118
+ for (const [id, f] of Object.entries(after))
119
+ try {
120
+ await f();
121
+ }
122
+ catch (e) {
123
+ printError(id, e);
124
+ }
121
125
  };
126
+ function printError(id, e) {
127
+ console.log('error middleware plugin', id, String(e));
128
+ console.debug(e);
129
+ }
122
130
  }
123
131
  exports.pluginsMiddleware = pluginsMiddleware;
124
132
  class Plugin {
@@ -222,10 +230,11 @@ async function rescan() {
222
230
  continue;
223
231
  const module = (0, path_1.resolve)(f);
224
232
  const { unwatch } = (0, watchLoad_1.watchLoad)(f, async () => {
233
+ var _a;
225
234
  try {
226
235
  const alreadyRunning = plugins[id];
227
236
  console.log(alreadyRunning ? "reloading plugin" : "loading plugin", id);
228
- const { init, ...data } = await Promise.resolve().then(() => __importStar(require(module)));
237
+ const { init, ...data } = await (_a = module, Promise.resolve().then(() => __importStar(require(_a))));
229
238
  delete data.default;
230
239
  deleteModule(require.resolve(module)); // avoid caching at next import
231
240
  calculateBadApi(data);
package/src/serveFile.js CHANGED
@@ -15,63 +15,61 @@ const lodash_1 = __importDefault(require("lodash"));
15
15
  const path_1 = __importDefault(require("path"));
16
16
  const util_1 = require("util");
17
17
  const allowedReferer = (0, config_1.defineConfig)('allowed_referer', '');
18
- function serveFileNode(node) {
18
+ function serveFileNode(ctx, node) {
19
+ var _a;
19
20
  const { source, mime } = node;
20
21
  const name = (0, vfs_1.getNodeName)(node);
21
22
  const mimeString = typeof mime === 'string' ? mime
22
23
  : lodash_1.default.find(mime, (val, mask) => (0, micromatch_1.isMatch)(name, mask));
23
- return (ctx, next) => {
24
- var _a;
25
- const allowed = allowedReferer.get();
26
- if (allowed) {
27
- const ref = (_a = /\/\/([^:/]+)/.exec(ctx.get('referer'))) === null || _a === void 0 ? void 0 : _a[1]; // extract host from url
28
- if (ref && ref !== host() // automatic accept if referer is basically the hosting domain
29
- && !(0, micromatch_1.isMatch)(ref, allowed))
30
- return ctx.status = const_1.HTTP_FORBIDDEN;
31
- function host() {
32
- const s = ctx.get('host');
33
- return s[0] === '[' ? s.slice(1, s.indexOf(']')) : s === null || s === void 0 ? void 0 : s.split(':')[0];
34
- }
24
+ const allowed = allowedReferer.get();
25
+ if (allowed) {
26
+ const ref = (_a = /\/\/([^:/]+)/.exec(ctx.get('referer'))) === null || _a === void 0 ? void 0 : _a[1]; // extract host from url
27
+ if (ref && ref !== host() // automatic accept if referer is basically the hosting domain
28
+ && !(0, micromatch_1.isMatch)(ref, allowed))
29
+ return ctx.status = const_1.HTTP_FORBIDDEN;
30
+ function host() {
31
+ const s = ctx.get('host');
32
+ return s[0] === '[' ? s.slice(1, s.indexOf(']')) : s === null || s === void 0 ? void 0 : s.split(':')[0];
35
33
  }
36
- ctx.vfsNode = node; // useful to tell service files from files shared by the user
37
- return serveFile(source || '', mimeString)(ctx, next);
38
- };
34
+ }
35
+ ctx.vfsNode = node; // useful to tell service files from files shared by the user
36
+ return serveFile(ctx, source || '', mimeString);
39
37
  }
40
38
  exports.serveFileNode = serveFileNode;
41
39
  const mimeCfg = (0, config_1.defineConfig)('mime', { '*': 'auto' });
42
- function serveFile(source, mime, content) {
43
- return async (ctx) => {
44
- if (!source)
45
- return;
46
- const fn = path_1.default.basename(source);
47
- mime = mime !== null && mime !== void 0 ? mime : lodash_1.default.find(mimeCfg.get(), (v, k) => k > '' && (0, micromatch_1.isMatch)(fn, k)); // isMatch throws on an empty string
48
- if (mime === vfs_1.MIME_AUTO)
49
- mime = mime_types_1.default.lookup(source) || '';
50
- if (mime)
51
- ctx.type = mime;
52
- if (ctx.method === 'OPTIONS') {
53
- ctx.status = const_1.HTTP_NO_CONTENT;
54
- ctx.set({ Allow: 'OPTIONS, GET, HEAD' });
55
- return;
56
- }
57
- if (ctx.method !== 'GET')
58
- return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
59
- try {
60
- const stats = await (0, util_1.promisify)(fs_1.stat)(source); // using fs's function instead of fs/promises, because only the former is supported by pkg
61
- ctx.set('Last-Modified', stats.mtime.toUTCString());
62
- ctx.fileSource = source;
63
- ctx.status = const_1.HTTP_OK;
64
- if (ctx.fresh)
65
- return ctx.status = const_1.HTTP_NOT_MODIFIED;
66
- if (content !== undefined)
67
- return ctx.body = content;
68
- const range = getRange(ctx, stats.size);
69
- ctx.body = (0, fs_1.createReadStream)(source, range);
70
- }
71
- catch (e) {
72
- return ctx.status = const_1.HTTP_NOT_FOUND;
73
- }
74
- };
40
+ async function serveFile(ctx, source, mime, content) {
41
+ if (!source)
42
+ return;
43
+ const fn = path_1.default.basename(source);
44
+ if (ctx.params.dl !== undefined) // please, download
45
+ ctx.attachment(fn);
46
+ mime = mime !== null && mime !== void 0 ? mime : lodash_1.default.find(mimeCfg.get(), (v, k) => k > '' && (0, micromatch_1.isMatch)(fn, k)); // isMatch throws on an empty string
47
+ if (mime === vfs_1.MIME_AUTO)
48
+ mime = mime_types_1.default.lookup(source) || '';
49
+ if (mime)
50
+ ctx.type = mime;
51
+ if (ctx.method === 'OPTIONS') {
52
+ ctx.status = const_1.HTTP_NO_CONTENT;
53
+ ctx.set({ Allow: 'OPTIONS, GET, HEAD' });
54
+ return;
55
+ }
56
+ if (ctx.method !== 'GET')
57
+ return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
58
+ try {
59
+ const stats = await (0, util_1.promisify)(fs_1.stat)(source); // using fs's function instead of fs/promises, because only the former is supported by pkg
60
+ ctx.set('Last-Modified', stats.mtime.toUTCString());
61
+ ctx.fileSource = source;
62
+ ctx.status = const_1.HTTP_OK;
63
+ if (ctx.fresh)
64
+ return ctx.status = const_1.HTTP_NOT_MODIFIED;
65
+ if (content !== undefined)
66
+ return ctx.body = content;
67
+ const range = getRange(ctx, stats.size);
68
+ ctx.body = (0, fs_1.createReadStream)(source, range);
69
+ }
70
+ catch (e) {
71
+ return ctx.status = const_1.HTTP_NOT_FOUND;
72
+ }
75
73
  }
76
74
  exports.serveFile = serveFile;
77
75
  function getRange(ctx, totalSize) {
@@ -59,14 +59,14 @@ function serveStatic(uri) {
59
59
  const content = await (0, misc_1.getOrSet)(cache, ctx.path, async () => {
60
60
  const data = await promises_1.default.readFile(fullPath).catch(() => null);
61
61
  return serveApp || !data ? data
62
- : adjustBundlerLinks(ctx.path, uri, data);
62
+ : adjustBundlerLinks(ctx, uri, data);
63
63
  });
64
64
  if (content === null)
65
65
  return ctx.status = const_1.HTTP_NOT_FOUND;
66
66
  if (!serveApp)
67
- return (0, serveFile_1.serveFile)(fullPath, 'auto', content)(ctx, next);
67
+ return (0, serveFile_1.serveFile)(ctx, fullPath, 'auto', content);
68
68
  // we don't cache the index as it's small and may prevent plugins change to apply
69
- ctx.body = await treatIndex(ctx, String(content), uri);
69
+ ctx.body = await treatIndex(ctx, uri, String(content));
70
70
  ctx.type = 'html';
71
71
  ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate');
72
72
  };
@@ -75,17 +75,18 @@ function shouldServeApp(ctx) {
75
75
  var _a;
76
76
  return (_a = ctx.state).serveApp || (_a.serveApp = ctx.path.endsWith('/'));
77
77
  }
78
- function adjustBundlerLinks(path, uri, data) {
79
- const ext = (0, path_1.extname)(path);
78
+ function adjustBundlerLinks(ctx, uri, data) {
79
+ const ext = (0, path_1.extname)(ctx.path);
80
80
  return ext && !ext.match(/\.(css|html|js|ts|scss)/) ? data
81
- : String(data).replace(/((?:import | from )['"])\//g, `$1${uri}`);
81
+ : String(data).replace(/((?:import | from )['"])\//g, `$1${ctx.state.revProxyPath}${uri}`);
82
82
  }
83
- async function treatIndex(ctx, body, filesUri) {
83
+ async function treatIndex(ctx, filesUri, body) {
84
84
  const session = await (0, api_auth_1.refresh_session)({}, ctx);
85
85
  ctx.set('etag', '');
86
86
  const isFrontend = filesUri === const_1.FRONTEND_URI;
87
- const css = (0, plugins_1.mapPlugins)((plug, k) => { var _a; return (_a = (isFrontend ? plug.frontend_css : null)) === null || _a === void 0 ? void 0 : _a.map(f => const_1.PLUGINS_PUB_URI + k + '/' + f); }).flat().filter(Boolean);
88
- const js = (0, plugins_1.mapPlugins)((plug, k) => { var _a; return (_a = (isFrontend ? plug.frontend_js : null)) === null || _a === void 0 ? void 0 : _a.map(f => const_1.PLUGINS_PUB_URI + k + '/' + f); }).flat().filter(Boolean);
87
+ const pub = ctx.state.revProxyPath + const_1.PLUGINS_PUB_URI;
88
+ const css = (0, plugins_1.mapPlugins)((plug, k) => { var _a; return (_a = (isFrontend ? plug.frontend_css : null)) === null || _a === void 0 ? void 0 : _a.map(f => pub + k + '/' + f); }).flat().filter(Boolean);
89
+ const js = (0, plugins_1.mapPlugins)((plug, k) => { var _a; return (_a = (isFrontend ? plug.frontend_js : null)) === null || _a === void 0 ? void 0 : _a.map(f => pub + k + '/' + f); }).flat().filter(Boolean);
89
90
  // expose plugins' configs that are declared with 'frontend' attribute
90
91
  const plugins = Object.fromEntries((0, misc_1.onlyTruthy)((0, plugins_1.mapPlugins)((pl, name) => {
91
92
  var _a, _b;
@@ -98,7 +99,7 @@ async function treatIndex(ctx, body, filesUri) {
98
99
  return !lodash_1.default.isEmpty(configs) && [name, configs];
99
100
  })));
100
101
  let ret = body
101
- .replace(/((?:src|href) *= *['"])\/?(?![a-z]+:\/\/)/g, '$1' + filesUri)
102
+ .replace(/((?:src|href) *= *['"])\/?(?![a-z]+:\/\/)/g, '$1' + ctx.state.revProxyPath + filesUri)
102
103
  .replace('</head>', () => `
103
104
  ${!isFrontend ? '' : `
104
105
  <title>${adminApis_1.title.get()}</title>
@@ -110,6 +111,7 @@ async function treatIndex(ctx, body, filesUri) {
110
111
  API_VERSION: const_1.API_VERSION,
111
112
  session: session instanceof apiMiddleware_1.ApiError ? null : session,
112
113
  plugins,
114
+ prefixUrl: ctx.state.revProxyPath,
113
115
  customHtml: lodash_1.default.omit(Object.fromEntries(customHtml_1.customHtmlState.sections), ['top', 'bottom']), // excluding sections we apply in this phase
114
116
  }, null, 4)}
115
117
  document.documentElement.setAttribute('ver', '${const_1.VERSION.split('-')[0] /*for style selectors*/}')
@@ -141,8 +143,8 @@ function serveProxied(port, uri) {
141
143
  proxy = lib.default('127.0.0.1:' + port, {
142
144
  proxyReqPathResolver: (ctx) => shouldServeApp(ctx) ? '/' : ctx.path,
143
145
  userResDecorator(res, data, ctx) {
144
- return shouldServeApp(ctx) ? treatIndex(ctx, String(data), uri)
145
- : adjustBundlerLinks(ctx.path, uri, data);
146
+ return shouldServeApp(ctx) ? treatIndex(ctx, uri, String(data))
147
+ : adjustBundlerLinks(ctx, uri, data);
146
148
  }
147
149
  }));
148
150
  return function () {
package/src/vfs.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.cantReadStatusCode = exports.walkNode = exports.hasPermission = exports.nodeIsDirectory = exports.getNodeName = exports.saveVfs = exports.vfs = exports.urlToNode = exports.MIME_AUTO = exports.defaultPerms = void 0;
7
+ exports.cantReadStatusCode = exports.walkNode = exports.hasPermission = exports.nodeIsDirectory = exports.getNodeName = exports.saveVfs = exports.vfs = exports.urlToNode = exports.isSameFilenameAs = exports.MIME_AUTO = exports.defaultPerms = void 0;
8
8
  const promises_1 = __importDefault(require("fs/promises"));
9
9
  const path_1 = require("path");
10
10
  const micromatch_1 = require("micromatch");
@@ -14,7 +14,6 @@ const config_1 = require("./config");
14
14
  const const_1 = require("./const");
15
15
  const events_1 = __importDefault(require("./events"));
16
16
  const perm_1 = require("./perm");
17
- const misc_2 = require("./misc");
18
17
  const WHO_ANYONE = true;
19
18
  const WHO_NO_ONE = false;
20
19
  const WHO_ANY_ACCOUNT = '*';
@@ -38,6 +37,11 @@ function inheritFromParent(parent, child) {
38
37
  child.mime || (child.mime = parent.mime);
39
38
  return child;
40
39
  }
40
+ function isSameFilenameAs(name) {
41
+ const lc = name.toLowerCase();
42
+ return (other) => lc === (typeof other === 'string' ? other : getNodeName(other)).toLowerCase();
43
+ }
44
+ exports.isSameFilenameAs = isSameFilenameAs;
41
45
  async function urlToNode(url, ctx, parent = exports.vfs, getRest) {
42
46
  var _a;
43
47
  let initialSlashes = 0;
@@ -54,9 +58,7 @@ async function urlToNode(url, ctx, parent = exports.vfs, getRest) {
54
58
  return;
55
59
  }
56
60
  // does the tree node have a child that goes by this name?
57
- const sameName = !const_1.IS_WINDOWS ? (x) => x === name // easy
58
- : (0, misc_2.with_)(name.toLowerCase(), lc => (x) => x.toLowerCase() === lc);
59
- const child = (_a = parent.children) === null || _a === void 0 ? void 0 : _a.find(x => sameName(getNodeName(x)));
61
+ const child = (_a = parent.children) === null || _a === void 0 ? void 0 : _a.find(isSameFilenameAs(name));
60
62
  const ret = {
61
63
  ...child,
62
64
  original: child,
@@ -124,8 +126,7 @@ exports.nodeIsDirectory = nodeIsDirectory;
124
126
  function hasPermission(node, perm, ctx) {
125
127
  var _a;
126
128
  return (node.source || perm !== 'can_upload') // Upload possible only if we know where to store. First check node.source because is supposedly faster.
127
- && matchWho((_a = node[perm]) !== null && _a !== void 0 ? _a : exports.defaultPerms[perm], ctx)
128
- && (perm !== 'can_see' || hasPermission(node, 'can_read', ctx)); // can_see is used to hide something you nonetheless can_read, so you MUST also can_read
129
+ && matchWho((_a = node[perm]) !== null && _a !== void 0 ? _a : exports.defaultPerms[perm], ctx);
129
130
  }
130
131
  exports.hasPermission = hasPermission;
131
132
  async function* walkNode(parent, ctx, depth = 0, prefixPath = '') {