hfs 0.42.3 → 0.44.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.
Files changed (56) hide show
  1. package/README.md +56 -17
  2. package/admin/assets/index-35f6e3dc.css +1 -0
  3. package/admin/assets/index-ef68a7ab.js +510 -0
  4. package/{frontend/assets/sha512-f6798733.js → admin/assets/sha512-d091720e.js} +1 -1
  5. package/admin/index.html +2 -2
  6. package/frontend/assets/index-a3b0d6ac.js +94 -0
  7. package/frontend/assets/index-fe0f3d77.css +1 -0
  8. package/{admin/assets/sha512-69b26793.js → frontend/assets/sha512-96fd938f.js} +1 -1
  9. package/frontend/fontello.css +9 -3
  10. package/frontend/fontello.woff2 +0 -0
  11. package/frontend/index.html +3 -2
  12. package/package.json +2 -2
  13. package/plugins/antibrute/plugin.js +1 -1
  14. package/plugins/download-counter/plugin.js +14 -5
  15. package/plugins/download-counter/public/main.js +12 -2
  16. package/plugins/vhosting/plugin.js +17 -11
  17. package/src/adminApis.js +4 -4
  18. package/src/api.auth.js +4 -1
  19. package/src/api.file_list.js +22 -10
  20. package/src/api.lang.js +9 -11
  21. package/src/api.vfs.js +43 -29
  22. package/src/apiMiddleware.js +3 -2
  23. package/src/block.js +6 -20
  24. package/src/config.js +6 -2
  25. package/src/const.js +5 -4
  26. package/src/customHtml.js +2 -2
  27. package/src/debounceAsync.js +3 -3
  28. package/src/frontEndApis.js +7 -32
  29. package/src/github.js +26 -8
  30. package/src/index.js +2 -1
  31. package/src/lang.js +67 -0
  32. package/src/langs/embedded.js +13 -0
  33. package/src/langs/hfs-lang-it.json +100 -0
  34. package/src/langs/hfs-lang-ko.json +103 -0
  35. package/src/langs/hfs-lang-ms.json +70 -0
  36. package/src/langs/hfs-lang-ru.json +106 -0
  37. package/src/langs/hfs-lang-sr.json +108 -0
  38. package/src/langs/hfs-lang-zh-tw.json +106 -0
  39. package/src/langs/hfs-lang-zh.json +98 -0
  40. package/src/listen.js +8 -3
  41. package/src/log.js +6 -2
  42. package/src/middlewares.js +24 -18
  43. package/src/misc.js +17 -9
  44. package/src/perm.js +1 -1
  45. package/src/plugins.js +6 -6
  46. package/src/serveFile.js +2 -2
  47. package/src/serveGuiFiles.js +23 -10
  48. package/src/update.js +1 -2
  49. package/src/upload.js +6 -6
  50. package/src/util-http.js +5 -3
  51. package/src/vfs.js +114 -74
  52. package/src/zip.js +4 -4
  53. package/admin/assets/index-08017e15.js +0 -511
  54. package/admin/assets/index-94bbe0be.css +0 -1
  55. package/frontend/assets/index-5f125477.js +0 -94
  56. package/frontend/assets/index-a09cacfd.css +0 -1
@@ -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: " – "}:root .highlightedText,:root .file-menu a:hover{color:#0006;text-shadow:0 0 3px rgba(0,0,0,.4)}:root .theme-dark{--bg: #000;--text: #999;--good-contrast: #fffa;--button-bg: #345;--button-text: #999;color-scheme:dark}:root .theme-dark .highlightedText,:root .theme-dark .file-menu a:hover,:root .file-menu .theme-dark a:hover{color:#fff;text-shadow:0 0 3px #fff}: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-color: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}.emoji-icon,.file-icon{display:inline-block;width:1.4em;text-align:center}.file-icon{height:1em;background-size:contain;background-repeat:no-repeat;background-position:center;vertical-align:text-bottom}.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:hover{outline:1px solid var(--mild-contrast)}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)}.icon-button,ul.dir li .entry-panel .file-menu-button{font-size:.7em;padding:.2em .4em;margin-left:.4em;vertical-align:bottom}.error-msg{background-color:#faa;color:#833;padding:.5em 1em}.hide-back,.upload-toolbar,header{background-color:var(--bg)}header{position:sticky;top:0;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:flex .5s}.show-sliding{transition:flex .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;display:inline-flex;justify-content:center;align-items:center;width:min-content}.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:.5em 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-panel{float:right;margin-top:.3em;display:flex;align-items:center}ul.dir li .entry-panel .file-menu-button{margin:-3px 0 -3px .4em}ul.dir li .entry-panel .entry-details{font-size:90%;margin-left:4px;font-variant-numeric:tabular-nums}ul.dir li .entry-panel .entry-details .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-color: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-toolbar{position:sticky;top:-4px}.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}.miss-perm{margin:.3em}.popup-menu-button{font-size:.8em;padding:.2em .3em;position:absolute;opacity:.8}.popup-menu-button:hover{opacity:1}.file-dialog .dialog-content{min-width:calc(100% - 1em)}.file-dialog .dialog{min-width:13em}.file-dialog-properties{word-break:break-word;line-height:1.5em}.file-dialog-properties dt{font-weight:700}.file-dialog-properties dd{margin-left:1.5em}.file-menu{margin-top:1em;display:flex;flex-direction:column}.file-menu a{padding:.5em 0}.file-menu a:first-child{padding-top:1em;border-top:1px solid var(--faint-contrast)}.file-menu a .icon{margin-right:.5em}@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:#8886;backdrop-filter:blur(2px);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;flex-direction:column;justify-content:center}.dialog-icon{color:#fff;background-color:var(--color);position:absolute;top:0;width:2em;height:1.8em;text-align:center;border-radius:.8em 0}.dialog-title{font-size:120%;margin-top:-.4em;padding:0 .5em}.dialog-closer~.dialog-title{margin-right:2em}.dialog-type~.dialog-title{margin-left:2em}.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.8em;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-08017e15.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-a3b0d6ac.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,6 +1,7 @@
1
1
  @font-face {
2
2
  font-family: 'fontello';
3
- src: url('fontello.woff2?66723712') format('woff2'); font-weight: normal;
3
+ src: url('fontello.woff2?19258766') format('woff2'); font-weight: normal;
4
+ font-weight: normal;
4
5
  font-style: normal;
5
6
  }
6
7
  /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
@@ -9,7 +10,7 @@
9
10
  @media screen and (-webkit-min-device-pixel-ratio:0) {
10
11
  @font-face {
11
12
  font-family: 'fontello';
12
- src: url('../font/fontello.svg?66723712#fontello') format('svg');
13
+ src: url('../font/fontello.svg?19258766#fontello') format('svg');
13
14
  }
14
15
  }
15
16
  */
@@ -49,9 +50,11 @@
49
50
  }
50
51
 
51
52
  .fa-cog:before { content: '\e800'; } /* '' */
53
+ .fa-audio:before { content: '\e801'; } /* '' */
52
54
  .fa-doc:before { content: '\e802'; } /* '' */
53
55
  .fa-stop:before { content: '\e803'; } /* '' */
54
56
  .fa-play:before { content: '\e804'; } /* '' */
57
+ .fa-music:before { content: '\e805'; } /* '' */
55
58
  .fa-cancel:before { content: '\e806'; } /* '' */
56
59
  .fa-edit:before { content: '\e807'; } /* '' */
57
60
  .fa-check:before { content: '\e808'; } /* '' */
@@ -63,8 +66,12 @@
63
66
  .fa-to_start:before { content: '\e80e'; } /* '' */
64
67
  .fa-retweet:before { content: '\e80f'; } /* '' */
65
68
  .fa-to_end:before { content: '\e810'; } /* '' */
69
+ .fa-picture:before { content: '\e811'; } /* '' */
70
+ .fa-camera:before { content: '\e812'; } /* '' */
66
71
  .fa-search:before { content: '\e813'; } /* '' */
67
72
  .fa-logout:before { content: '\e814'; } /* '' */
73
+ .fa-video:before { content: '\e815'; } /* '' */
74
+ .fa-left:before { content: '\e816'; } /* '' */
68
75
  .fa-spin6:before { content: '\e839'; } /* '' */
69
76
  .fa-crown:before { content: '\e844'; } /* '' */
70
77
  .fa-download:before { content: '\f02e'; } /* '' */
@@ -73,5 +80,4 @@
73
80
  .fa-menu:before { content: '\f0c9'; } /* '' */
74
81
  .fa-quote:before { content: '\f10d'; } /* '' */
75
82
  .fa-unlink:before { content: '\f127'; } /* '' */
76
- .fa-level-up:before { content: '\f148'; } /* '' */
77
83
  .fa-archive:before { content: '\f1c6'; } /* '' */
Binary file
@@ -5,8 +5,9 @@
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-5f125477.js"></script>
9
- <link rel="stylesheet" href="/assets/index-a09cacfd.css">
8
+ <script>HFS={}</script>
9
+ <script type="module" crossorigin src="/assets/index-a3b0d6ac.js"></script>
10
+ <link rel="stylesheet" href="/assets/index-fe0f3d77.css">
10
11
  </head>
11
12
  <body>
12
13
  <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.42.3",
3
+ "version": "0.44.0",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -20,7 +20,7 @@
20
20
  "build-frontend": "npm run build --workspace=frontend",
21
21
  "build-admin": "npm run build --workspace=admin",
22
22
  "server-for-test": "node dist/src --cwd . --config tests && rm custom.html",
23
- "server-for-test-dev": "cross-env DEV=1 nodemon --ignore tests/ --watch src -e ts,tsx --exec ts-node src -- --cwd . --config tests",
23
+ "server-for-test-dev": "cross-env DEV=1 FRONTEND_PROXY=3005 ADMIN_PROXY=3006 nodemon --ignore tests/ --watch src -e ts,tsx --exec ts-node src -- --cwd . --config tests",
24
24
  "test": "mocha -r ts-node/register 'tests/**/*.ts'",
25
25
  "pub": "cd dist && npm publish",
26
26
  "dist": "npm run build-all && npm run dist-bin",
@@ -7,7 +7,7 @@ exports.config = {
7
7
  max: { type: 'number', min: 1, defaultValue: 60, helperText: "Max seconds to delay before next login is allowed" },
8
8
  }
9
9
  exports.configDialog = {
10
- maxWidth: '25em',
10
+ maxWidth: 'xs',
11
11
  }
12
12
 
13
13
  const byIp = {}
@@ -1,6 +1,15 @@
1
- exports.description = "Counts downloads for each file, and displays the total in the list"
2
- exports.version = 3.1 // removed "hits" word
3
- exports.apiRequired = 3
1
+ exports.description = "Counts downloads for each file, and displays the total in the list or file menu"
2
+ exports.version = 4 // config.where
3
+ exports.apiRequired = 8
4
+
5
+ exports.config = {
6
+ where: { frontend: true, type: 'select', defaultValue: 'menu',
7
+ options: ['list', { value: 'menu', label: "file menu" }],
8
+ }
9
+ }
10
+ exports.configDialog = {
11
+ sx: { maxWidth: '20em' },
12
+ }
4
13
 
5
14
  exports.init = async api => {
6
15
  const _ = api.require('lodash')
@@ -38,8 +47,8 @@ exports.init = async api => {
38
47
  counters[path] = counters[path] + 1 || 1
39
48
  save()
40
49
  },
41
- onDirEntry: ({ entry, listPath }) => {
42
- const path = listPath + entry.n
50
+ onDirEntry: ({ entry, listUri }) => {
51
+ const path = listUri + entry.n
43
52
  const n = counters[path]
44
53
  if (n)
45
54
  entry.hits = n
@@ -1,2 +1,12 @@
1
- HFS.onEvent('additionalEntryProps', ({ entry: { hits } }, { t }) =>
2
- hits && `<span class="download-counter" title="${t`download counter`}">${hits}</span>`)
1
+ (() => { // this wrapper avoids name clashing of outer variables and functions
2
+ const config = HFS.getPluginConfig()
3
+
4
+ const inMenu = config.where === 'menu'
5
+ HFS.onEvent('additionalEntryDetails', ({ entry: { hits } }) =>
6
+ hits && !inMenu && `<span class="download-counter" title="${HFS.t`download counter`}">${hits}</span>`)
7
+
8
+ HFS.onEvent('fileMenu', ({ entry, props }) => {
9
+ if (inMenu && !entry.isFolder)
10
+ props.push(["Downloads", entry.hits || 0])
11
+ })
12
+ })()
@@ -21,26 +21,32 @@ exports.init = api => {
21
21
  const { matches } = api.require('./misc')
22
22
  return {
23
23
  middleware(ctx) {
24
- let toModify = ctx
24
+ let params // undefined if we are not going to work on api parameters
25
25
  if (ctx.path.startsWith(api.const.SPECIAL_URI)) { // special uris should be excluded...
26
26
  // ...unless it's a frontend api with a path param
27
- if (!ctx.path.startsWith(api.const.API_URI) || ctx.params.path === undefined) return
27
+ if (!ctx.path.startsWith(api.const.API_URI)) return
28
28
  let { referer } = ctx.headers
29
29
  referer &&= new URL(referer).pathname
30
- if (referer?.startsWith(ctx.state.revProxyPath + api.const.ADMIN_URI)) return
31
- toModify = ctx.params
30
+ if (referer?.startsWith(ctx.state.revProxyPath + api.const.ADMIN_URI)) return // exclude apis for admin-panel
31
+ params = ctx.params
32
32
  }
33
+
33
34
  const hosts = api.getConfig('hosts')
34
35
  if (!hosts?.length) return
35
- for (const row of hosts)
36
- if (matches(ctx.host, row.host)) {
37
- toModify.path = row.root + toModify.path
38
- return
36
+ const row = hosts?.find(x => matches(ctx.host, x.host))
37
+ if (!row) {
38
+ if (api.getConfig('mandatory')) {
39
+ ctx.socket.destroy()
40
+ return true
39
41
  }
40
- if (api.getConfig('mandatory')) {
41
- ctx.socket.destroy()
42
- return true
42
+ return
43
43
  }
44
+ if (!params)
45
+ ctx.path = row.root + ctx.path
46
+ else
47
+ for (const [k,v] of Object.entries(params))
48
+ if (k.startsWith('uri'))
49
+ params[k] = row.root + v
44
50
  }
45
51
  }
46
52
  }
package/src/adminApis.js CHANGED
@@ -167,11 +167,11 @@ for (const [k, was] of Object.entries(exports.adminApis))
167
167
  : new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, props);
168
168
  };
169
169
  exports.localhostAdmin = (0, config_1.defineConfig)('localhost_admin', true);
170
- exports.adminNet = (0, config_1.defineConfig)('admin_net', '');
171
- exports.favicon = (0, config_1.defineConfig)('favicon');
170
+ exports.adminNet = (0, config_1.defineConfig)('admin_net', '', v => (0, misc_1.makeNetMatcher)(v, true));
171
+ exports.favicon = (0, config_1.defineConfig)('favicon', '');
172
172
  exports.title = (0, config_1.defineConfig)('title', "File server");
173
173
  function ctxAdminAccess(ctx) {
174
- return !ctx.state.proxiedFor // we consider localhost_admin only if no proxy is detected
174
+ return !ctx.ips.length // we consider localhost_admin only if no proxy is being usedø
175
175
  && exports.localhostAdmin.get() && (0, misc_1.isLocalHost)(ctx)
176
176
  || (0, perm_1.getFromAccount)(ctx.state.account, a => a.admin);
177
177
  }
@@ -187,6 +187,6 @@ function anyAccountCanLoginAdmin() {
187
187
  }
188
188
  exports.anyAccountCanLoginAdmin = anyAccountCanLoginAdmin;
189
189
  function allowAdmin(ctx) {
190
- return (0, misc_1.matchesNet)(ctx, exports.adminNet.get(), true);
190
+ return exports.adminNet.compiled()(ctx.ip);
191
191
  }
192
192
  exports.allowAdmin = allowAdmin;
package/src/api.auth.js CHANGED
@@ -11,8 +11,10 @@ const misc_1 = require("./misc");
11
11
  const api_helpers_1 = require("./api.helpers");
12
12
  const adminApis_1 = require("./adminApis");
13
13
  const middlewares_1 = require("./middlewares");
14
+ const config_1 = require("./config");
14
15
  const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
15
16
  const ongoingLogins = {}; // store data that doesn't fit session object
17
+ const keepSessionAlive = (0, config_1.defineConfig)('keep_session_alive', true);
16
18
  // centralized log-in state
17
19
  async function loggedIn(ctx, username) {
18
20
  const s = ctx.session;
@@ -29,7 +31,8 @@ async function loggedIn(ctx, username) {
29
31
  ctx.cookies.set('csrf', (0, misc_1.randomId)(), { signed: false, httpOnly: false });
30
32
  }
31
33
  function makeExp() {
32
- return { exp: new Date(Date.now() + const_1.SESSION_DURATION) };
34
+ return !keepSessionAlive.get() ? undefined
35
+ : { exp: new Date(Date.now() + const_1.SESSION_DURATION) };
33
36
  }
34
37
  const login = async ({ username, password }, ctx) => {
35
38
  if (!username || !password) // some validation
@@ -12,19 +12,18 @@ const plugins_1 = require("./plugins");
12
12
  const misc_1 = require("./misc");
13
13
  const lodash_1 = __importDefault(require("lodash"));
14
14
  const const_1 = require("./const");
15
- const file_list = async ({ path, offset, limit, search, omit, sse }, ctx) => {
16
- let node = await (0, vfs_1.urlToNode)(path || '/', ctx);
15
+ const file_list = async ({ uri, offset, limit, search, omit, sse }, ctx) => {
16
+ const node = await (0, vfs_1.urlToNode)(uri || '/', ctx);
17
17
  const list = new apiMiddleware_1.SendListReadable();
18
18
  if (!node)
19
19
  return fail(const_1.HTTP_NOT_FOUND);
20
- const res = (0, vfs_1.statusCodeForMissingPerm)(node, 'can_list', ctx);
21
- if (res)
22
- return fail(res);
20
+ if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_list', ctx))
21
+ return fail();
23
22
  if ((0, misc_1.dirTraversal)(search))
24
23
  return fail(const_1.HTTP_FOOL);
25
24
  if (node.default)
26
25
  return (sse ? list.custom : lodash_1.default.identity)({
27
- redirect: path // tell the browser to access the folder (instead of using this api), so it will get the default file
26
+ redirect: uri // tell the browser to access the folder (instead of using this api), so it will get the default file
28
27
  });
29
28
  if (!await (0, vfs_1.nodeIsDirectory)(node))
30
29
  return fail(const_1.HTTP_METHOD_NOT_ALLOWED);
@@ -46,7 +45,7 @@ const file_list = async ({ path, offset, limit, search, omit, sse }, ctx) => {
46
45
  list.close();
47
46
  });
48
47
  return list;
49
- function fail(code) {
48
+ function fail(code = ctx.status) {
50
49
  if (!sse)
51
50
  return new apiMiddleware_1.ApiError(code);
52
51
  list.error(code, true);
@@ -58,10 +57,10 @@ const file_list = async ({ path, offset, limit, search, omit, sse }, ctx) => {
58
57
  break;
59
58
  if (!filter((0, vfs_1.getNodeName)(sub)))
60
59
  continue;
61
- const entry = await nodeToDirEntry(sub);
60
+ const entry = await nodeToDirEntry(ctx, sub);
62
61
  if (!entry)
63
62
  continue;
64
- const cbParams = { entry, ctx, listPath: path, node: sub };
63
+ const cbParams = { entry, ctx, listUri: uri, node: sub };
65
64
  try {
66
65
  if (onDirEntryHandlers.some(cb => cb(cbParams) === false))
67
66
  continue;
@@ -87,7 +86,7 @@ const file_list = async ({ path, offset, limit, search, omit, sse }, ctx) => {
87
86
  }
88
87
  };
89
88
  exports.file_list = file_list;
90
- async function nodeToDirEntry(node) {
89
+ async function nodeToDirEntry(ctx, node) {
91
90
  let { source, default: def } = node;
92
91
  const name = (0, vfs_1.getNodeName)(node);
93
92
  if (!source)
@@ -98,14 +97,27 @@ async function nodeToDirEntry(node) {
98
97
  const st = await (0, promises_1.stat)(source);
99
98
  const folder = st.isDirectory();
100
99
  const { ctime, mtime } = st;
100
+ const pl = node.can_list === vfs_1.WHO_NO_ONE ? 'l'
101
+ : !(0, vfs_1.hasPermission)(node, 'can_list', ctx) ? 'L'
102
+ : '';
103
+ // no download here, but maybe inside?
104
+ const pr = node.can_read === vfs_1.WHO_NO_ONE && !(folder && filesInsideCould()) ? 'r'
105
+ : !(0, vfs_1.hasPermission)(node, 'can_read', ctx) ? 'R'
106
+ : '';
101
107
  return {
102
108
  n: name + (folder ? '/' : ''),
103
109
  c: ctime,
104
110
  m: Math.abs(+mtime - +ctime) < 1000 ? undefined : mtime,
105
111
  s: folder ? undefined : st.size,
112
+ p: (pr + pl) || undefined
106
113
  };
107
114
  }
108
115
  catch (_a) {
109
116
  return null;
110
117
  }
118
+ function filesInsideCould(n = node) {
119
+ var _a;
120
+ return (0, vfs_1.masksCouldGivePermission)(n.masks, 'can_read')
121
+ || ((_a = n.children) === null || _a === void 0 ? void 0 : _a.some(c => c.can_read || filesInsideCould(c))); // we count on the boolean-compliant nature of the permission type here
122
+ }
111
123
  }
package/src/api.lang.js CHANGED
@@ -10,21 +10,23 @@ const fast_glob_1 = __importDefault(require("fast-glob"));
10
10
  const promises_1 = require("fs/promises");
11
11
  const const_1 = require("./const");
12
12
  const misc_1 = require("./misc");
13
- const PREFIX = 'hfs-lang-';
14
- const SUFFIX = '.json';
13
+ const lang_1 = require("./lang");
14
+ const embedded_1 = __importDefault(require("./langs/embedded"));
15
15
  const apis = {
16
16
  list_langs() {
17
17
  return new apiMiddleware_1.SendListReadable({
18
18
  doAtStart: async (list) => {
19
- for await (let name of fast_glob_1.default.stream(code2file('*'))) {
19
+ for await (let name of fast_glob_1.default.stream((0, lang_1.code2file)('*'))) {
20
20
  name = String(name);
21
- const code = name.slice(PREFIX.length, -SUFFIX.length);
21
+ const code = (0, lang_1.file2code)(name);
22
22
  try {
23
23
  const data = JSON.parse(await (0, promises_1.readFile)(name, 'utf8'));
24
24
  list.add({ code, ...lodash_1.default.omit(data, 'translate') });
25
25
  }
26
26
  catch (_a) { }
27
27
  }
28
+ for (const [code, data] of Object.entries(embedded_1.default))
29
+ list.add({ code, embedded: true, ...lodash_1.default.omit(data, 'translate') });
28
30
  list.close();
29
31
  }
30
32
  });
@@ -32,7 +34,7 @@ const apis = {
32
34
  async del_lang({ code }) {
33
35
  validateCode(code);
34
36
  try {
35
- await (0, promises_1.rm)(code2file(code));
37
+ await (0, promises_1.rm)((0, lang_1.code2file)(code));
36
38
  return {};
37
39
  }
38
40
  catch (e) {
@@ -41,10 +43,9 @@ const apis = {
41
43
  },
42
44
  async add_langs({ langs }) {
43
45
  for (let [code, content] of Object.entries(langs)) {
44
- if (code.endsWith(SUFFIX)) // filename, actually
45
- code = code.slice(PREFIX.length, -SUFFIX.length);
46
+ code = (0, lang_1.file2code)(code);
46
47
  validateCode(code);
47
- const fn = code2file(code);
48
+ const fn = (0, lang_1.code2file)(code);
48
49
  const s = content = String(content);
49
50
  if (!(0, misc_1.tryJson)(s))
50
51
  return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_ACCEPTABLE, "bad content for file " + fn);
@@ -54,9 +55,6 @@ const apis = {
54
55
  }
55
56
  };
56
57
  exports.default = apis;
57
- function code2file(code) {
58
- return PREFIX + code.toLowerCase() + SUFFIX;
59
- }
60
58
  function validateCode(code) {
61
59
  if (!/^(\w\w)(-\w\w)*$/.test(code))
62
60
  throw new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad code/filename');
package/src/api.vfs.js CHANGED
@@ -20,37 +20,45 @@ async function urlToNodeOriginal(uri) {
20
20
  const apis = {
21
21
  async get_vfs() {
22
22
  return {
23
- root: vfs_1.vfs && await recur(vfs_1.vfs),
23
+ root: await recur(),
24
24
  defaultPerms: vfs_1.defaultPerms,
25
25
  };
26
- async function recur(node) {
26
+ async function recur(node = vfs_1.vfs) {
27
27
  var _a;
28
- const stats = Boolean(node.source) && await (0, promises_1.stat)(node.source).catch(e => false);
29
- const isDir = !node.source || stats && stats.isDirectory();
28
+ const { source } = node;
29
+ const stats = Boolean(source) && await (0, promises_1.stat)(source).catch(() => false);
30
+ const isDir = !source || stats && stats.isDirectory();
30
31
  const copyStats = stats ? lodash_1.default.pick(stats, ['size', 'ctime', 'mtime'])
31
- : { size: node.source ? -1 : undefined };
32
+ : { size: source ? -1 : undefined };
32
33
  if (copyStats.mtime && Number(copyStats.mtime) === Number(copyStats.ctime))
33
34
  delete copyStats.mtime;
34
- const isRoot = node === vfs_1.vfs;
35
+ let byMasks = node.original && lodash_1.default.pickBy(node, (v, k) => v !== node.original[k] // something is changing me...
36
+ && v !== node.parent[k] // ...and it's not inheritance...
37
+ && vfs_1.PERM_KEYS.includes(k)); // ...must be masks. Please limit this to perms
38
+ if (lodash_1.default.isEmpty(byMasks))
39
+ byMasks = undefined;
35
40
  return {
36
41
  ...copyStats,
37
- ...node,
42
+ ...node.original || node,
43
+ byMasks,
38
44
  website: Boolean((_a = node.children) === null || _a === void 0 ? void 0 : _a.find((0, vfs_1.isSameFilenameAs)('index.html')))
39
- || isDir && node.source && await (0, promises_1.stat)((0, path_1.join)(node.source, 'index.html')).then(() => true, () => undefined)
45
+ || isDir && source && await (0, promises_1.stat)((0, path_1.join)(source, 'index.html')).then(() => true, () => undefined)
40
46
  || undefined,
41
- name: isRoot ? undefined : (0, vfs_1.getNodeName)(node),
47
+ name: node === vfs_1.vfs ? undefined : (0, vfs_1.getNodeName)(node),
42
48
  type: isDir ? 'folder' : undefined,
43
- children: node.children && await Promise.all(node.children.map(recur)),
49
+ children: node.children && await Promise.all(node.children.map(async (original) => recur((0, vfs_1.applyParentToChild)(original, node))))
44
50
  };
45
51
  }
46
52
  },
47
53
  async move_vfs({ from, parent }) {
48
54
  var _a;
49
- if (from <= '/' || !parent)
55
+ if (!from || !parent)
50
56
  return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST);
51
57
  const fromNode = await urlToNodeOriginal(from);
52
58
  if (!fromNode)
53
59
  return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'from not found');
60
+ if (fromNode === vfs_1.vfs)
61
+ return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'from is root');
54
62
  const parentNode = await urlToNodeOriginal(parent);
55
63
  if (!parentNode)
56
64
  return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'parent not found');
@@ -70,39 +78,39 @@ const apis = {
70
78
  const n = await urlToNodeOriginal(uri);
71
79
  if (!n)
72
80
  return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'path not found');
73
- props = pickProps(props, ['name', 'source', 'masks', 'default', 'accept', ...Object.keys(vfs_1.defaultPerms)]);
81
+ props = pickProps(props, ['name', 'source', 'masks', 'default', 'accept', ...vfs_1.PERM_KEYS]); // sanitize
74
82
  if (props.name && props.name !== (0, vfs_1.getNodeName)(n)) {
75
83
  const parent = await urlToNodeOriginal((0, path_1.dirname)(uri));
76
84
  if ((_a = parent === null || parent === void 0 ? void 0 : parent.children) === null || _a === void 0 ? void 0 : _a.find(x => (0, vfs_1.getNodeName)(x) === props.name))
77
85
  return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, 'name already present');
78
86
  }
79
- props = (0, misc_1.newObj)(props, v => v === null ? undefined : v); // null is a way to serialize undefined, that will restore default values
80
87
  if (props.masks && typeof props.masks !== 'object')
81
88
  delete props.masks;
82
89
  Object.assign(n, props);
83
- if ((0, vfs_1.getNodeName)(lodash_1.default.omit(n, ['name'])) === n.name) // name only if necessary
84
- n.name = undefined;
90
+ simplifyName(n);
85
91
  await (0, vfs_1.saveVfs)();
86
92
  return n;
87
93
  },
88
94
  async add_vfs({ parent, source, name }) {
89
95
  var _a;
90
- const n = parent ? await urlToNodeOriginal(parent) : vfs_1.vfs;
91
- if (!n)
92
- return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'invalid parent');
93
- if (n.isTemp || !await (0, vfs_1.nodeIsDirectory)(n))
94
- return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_ACCEPTABLE, 'invalid parent');
96
+ if (!source && !name)
97
+ return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'name or source required');
98
+ parent = parent ? await urlToNodeOriginal(parent) : vfs_1.vfs;
99
+ if (!parent)
100
+ return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND, 'parent not found');
101
+ if (!await (0, vfs_1.nodeIsDirectory)(parent))
102
+ return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_ACCEPTABLE, 'parent not a folder');
95
103
  if ((0, misc_1.isWindowsDrive)(source))
96
104
  source += '\\'; // slash must be included, otherwise it will refer to the cwd of that drive
97
- let tryName = (0, vfs_1.getNodeName)({ name, source });
98
- const ext = (0, path_1.extname)(tryName);
99
- const noExt = ext ? tryName.slice(0, -ext.length) : tryName;
105
+ const child = { source, name };
106
+ name = (0, vfs_1.getNodeName)(child); // could be not given as input
107
+ const ext = (0, path_1.extname)(name);
108
+ const noExt = ext ? name.slice(0, -ext.length) : name;
100
109
  let idx = 2;
101
- while ((_a = n.children) === null || _a === void 0 ? void 0 : _a.find((0, vfs_1.isSameFilenameAs)(tryName)))
102
- tryName = `${noExt} ${idx++}${ext}`;
103
- name = tryName;
104
- n.children || (n.children = []);
105
- n.children.unshift({ source, name });
110
+ while ((_a = parent.children) === null || _a === void 0 ? void 0 : _a.find((0, vfs_1.isSameFilenameAs)(name)))
111
+ name = `${noExt} ${idx++}${ext}`;
112
+ child.name = name;
113
+ (parent.children || (parent.children = [])).unshift({ source, name });
106
114
  await (0, vfs_1.saveVfs)();
107
115
  return { name };
108
116
  },
@@ -154,13 +162,14 @@ const apis = {
154
162
  return;
155
163
  }
156
164
  try {
165
+ const matching = (0, misc_1.makeMatcher)(fileMask);
157
166
  path = (0, misc_1.isWindowsDrive)(path) ? path + '\\' : (0, path_1.resolve)(path || '/');
158
167
  for await (const [name, isDir] of (0, misc_1.dirStream)(path)) {
159
168
  if (ctx.req.aborted)
160
169
  return;
161
170
  try {
162
171
  if (!isDir)
163
- if (!files || fileMask && !(0, misc_1.matches)(name, fileMask))
172
+ if (!files || fileMask && !matching(name))
164
173
  continue;
165
174
  const stats = await (0, promises_1.stat)((0, path_1.join)(path, name));
166
175
  yield {
@@ -191,3 +200,8 @@ function pickProps(o, keys) {
191
200
  ret[k] = o[k] === null || o[k] === '' ? undefined : o[k];
192
201
  return ret;
193
202
  }
203
+ function simplifyName(node) {
204
+ const { name, ...noName } = node;
205
+ if ((0, vfs_1.getNodeName)(noName) === name)
206
+ delete node.name;
207
+ }
@@ -31,8 +31,9 @@ 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
- if (params.path)
35
- params.path = (0, misc_1.removeStarting)(ctx.state.revProxyPath, params.path);
34
+ for (const [k, v] of Object.entries(params))
35
+ if (k.startsWith('uri') && typeof v === 'string')
36
+ params[k] = (0, misc_1.removeStarting)(ctx.state.revProxyPath, v);
36
37
  res = csrf && csrf !== params.csrf ? new ApiError(const_1.HTTP_UNAUTHORIZED, 'csrf')
37
38
  : await apiFun(params || {}, ctx);
38
39
  }
package/src/block.js CHANGED
@@ -1,34 +1,20 @@
1
1
  "use strict";
2
2
  // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
3
- var __importDefault = (this && this.__importDefault) || function (mod) {
4
- return (mod && mod.__esModule) ? mod : { "default": mod };
5
- };
6
3
  Object.defineProperty(exports, "__esModule", { value: true });
7
4
  exports.applyBlock = void 0;
8
5
  const config_1 = require("./config");
9
6
  const connections_1 = require("./connections");
10
7
  const misc_1 = require("./misc");
11
- const cidr_tools_1 = __importDefault(require("cidr-tools"));
12
- const lodash_1 = __importDefault(require("lodash"));
13
- let blockFunctions = []; // "compiled" versions of the rules in config.block
14
- (0, config_1.defineConfig)('block', []).sub(rules => {
15
- compileBlock(rules);
8
+ const block = (0, config_1.defineConfig)('block', [], rules => {
9
+ const ret = !Array.isArray(rules) ? []
10
+ : (0, misc_1.onlyTruthy)(rules.map(rule => (0, misc_1.makeNetMatcher)(rule.ip, true)));
11
+ // reapply new block to existing connections
16
12
  for (const { socket, ip } of (0, connections_1.getConnections)())
17
13
  applyBlock(socket, ip);
14
+ return ret;
18
15
  });
19
- function compileBlock(rules) {
20
- blockFunctions = !Array.isArray(rules) ? []
21
- : (0, misc_1.onlyTruthy)(rules.map(rule => !rule ? null
22
- : (0, misc_1.with_)(rule.ip, ip => typeof ip !== 'string' ? null
23
- : ip.includes('/') ? x => cidr_tools_1.default.contains(ip, x)
24
- : ip.includes('*') ? (0, misc_1.with_)(ipMask2regExp(ip), re => x => re.test(x))
25
- : x => x === ip)));
26
- function ipMask2regExp(ipMask) {
27
- return new RegExp(lodash_1.default.escapeRegExp(ipMask).replace(/\\\*/g, '.*'));
28
- }
29
- }
30
16
  function applyBlock(socket, ip = (0, connections_1.normalizeIp)(socket.remoteAddress || '')) {
31
- if (ip && blockFunctions.find(rule => rule(ip)))
17
+ if (ip && block.compiled().find(rule => rule(ip)))
32
18
  return socket.destroy();
33
19
  }
34
20
  exports.applyBlock = applyBlock;
package/src/config.js CHANGED
@@ -51,8 +51,11 @@ const { save } = (0, watchLoad_1.watchLoad)(path, values => setConfig(values ||
51
51
  setConfig({}, false);
52
52
  }
53
53
  });
54
- function defineConfig(k, defaultValue) {
54
+ function defineConfig(k, defaultValue, compiler = lodash_1.default.identity) {
55
55
  configProps[k] = { defaultValue };
56
+ let compiled = compiler(defaultValue);
57
+ if (compiler)
58
+ subscribeConfig(k, (v) => compiled = compiler(v));
56
59
  return {
57
60
  key() {
58
61
  return k;
@@ -68,7 +71,8 @@ function defineConfig(k, defaultValue) {
68
71
  this.set(v(this.get()));
69
72
  else
70
73
  setConfig1(k, v);
71
- }
74
+ },
75
+ compiled: () => compiled
72
76
  };
73
77
  }
74
78
  exports.defineConfig = defineConfig;