hfs 0.53.0 → 0.54.0-alpha3

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 (65) hide show
  1. package/admin/assets/index-CcUio6cm.css +1 -0
  2. package/admin/assets/index-DCsMhd8s.js +807 -0
  3. package/admin/assets/{sha512-rpja4qx0.js → sha512-VvQJXb6I.js} +1 -1
  4. package/admin/index.html +4 -2
  5. package/frontend/assets/index-legacy-vQymU_EM.js +60 -0
  6. package/frontend/assets/{sha512-legacy-pz5im6D1.js → sha512-legacy-BD2cMJIT.js} +1 -1
  7. package/frontend/index.html +2 -2
  8. package/package.json +8 -6
  9. package/plugins/antibrute/plugin.js +1 -1
  10. package/plugins/download-counter/plugin.js +0 -15
  11. package/plugins/list-uploader/public/main.js +3 -2
  12. package/src/acme.js +2 -2
  13. package/src/adminApis.js +23 -3
  14. package/src/api.accounts.js +1 -1
  15. package/src/api.auth.js +6 -4
  16. package/src/api.get_file_list.js +18 -9
  17. package/src/api.monitor.js +1 -1
  18. package/src/api.net.js +4 -1
  19. package/src/api.plugins.js +2 -0
  20. package/src/api.vfs.js +6 -4
  21. package/src/apiMiddleware.js +4 -1
  22. package/src/auth.js +7 -7
  23. package/src/basicWeb.js +1 -1
  24. package/src/block.js +13 -1
  25. package/src/commands.js +8 -2
  26. package/src/const.js +13 -10
  27. package/src/cross-const.js +2 -1
  28. package/src/cross.js +25 -8
  29. package/src/debounceAsync.js +3 -5
  30. package/src/dirStream.js +138 -0
  31. package/src/events.js +18 -8
  32. package/src/frontEndApis.js +32 -15
  33. package/src/github.js +21 -3
  34. package/src/langs/embedded.js +4 -1
  35. package/src/langs/hfs-lang-fi.json +5 -3
  36. package/src/langs/hfs-lang-hu.json +27 -19
  37. package/src/langs/hfs-lang-it.json +2 -1
  38. package/src/langs/hfs-lang-th.json +166 -0
  39. package/src/langs/hfs-lang-tr.json +166 -0
  40. package/src/langs/hfs-lang-uk.json +170 -0
  41. package/src/langs/hfs-lang-vi.json +5 -5
  42. package/src/listen.js +12 -4
  43. package/src/log.js +1 -1
  44. package/src/makeQ.js +31 -0
  45. package/src/middlewares.js +11 -8
  46. package/src/misc.js +2 -1
  47. package/src/nat.js +4 -4
  48. package/src/perm.js +1 -1
  49. package/src/persistence.js +2 -1
  50. package/src/plugins.js +22 -3
  51. package/src/serveFile.js +16 -14
  52. package/src/serveGuiAndSharedFiles.js +9 -4
  53. package/src/serveGuiFiles.js +8 -6
  54. package/src/srp.js +2 -2
  55. package/src/update.js +48 -7
  56. package/src/upload.js +33 -18
  57. package/src/util-files.js +7 -29
  58. package/src/vfs.js +23 -11
  59. package/src/watchLoad.js +1 -1
  60. package/src/zip.js +4 -4
  61. package/README.md +0 -232
  62. package/admin/assets/index-Ly9xr1F6.js +0 -811
  63. package/admin/assets/index-gNE0rRfC.css +0 -1
  64. package/frontend/assets/index-legacy-5MEl9EM8.js +0 -60
  65. /package/frontend/assets/{polyfills-legacy-31b7nyOi.js → polyfills-legacy-DMrMt_pQ.js} +0 -0
package/src/util-files.js CHANGED
@@ -11,8 +11,8 @@ const fs_1 = require("fs");
11
11
  const path_1 = require("path");
12
12
  const fast_glob_1 = __importDefault(require("fast-glob"));
13
13
  const const_1 = require("./const");
14
- const util_os_1 = require("./util-os");
15
14
  const stream_1 = require("stream");
15
+ const dirStream_1 = require("./dirStream");
16
16
  // @ts-ignore
17
17
  const unzip_stream_1 = __importDefault(require("unzip-stream"));
18
18
  async function isDirectory(path) {
@@ -77,36 +77,14 @@ function adjustStaticPathForGlob(path) {
77
77
  return fast_glob_1.default.escapePath(path.replace(/\\/g, '/'));
78
78
  }
79
79
  exports.adjustStaticPathForGlob = adjustStaticPathForGlob;
80
- async function* dirStream(path, { depth = 0, onlyFiles = false, onlyFolders = false } = {}) {
80
+ async function* dirStream(path, { depth = 0, onlyFiles = false, onlyFolders = false, hidden = true } = {}) {
81
81
  if (!await isDirectory(path))
82
82
  throw Error('ENOTDIR');
83
- const dirStream = fast_glob_1.default.stream(depth ? '**/*' : '*', {
84
- cwd: path,
85
- dot: true,
86
- deep: depth + 1,
87
- onlyFiles,
88
- onlyDirectories: onlyFolders,
89
- suppressErrors: true,
90
- objectMode: true,
91
- unique: false,
92
- });
93
- const skip = await getItemsToSkip(path);
94
- for await (const entry of dirStream) {
95
- let { path, dirent } = entry;
96
- const isDir = dirent.isDirectory();
97
- if (!isDir && !dirent.isFile())
83
+ for await (const entry of (0, dirStream_1.createDirStream)(path, { depth, hidden })) {
84
+ const dirent = entry;
85
+ if (dirent.isDirectory() ? onlyFiles : (onlyFolders || !dirent.isFile()))
98
86
  continue;
99
- path = String(path);
100
- if (!(skip === null || skip === void 0 ? void 0 : skip.includes(path)))
101
- yield [path, isDir];
102
- }
103
- async function getItemsToSkip(path) {
104
- if (!const_1.IS_WINDOWS)
105
- return;
106
- const winPath = path.replace(/\//g, '\\');
107
- const out = await (0, util_os_1.runCmd)('dir', ['/ah', '/b', depth ? '/s' : '/c', winPath]) // cannot pass '', so we pass /c as a noop parameter
108
- .catch(() => ''); // error in case of no matching file
109
- return out.split('\n').map(x => x.slice(!depth ? 0 : winPath.length + 1).trim().replace(/\\/g, '/'));
87
+ yield dirent;
110
88
  }
111
89
  }
112
90
  exports.dirStream = dirStream;
@@ -154,7 +132,7 @@ function createFileWithPath(path, options) {
154
132
  }
155
133
  exports.createFileWithPath = createFileWithPath;
156
134
  function isValidFileName(name) {
157
- return !/[/*?<>|\\]/.test(name) && !dirTraversal(name);
135
+ return !(const_1.IS_WINDOWS ? /[/:"*?<>|\\]/ : /\//).test(name) && !dirTraversal(name);
158
136
  }
159
137
  exports.isValidFileName = isValidFileName;
160
138
  function exists(path) {
package/src/vfs.js CHANGED
@@ -14,6 +14,8 @@ const const_1 = require("./const");
14
14
  const events_1 = __importDefault(require("./events"));
15
15
  const perm_1 = require("./perm");
16
16
  const auth_1 = require("./auth");
17
+ const fswin_1 = __importDefault(require("fswin"));
18
+ const showHiddenFiles = (0, config_1.defineConfig)('show_hidden_files', false);
17
19
  function permsFromParent(parent, child) {
18
20
  const ret = {};
19
21
  for (const k of misc_1.PERM_KEYS) {
@@ -88,7 +90,9 @@ async function urlToNode(url, ctx, parent = exports.vfs, getRest) {
88
90
  return urlToNode(rest, ctx, ret, getRest);
89
91
  if (ret.source)
90
92
  try {
91
- const st = await promises_1.default.stat(ret.source); // check existence
93
+ if (!showHiddenFiles.get() && await isHiddenFile(ret.source))
94
+ throw 'hiddenFile';
95
+ const st = ret.stats || await promises_1.default.stat(ret.source); // check existence
92
96
  ret.isFolder = st.isDirectory();
93
97
  }
94
98
  catch (_a) {
@@ -101,10 +105,12 @@ async function urlToNode(url, ctx, parent = exports.vfs, getRest) {
101
105
  return ret;
102
106
  }
103
107
  exports.urlToNode = urlToNode;
108
+ async function isHiddenFile(path) {
109
+ return const_1.IS_WINDOWS ? new Promise(res => fswin_1.default.getAttributes(path, x => res(x === null || x === void 0 ? void 0 : x.IS_HIDDEN)))
110
+ : path[0] === '.';
111
+ }
104
112
  async function getNodeByName(name, parent) {
105
113
  var _a;
106
- if (!(0, misc_1.isValidFileName)(name))
107
- return;
108
114
  // does the tree node have a child that goes by this name, otherwise attempt disk
109
115
  const child = ((_a = parent.children) === null || _a === void 0 ? void 0 : _a.find(isSameFilenameAs(name))) || childFromDisk();
110
116
  return child && applyParentToChild(child, parent, name);
@@ -121,6 +127,8 @@ async function getNodeByName(name, parent) {
121
127
  }
122
128
  ret.rename = renameUnderPath(parent.rename, name);
123
129
  }
130
+ if (!(0, misc_1.isValidFileName)(onDisk))
131
+ return;
124
132
  ret.source = (0, path_1.join)(parent.source, onDisk);
125
133
  ret.original = undefined; // overwrite in applyParentToChild, so we know this is not part of the vfs
126
134
  return ret;
@@ -157,16 +165,16 @@ function getNodeName(node) {
157
165
  }
158
166
  exports.getNodeName = getNodeName;
159
167
  async function nodeIsDirectory(node) {
160
- var _a;
168
+ var _a, _b, _c;
161
169
  if (node.isFolder !== undefined)
162
170
  return node.isFolder;
163
- const isFolder = Boolean(((_a = node.children) === null || _a === void 0 ? void 0 : _a.length) || !nodeIsLink(node) && (!node.source || await (0, misc_1.isDirectory)(node.source)));
171
+ const isFolder = Boolean(((_a = node.children) === null || _a === void 0 ? void 0 : _a.length) || !nodeIsLink(node) && ((_c = (_b = node.stats) === null || _b === void 0 ? void 0 : _b.isDirectory()) !== null && _c !== void 0 ? _c : (!node.source || await (0, misc_1.isDirectory)(node.source))));
164
172
  (0, misc_1.setHidden)(node, { isFolder }); // don't make it to the storage (a node.isTemp doesn't need it to be hidden)
165
173
  return isFolder;
166
174
  }
167
175
  exports.nodeIsDirectory = nodeIsDirectory;
168
176
  async function hasDefaultFile(node, ctx) {
169
- return node.default && await urlToNode(node.default, ctx, node) || undefined;
177
+ return node.default && await nodeIsDirectory(node) && await urlToNode(node.default, ctx, node) || undefined;
170
178
  }
171
179
  exports.hasDefaultFile = hasDefaultFile;
172
180
  function nodeIsLink(node) {
@@ -220,7 +228,7 @@ function statusCodeForMissingPerm(node, perm, ctx, assign = true) {
220
228
  exports.statusCodeForMissingPerm = statusCodeForMissingPerm;
221
229
  // it's responsibility of the caller to verify you have list permission on parent, as callers have different needs.
222
230
  async function* walkNode(parent, { ctx, depth = Infinity, prefixPath = '', requiredPerm, onlyFolders = false } = {}) {
223
- var _a;
231
+ var _a, _b;
224
232
  const { children, source } = parent;
225
233
  const took = prefixPath ? undefined : new Set();
226
234
  const maskApplier = parentMaskApplier(parent);
@@ -239,7 +247,7 @@ async function* walkNode(parent, { ctx, depth = Infinity, prefixPath = '', requi
239
247
  try {
240
248
  await promises_1.default.access(item.source);
241
249
  }
242
- catch (_b) {
250
+ catch (_c) {
243
251
  continue;
244
252
  }
245
253
  yield item;
@@ -256,13 +264,15 @@ async function* walkNode(parent, { ctx, depth = Infinity, prefixPath = '', requi
256
264
  && !hasPermission(parent, requiredPerm, ctx)
257
265
  && !masksCouldGivePermission(parent.masks, requiredPerm))
258
266
  return;
267
+ let n = 0;
259
268
  try {
260
269
  let lastDir = prefixPath.slice(0, -1) || '.';
261
270
  parentsCache.set(lastDir, parent);
262
- // it's important to keep using dirStream in deep-mode, as it is manyfold faster (it parallelizes)
263
- for await (const [path, isFolder] of (0, misc_1.dirStream)(source, { depth, onlyFolders })) {
271
+ for await (const entry of (0, misc_1.dirStream)(source, { depth, onlyFolders, hidden: showHiddenFiles.get() })) {
264
272
  if (ctx === null || ctx === void 0 ? void 0 : ctx.req.aborted)
265
273
  return;
274
+ const { path } = entry;
275
+ const isFolder = entry.isDirectory();
266
276
  const name = prefixPath + (((_a = parent.rename) === null || _a === void 0 ? void 0 : _a[path]) || path);
267
277
  if (took === null || took === void 0 ? void 0 : took.has(normalizeFilename(name)))
268
278
  continue;
@@ -281,11 +291,13 @@ async function* walkNode(parent, { ctx, depth = Infinity, prefixPath = '', requi
281
291
  parentsCache.set(name, item);
282
292
  if (canSee(item))
283
293
  yield item;
294
+ (_b = entry.closingBranch) === null || _b === void 0 ? void 0 : _b.then(p => parentsCache.delete(p || '.'));
284
295
  }
285
296
  }
286
297
  catch (e) {
287
- console.debug('glob', source, e); // ENOTDIR, or lacking permissions
298
+ console.debug('walkNode', source, e); // ENOTDIR, or lacking permissions
288
299
  }
300
+ parentsCache.clear(); // hoping for faster GC
289
301
  // item will be changed, so be sure to pass a temp node
290
302
  function canSee(item) {
291
303
  // we basename for depth>0 where we already have the rest of the path in the parent's url, and would be duplicated
package/src/watchLoad.js CHANGED
@@ -11,7 +11,7 @@ const misc_1 = require("./misc");
11
11
  function watchLoad(path, parser, { failedOnFirstAttempt, immediateFirst } = {}) {
12
12
  let doing = false;
13
13
  let watcher;
14
- const debounced = (0, misc_1.debounceAsync)(load, 500, { maxWait: 1000 });
14
+ const debounced = (0, misc_1.debounceAsync)(load, { wait: 500, maxWait: 1000 });
15
15
  let retry;
16
16
  let last;
17
17
  install(true);
package/src/zip.js CHANGED
@@ -17,12 +17,12 @@ const const_1 = require("./const");
17
17
  // expects 'node' to have had permissions checked by caller
18
18
  async function zipStreamFromFolder(node, ctx) {
19
19
  var _a;
20
- if ((0, vfs_1.statusCodeForMissingPerm)(node, 'can_archive', ctx))
20
+ const list = (_a = (0, misc_1.wantArray)(ctx.query.list)[0]) === null || _a === void 0 ? void 0 : _a.split('*'); // we are using * as separator because it cannot be used in a file name and doesn't need url encoding
21
+ if (!list && (0, vfs_1.statusCodeForMissingPerm)(node, 'can_archive', ctx))
21
22
  return;
22
23
  ctx.status = const_1.HTTP_OK;
23
24
  ctx.mime = 'zip';
24
25
  // ctx.query.list is undefined | string | string[]
25
- const list = (_a = (0, misc_1.wantArray)(ctx.query.list)[0]) === null || _a === void 0 ? void 0 : _a.split('*'); // we are using * as separator because it cannot be used in a file name and doesn't need url encoding
26
26
  const name = (list === null || list === void 0 ? void 0 : list.length) === 1 ? (0, misc_1.safeDecodeURIComponent)((0, path_1.basename)(list[0])) : (0, vfs_1.getNodeName)(node);
27
27
  ctx.attachment(((0, misc_1.isWindowsDrive)(name) ? name[0] : (name || 'archive')) + '.zip');
28
28
  const filter = (0, misc_1.pattern2filter)(String(ctx.query.search || ''));
@@ -33,7 +33,7 @@ async function zipStreamFromFolder(node, ctx) {
33
33
  if (!subNode)
34
34
  continue;
35
35
  if (await (0, vfs_1.nodeIsDirectory)(subNode)) { // a directory needs to walked
36
- if ((0, vfs_1.hasPermission)(subNode, 'can_list', ctx)) {
36
+ if ((0, vfs_1.hasPermission)(subNode, 'can_list', ctx) && (0, vfs_1.hasPermission)(subNode, 'can_archive', ctx)) {
37
37
  yield subNode; // it could be empty
38
38
  yield* (0, vfs_1.walkNode)(subNode, { ctx, prefixPath: decodeURI(uri) + '/', requiredPerm: 'can_archive' });
39
39
  }
@@ -58,7 +58,7 @@ async function zipStreamFromFolder(node, ctx) {
58
58
  return { path: name + '/' };
59
59
  if (!source)
60
60
  return;
61
- const st = await promises_1.default.stat(source);
61
+ const st = el.stats || await promises_1.default.stat(source);
62
62
  if (!st || !st.isFile())
63
63
  return;
64
64
  return {
package/README.md DELETED
@@ -1,232 +0,0 @@
1
- # HFS: HTTP File Server (version 3)
2
-
3
- ![logo and motto](hfs-logo-color-motto.svg)
4
-
5
- ## Introduction
6
-
7
- Access via web your files directly from your disk.
8
-
9
- - You be the server, share files **fresh from your disk**, with **unlimited** space and bandwidth.
10
- - **Fast!** Try zipping 100GB, download starts immediately!
11
- - **Intelligent**. HFS tries to detect problems and suggest solutions.
12
- - Share **even a single file** with our *virtual file system*, even with a different name, all without touching the real file. Present things the way you want!
13
- - **Watch** all activities in real-time.
14
- - **Control bandwidth**, decide how much to give.
15
- - **No intermediaries**, give a huge file to your friend without waiting for it to be uploaded on a server first.
16
-
17
- This is a full rewrite of [the Delphi version](https://github.com/rejetto/hfs2).
18
-
19
- ## How does it work
20
-
21
- - run HFS on your computer, administration page automatically shows up
22
- - select what files and folders you want to be accessible
23
- - access those files from a phone or another computer just using a browser
24
- - possibly create accounts and limit access to files
25
-
26
- ## Features
27
-
28
- - https
29
- - easy certificate generation
30
- - unicode
31
- - virtual file system
32
- - mobile friendly
33
- - search
34
- - accounts
35
- - resumable downloads & uploads
36
- - download folders as zip archive
37
- - delete, move and rename files
38
- - simple website serving
39
- - plug-ins
40
- - real-time monitoring of connections
41
- - [show some files](https://github.com/rejetto/hfs/discussions/270)
42
- - speed throttler
43
- - geographic firewall
44
- - admin web interface
45
- - multi-language front-end
46
- - virtual hosting
47
- - anti-brute-force (plug-in)
48
- - [reverse-proxy support](https://github.com/rejetto/hfs/wiki/Reverse-proxy)
49
- - comments in file descript.ion
50
- - integrated media player
51
- - [customizable with html, css and javascript](https://github.com/rejetto/hfs/wiki/Customization)
52
- - dynamic-dns updater
53
-
54
- ## Installation
55
-
56
- NB: minimum Windows version required is 8.1 , Windows Server 2012 R2 (because of Node.js 18)
57
-
58
- 1. go to https://github.com/rejetto/hfs/releases
59
- 2. click on `Assets`
60
- 3. **download** the right version for your system, unzip and launch `hfs` file.
61
- - Mac: if you get *"cannot be opened because it is from an unidentified developer"*,
62
- you can hold `control` key while clicking, then click `open`.
63
- - If you cannot find your system in the list, see next section [Other systems](#other-systems).
64
- 4. the browser should automatically open on `localhost` address, so you can configure the rest in the Admin-panel.
65
- - if a browser cannot be opened on the computer where you are installing HFS,
66
- you should enter this command in the HFS console: `create-admin <PASSWORD>`
67
- - if you cannot access the console (like when you are running as a service),
68
- you can [edit the config file to add your admin account](config.md#accounts)
69
- - if you don't want to use an editor you can create the file with this command:
70
-
71
- `echo "create-admin: PASSWORD" > config.yaml`
72
-
73
- If you access *Admin-panel* via localhost, by default HFS **won't** require you to login.
74
- If you don't like this behavior, disable it in the Admin-panel or enter this console command `config localhost_admin false`.
75
-
76
- ### Other systems
77
-
78
- If your system is not Windows/Linux/Mac or you just don't want to run the binaries, you can try this alternative version:
79
-
80
- 1. [install node.js](https://nodejs.org) version 18
81
- 2. execute at command line `npx hfs@latest`
82
-
83
- The `@latest` part is optional, and ensures that you are always up to date.
84
-
85
- If this procedure fails, it may be that you are missing one of [these requirements](https://github.com/nodejs/node-gyp#installation).
86
-
87
- Configuration and other files will be stored in `%HOME%/.vfs`
88
-
89
- ### Service
90
-
91
- If you want to run HFS at boot (as a service), we suggest the following methods
92
-
93
- #### On Linux
94
- 1. [install node.js](https://nodejs.org)
95
- 2. create a file `/etc/systemd/system/hfs.service` with this content
96
- ```
97
- [Unit]
98
- Description=HFS
99
- After=network.target
100
-
101
- [Service]
102
- Type=simple
103
- Restart=always
104
- ExecStart=/usr/bin/npx -y hfs@latest
105
-
106
- [Install]
107
- WantedBy=multi-user.target
108
- ```
109
- 3. run `sudo systemctl daemon-reload && sudo systemctl enable hfs && sudo systemctl start hfs && sudo systemctl status hfs`
110
-
111
- NB: update will be attempted at each restart
112
-
113
- #### On Windows
114
-
115
- 1. [install node.js](https://nodejs.org)
116
- 2. run `npm -g i hfs`
117
- 3. run `npx qckwinsvc2 install name="HFS" description="HFS" path="%APPDATA%\npm\node_modules\hfs\src\index.js" args="--cwd %HOMEPATH%\.hfs" now`
118
-
119
- To update
120
- - run `npx qckwinsvc2 uninstall name="HFS"`
121
- - run `npm -g update hfs`
122
- - run `npx qckwinsvc2 install name="HFS" description="HFS" path="%APPDATA%\npm\node_modules\hfs\src\index.js" args="--cwd %HOMEPATH%\.hfs" now`
123
-
124
- ## Console commands
125
-
126
- If you have full access to HFS' console, you can enter commands. Start with `help` to have a full list.
127
-
128
- ## Configuration
129
-
130
- For configuration please see [file config.md](config.md).
131
-
132
- ### Where is it stored
133
-
134
- Configuration is stored in the file `config.yaml`, exception made for custom HTML which is stored in `custom.html`.
135
-
136
- These files are kept in the Current Working Directory (cwd), which is by default the same folder of `hfs.exe`
137
- if you are using this kind of distribution on Windows, or `USER_FOLDER/.hfs` on other systems.
138
- You can decide a different folder passing `--cwd SOME_FOLDER` parameter at command line.
139
- Any relative path provided is relative to the *cwd*.
140
-
141
- [Check details about config file format](config.md).
142
-
143
- ## Internationalization
144
-
145
- It is possible to show the Front-end in other languages.
146
- Translation for some languages is already provided. If you find an error, consider reporting it
147
- or [editing the source file](https://github.com/rejetto/hfs/tree/main/src/langs).
148
-
149
- In the Languages section of the Admin-panel you can install additional language files.
150
-
151
- If your language is missing, please consider [translating yourself](https://github.com/rejetto/hfs/wiki/Translation).
152
-
153
- ## Why you should upgrade from HFS 2.x
154
-
155
- HFS 2.x is vulnerable to important attacks, and there is no known solution at the moment.
156
-
157
- As you can see from the list of features, we already have some goods that you cannot find in HFS 2.
158
- Other than that, you can also consider:
159
-
160
- - it's more robust: it was designed to be an always-running server, while HFS 1-2 was designed for occasional usage (transfer and quit)
161
- - passwords are never really stored, just a non-reversible hash is
162
- - faster search (up to 12x)
163
- - more flexible permissions
164
-
165
- ## Security
166
-
167
- While this project focuses on ease of use, we care about security.
168
- - HTTPS support
169
- - Passwords are not saved, and not disclosed even without https thanks to [SRP](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol)
170
- - Automated tests ran on every release, including libraries audit
171
- - No default admin password
172
-
173
- Some actions you can take for improved security:
174
- - use https, better if using a proper certificate, even free with [Letsencrypt](https://letsencrypt.org/).
175
- - have a domain (ddns is ok too), configure it in "Internet" page, and enable "Accept requests only using domain"
176
- - install "antidos" plugin
177
- - ensure "antibrute" plugin is running
178
- - disable "unprotected admin on localhost"
179
-
180
- ## Hidden features
181
-
182
- - Appending `#LOGIN` to address will bring up the login dialog
183
- - Appending ?lang=CODE to address will force a specific language
184
- - right/ctrl/command click on toggle-all checkbox will invert each checkbox state
185
- - Appending `?login=USER:PASSWORD` will automatically log in the browser
186
- - Appending `?overwrite` on uploads, will override the dont_overwrite_uploading configuration, provided you also have delete permission
187
- - Appending `?search=PATTERN` will trigger search at start
188
- - Right-click on "check for updates" will let you input a URL of a version to install
189
- - shift+click on a file will show & play
190
-
191
- ## Contribute
192
-
193
- There are several ways to contribute
194
-
195
- - [Report bugs](https://github.com/rejetto/hfs/issues/new?labels=bug&template=bug_report.md)
196
-
197
- It's very important to report bugs, and if you are not so sure about it, don't worry, we'll discuss it.
198
- If you find important security problems, please [contact us privately](mailto:a@rejetto.com) so that we can publish a fix before
199
- the problem is disclosed, for the safety of other users.
200
-
201
- - Use beta versions, and give feedback.
202
-
203
- While betas have more problems, you'll get more features and give a huge help to the project.
204
-
205
- - [Translate to your language](https://github.com/rejetto/hfs/wiki/Translation).
206
-
207
- - [Suggest ideas](https://github.com/rejetto/hfs/discussions)
208
-
209
- While the project should not become too complex, yours may be an idea for a plugin.
210
-
211
- - Write guides or make videos for other users. [We got a wiki](https://github.com/rejetto/hfs/wiki)!
212
-
213
- - Submit your code
214
-
215
- If you'd like to make a change yourself in the code, please first open an "issue" or "discussion" about it,
216
- so we'll try to cooperate and understand what's the best path for it.
217
-
218
- - [Make a plugin](dev-plugins.md)
219
-
220
- A plugin can change the look (a theme), and/or introduce a new functionality.
221
-
222
- ## More
223
-
224
- - [APIs](https://github.com/rejetto/hfs/wiki/APIs)
225
-
226
- - [Build yourself](dev.md)
227
-
228
- - [License](https://github.com/rejetto/hfs/blob/master/LICENSE.txt)
229
-
230
- - [To-do list](todo.md)
231
-
232
- - Flag images are public-domain, downloaded from https://flagpedia.net