hfs 3.0.2 → 3.0.3
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.
- package/README.md +194 -0
- package/package.json +7 -4
- package/src/api.auth.js +1 -0
- package/src/auth.js +5 -6
- package/src/config.js +2 -2
- package/src/consoleLog.js +1 -1
- package/src/log.js +2 -1
- package/src/middlewares.js +18 -9
- package/src/outboundProxy.js +3 -2
- package/src/serveFile.js +28 -13
- package/src/upload.js +4 -1
- package/src/util-http.js +24 -8
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# HFS: HTTP File Server
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
## Index
|
|
6
|
+
|
|
7
|
+
- [Introduction](#introduction)
|
|
8
|
+
- [How does it work](#how-does-it-work)
|
|
9
|
+
- [Features](#features)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Other systems](#other-systems)
|
|
12
|
+
- [Configuration](#configuration)
|
|
13
|
+
- [Where is it stored](#where-is-it-stored)
|
|
14
|
+
- [Internationalization](#internationalization)
|
|
15
|
+
- [Hidden features](#hidden-features)
|
|
16
|
+
- [Contribute](#contribute)
|
|
17
|
+
- [More](#more)
|
|
18
|
+
|
|
19
|
+
## Introduction
|
|
20
|
+
|
|
21
|
+
Access your files via web, directly from your disk.
|
|
22
|
+
|
|
23
|
+
- Be your own server: share files **fresh from your disk** with **unlimited** space and bandwidth.
|
|
24
|
+
- **Fast:** try zipping 100GB – download starts immediately.
|
|
25
|
+
- **Smart:** HFS detects problems and suggests solutions.
|
|
26
|
+
- Present things the way you want: share **even a single file**, even with a different name, with our *virtual file system*.
|
|
27
|
+
- **Monitor** all activities in real-time.
|
|
28
|
+
- **Bandwidth throttling**: decide how much to give.
|
|
29
|
+
- **Direct transfers**: share large files with friends without having to upload them first.
|
|
30
|
+
- **Expandable**: install plugins for additional features.
|
|
31
|
+
|
|
32
|
+
Runs on: Windows, Linux, macOS, FreeBSD, Android
|
|
33
|
+
|
|
34
|
+
## How does it work
|
|
35
|
+
|
|
36
|
+
- run HFS on your computer; an administration webpage automatically shows up
|
|
37
|
+
- select which files and folders you want to be accessible
|
|
38
|
+
- access those files from a phone or another computer just using a browser
|
|
39
|
+
- possibly create accounts and limit access to files
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- https
|
|
44
|
+
- easy certificate generation
|
|
45
|
+
- virtual file system
|
|
46
|
+
- mobile friendly
|
|
47
|
+
- search
|
|
48
|
+
- accounts
|
|
49
|
+
- resumable downloads & uploads
|
|
50
|
+
- download folders as zip archive
|
|
51
|
+
- delete, move and rename files
|
|
52
|
+
- plug-ins (anti-brute-force, thumbnails, ldap, themes, and more)
|
|
53
|
+
- simple website serving
|
|
54
|
+
- real-time monitoring of connections
|
|
55
|
+
- [show some files](https://github.com/rejetto/hfs/discussions/270)
|
|
56
|
+
- speed throttler
|
|
57
|
+
- geographic firewall
|
|
58
|
+
- admin web interface
|
|
59
|
+
- multi-language front-end
|
|
60
|
+
- virtual hosting
|
|
61
|
+
- [reverse-proxy support](https://github.com/rejetto/hfs/wiki/Reverse-proxy)
|
|
62
|
+
- comments in file descript.ion
|
|
63
|
+
- integrated media player
|
|
64
|
+
- [customizable with html, css, and javascript](https://github.com/rejetto/hfs/wiki/Customization)
|
|
65
|
+
- dynamic-dns updater
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
For Docker, see https://github.com/rejetto/hfs/wiki/Docker .
|
|
70
|
+
|
|
71
|
+
For service installation, see https://github.com/rejetto/hfs/wiki/Service-installation.
|
|
72
|
+
|
|
73
|
+
The minimum Windows version required is 10 or Server 2019.
|
|
74
|
+
|
|
75
|
+
1. Download the zip file for your operating system from https://github.com/rejetto/hfs/releases
|
|
76
|
+
- ⚠️ Antivirus problems on Windows? [READ THIS](https://github.com/rejetto/hfs/wiki/Antivirus)
|
|
77
|
+
- ⚠️ If you have Linux ARM or other unlisted/unsupported platforms, please see the [Other systems](#other-systems) section.
|
|
78
|
+
2. Unzip and launch the `hfs` file.
|
|
79
|
+
- ⚠️ Mac: if you get *"cannot be opened because it is from an unidentified developer"*,
|
|
80
|
+
you can hold `control` key while clicking, then click `open`.
|
|
81
|
+
3. The browser should automatically open at `localhost`, so you can configure the rest in the Admin-panel.
|
|
82
|
+
|
|
83
|
+
Troubleshooting
|
|
84
|
+
- If a browser cannot be opened on the computer where you are installing HFS,
|
|
85
|
+
you should enter this command in the HFS console: `create-admin <PASSWORD>`
|
|
86
|
+
- If you cannot access the console (like when you are running as a service),
|
|
87
|
+
you can [edit the config file to add your admin account](config.md#accounts)
|
|
88
|
+
- If you don't want to use an editor, you can create the file with this command:
|
|
89
|
+
|
|
90
|
+
`echo "create-admin: PASSWORD" > config.yaml`
|
|
91
|
+
|
|
92
|
+
By default, HFS does not require a login when you access the *Admin-panel* from localhost.
|
|
93
|
+
If you don't like this behavior, disable it in the Admin-panel or enter this console command `config localhost_admin false`.
|
|
94
|
+
|
|
95
|
+
To uninstall, remove the files you unzipped and the configuration/data directory (see `config.md` for the location).
|
|
96
|
+
|
|
97
|
+
### Other systems
|
|
98
|
+
|
|
99
|
+
If you can't or don't want to run our binary versions, you can try this:
|
|
100
|
+
|
|
101
|
+
1. [install node.js](https://nodejs.org) version 20 (or greater, but then compatibility is not guaranteed)
|
|
102
|
+
2. run at the command line `npx hfs@latest`
|
|
103
|
+
|
|
104
|
+
The `@latest` part is optional, and ensures that you are always up to date.
|
|
105
|
+
|
|
106
|
+
If this procedure fails, it may be that you are missing one of [these requirements](https://github.com/nodejs/node-gyp#installation).
|
|
107
|
+
|
|
108
|
+
Configuration and other files will be stored in `%HOME%/.vfs`
|
|
109
|
+
|
|
110
|
+
## Configuration
|
|
111
|
+
|
|
112
|
+
For configuration please see [config.md](config.md); it explains also where all configurations are stored.
|
|
113
|
+
|
|
114
|
+
## Internationalization
|
|
115
|
+
|
|
116
|
+
It is possible to show the Front-end in other languages.
|
|
117
|
+
Translation for some languages is already provided. If you find an error, consider reporting it
|
|
118
|
+
or [editing the source file](https://github.com/rejetto/hfs/tree/main/src/langs).
|
|
119
|
+
|
|
120
|
+
In the Languages section of the Admin-panel you can install additional language files.
|
|
121
|
+
|
|
122
|
+
If your language is missing, please consider [translating yourself](https://github.com/rejetto/hfs/wiki/Translation).
|
|
123
|
+
|
|
124
|
+
## Hidden features
|
|
125
|
+
|
|
126
|
+
- Append `#LOGIN` to the URL to open the login dialog
|
|
127
|
+
- Append `?lang=CODE` to the URL to force a specific language
|
|
128
|
+
- `Right-click` on toggle-all checkbox to *invert* the state of all checkboxes
|
|
129
|
+
- Append `?login=USER:PASSWORD` to automatically log in to the browser
|
|
130
|
+
- Append `?overwrite` when uploading to override the `dont_overwrite_uploading` configuration, provided you also have *delete* permission
|
|
131
|
+
- Append `?search=PATTERN` to trigger a search on startup
|
|
132
|
+
- Append `?onlyFiles` or `?onlyFolders` to limit the type of results
|
|
133
|
+
- Append `?get=basic` to display a basic web interface, intended for older/simpler browsers
|
|
134
|
+
- This is automatic if a basic browser is detected.
|
|
135
|
+
- Append `?autoplay=shuffle` to trigger show & play; `?autoplay` will not shuffle, but also will not start until the list is complete
|
|
136
|
+
- `Right-click` on "check for updates" to enter a URL of a version to install
|
|
137
|
+
- `Shift+click` on a file to *show* and play
|
|
138
|
+
- `Ctrl+backspace` to navigate to the parent folder
|
|
139
|
+
- Start typing a filename to focus it in the list
|
|
140
|
+
- `--consoleFile PATH` to also output all stdout/stderr to a file
|
|
141
|
+
- Set env.var. `DISABLE_UPDATE=1` (for containers)
|
|
142
|
+
- Launch with `--debug` or env.var. `HFS_DEBUG=1` to generate additional console logs
|
|
143
|
+
- Launch with `--no-central` to skip fetching updated info from GitHub (uses built-in data only)
|
|
144
|
+
|
|
145
|
+
## Contribute
|
|
146
|
+
|
|
147
|
+
There are several ways to contribute
|
|
148
|
+
|
|
149
|
+
- [Report bugs](https://github.com/rejetto/hfs/issues/new?labels=bug&template=bug_report.md)
|
|
150
|
+
|
|
151
|
+
It's very important to report bugs, and if you are not so sure about it, don't worry, we'll discuss it.
|
|
152
|
+
If you find important security problems, please [contact us privately](mailto:a@rejetto.com) so that we can publish a fix before
|
|
153
|
+
the problem is disclosed, for the safety of other users.
|
|
154
|
+
|
|
155
|
+
- Use beta versions, and give feedback.
|
|
156
|
+
|
|
157
|
+
While betas have more problems, you'll get more features and give a huge help to the project.
|
|
158
|
+
|
|
159
|
+
- [Translate to your language](https://github.com/rejetto/hfs/wiki/Translation).
|
|
160
|
+
|
|
161
|
+
- [Suggest ideas](https://github.com/rejetto/hfs/discussions)
|
|
162
|
+
|
|
163
|
+
While the project should not become too complex, yours may be an idea for a plugin.
|
|
164
|
+
|
|
165
|
+
- Write guides or make videos for other users. [We got a wiki](https://github.com/rejetto/hfs/wiki)!
|
|
166
|
+
|
|
167
|
+
- Submit your code
|
|
168
|
+
|
|
169
|
+
If you'd like to make a change yourself in the code, please first open an "issue" or "discussion" about it,
|
|
170
|
+
so we'll try to cooperate and understand what's the best path for it.
|
|
171
|
+
|
|
172
|
+
- [Make a plugin](dev-plugins.md)
|
|
173
|
+
|
|
174
|
+
A plugin can change the look (a theme), and/or introduce a new functionality.
|
|
175
|
+
|
|
176
|
+
## Code Signing Policy
|
|
177
|
+
|
|
178
|
+
Free code signing 🙏 provided by SignPath.io, certificate by [SignPath Foundation](https://signpath.org).
|
|
179
|
+
|
|
180
|
+
Author/Reviewer/Approver: Massimo Melina.
|
|
181
|
+
|
|
182
|
+
Privacy: Update checks are opt-out; other outbound connections are user-triggered.
|
|
183
|
+
|
|
184
|
+
## More
|
|
185
|
+
|
|
186
|
+
- [Additional information (Wiki)](https://github.com/rejetto/hfs/wiki)
|
|
187
|
+
|
|
188
|
+
- [APIs](https://github.com/rejetto/hfs/wiki/APIs)
|
|
189
|
+
|
|
190
|
+
- [Build yourself](dev.md)
|
|
191
|
+
|
|
192
|
+
- [License](LICENSE.txt)
|
|
193
|
+
|
|
194
|
+
- Flag images are public-domain, downloaded from https://flagpedia.net
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hfs",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "HTTP File Server",
|
|
5
5
|
"keywords": ["file server", "http server"],
|
|
6
6
|
"homepage": "https://rejetto.com/hfs",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dist-bin-mac-arm": "cd dist && pkg . --public -C gzip -t node20-macos-arm64 && zip hfs-mac-arm64-$(jq -r .version ../package.json).zip hfs -r plugins && cd ..",
|
|
34
34
|
"dist-bin-mac": "cd dist && pkg . --public -C gzip -t node20-macos-x64 && zip hfs-mac-x64-$(jq -r .version ../package.json).zip hfs -r plugins && cd ..",
|
|
35
35
|
"dist-bin-linux": "cd dist && pkg . --public -C gzip -t node20-linux-x64 && zip hfs-linux-x64-$(jq -r .version ../package.json).zip hfs -r plugins && cd ..",
|
|
36
|
-
"dist-bin-linux-arm": "cd dist && pkg . --public -C gzip -t node20-linux-arm64 && zip hfs-linux-arm64-$(jq -r .version ../package.json).zip hfs -r plugins && cd ..",
|
|
36
|
+
"dist-bin-linux-arm": "cd dist && pkg . --public -C gzip -t node20-linux-arm64 ${GITHUB_ACTIONS:+--public-packages \"*\"} && zip hfs-linux-arm64-$(jq -r .version ../package.json).zip hfs -r plugins && cd ..",
|
|
37
37
|
"dist-win": "npm run dist-modules && npm run dist-bin-win",
|
|
38
38
|
"dist-mac-arm": "npm run dist-modules && npm run dist-bin-mac-arm",
|
|
39
39
|
"dist-mac": "npm run dist-modules && npm run dist-bin-mac",
|
|
@@ -64,7 +64,10 @@
|
|
|
64
64
|
"admin/**/*",
|
|
65
65
|
"frontend/**/*",
|
|
66
66
|
"**/node_modules/fswin/x64/*",
|
|
67
|
-
"**/node_modules/axios/dist/node/*"
|
|
67
|
+
"**/node_modules/axios/dist/node/*",
|
|
68
|
+
"**/node_modules/buffers/**",
|
|
69
|
+
"**/node_modules/binary/**",
|
|
70
|
+
"**/node_modules/unzip-stream/**"
|
|
68
71
|
],
|
|
69
72
|
"targets": [
|
|
70
73
|
"node20-win-x64",
|
|
@@ -119,7 +122,7 @@
|
|
|
119
122
|
"@types/node": "^20.17.30",
|
|
120
123
|
"@types/node-forge": "^1.3.14",
|
|
121
124
|
"@types/picomatch": "^4.0.2",
|
|
122
|
-
"@yao-pkg/pkg": "
|
|
125
|
+
"@yao-pkg/pkg": "6.13.1",
|
|
123
126
|
"cross-env": "^10.0.0",
|
|
124
127
|
"koa-better-http-proxy": "^0.2.10",
|
|
125
128
|
"nm-prune": "^5.0.0",
|
package/src/api.auth.js
CHANGED
|
@@ -24,6 +24,7 @@ const login = async ({ username, password }, ctx) => {
|
|
|
24
24
|
const account = await (0, auth_1.clearTextLogin)(ctx, username, password, 'api');
|
|
25
25
|
if (!account)
|
|
26
26
|
return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED);
|
|
27
|
+
await (0, auth_1.setLoggedIn)(ctx, account.username);
|
|
27
28
|
}
|
|
28
29
|
catch (e) {
|
|
29
30
|
return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, String(e));
|
package/src/auth.js
CHANGED
|
@@ -51,11 +51,7 @@ async function clearTextLogin(ctx, u, p, via) {
|
|
|
51
51
|
return;
|
|
52
52
|
const plugins = await events_1.default.emitAsync('clearTextLogin', { ctx, username: u, password: p, via }); // provide clear password to plugins
|
|
53
53
|
const a = plugins?.some(x => x === true) ? (0, perm_1.getAccount)(u) : await srpCheck(u, p);
|
|
54
|
-
if (a)
|
|
55
|
-
await setLoggedIn(ctx, a.username);
|
|
56
|
-
ctx.headers['x-username'] = a.username; // give an easier way to determine if the login was successful
|
|
57
|
-
}
|
|
58
|
-
else if (u)
|
|
54
|
+
if (!a && u)
|
|
59
55
|
events_1.default.emit('failedLogin', { ctx, username: u, via });
|
|
60
56
|
return a;
|
|
61
57
|
}
|
|
@@ -64,8 +60,11 @@ async function setLoggedIn(ctx, username) {
|
|
|
64
60
|
const s = ctx.session;
|
|
65
61
|
if (!s)
|
|
66
62
|
return ctx.throw(cross_const_1.HTTP_SERVER_ERROR, 'session');
|
|
63
|
+
delete ctx.state.usernames;
|
|
67
64
|
if (username === false) {
|
|
68
|
-
|
|
65
|
+
if (s.username)
|
|
66
|
+
events_1.default.emit('logout', ctx);
|
|
67
|
+
delete ctx.state.account;
|
|
69
68
|
delete s.username;
|
|
70
69
|
delete s.allowNet;
|
|
71
70
|
return;
|
package/src/config.js
CHANGED
|
@@ -82,7 +82,7 @@ function defineConfig(k, defaultValue, compiler) {
|
|
|
82
82
|
return; // avoid infinite loop in case a subscriber changes the value
|
|
83
83
|
stack.push(cb);
|
|
84
84
|
try {
|
|
85
|
-
cb(v, { k, was, version, defaultValue, object, onlyCompileChanged });
|
|
85
|
+
return cb(v, { k, was, version, defaultValue, object, onlyCompileChanged });
|
|
86
86
|
}
|
|
87
87
|
finally {
|
|
88
88
|
stack.pop();
|
|
@@ -106,7 +106,7 @@ function defineConfig(k, defaultValue, compiler) {
|
|
|
106
106
|
if (compiler)
|
|
107
107
|
object.sub((v, more) => {
|
|
108
108
|
if (!more.onlyCompileChanged)
|
|
109
|
-
compiled = compiler(v, more);
|
|
109
|
+
return compiled = compiler(v, more);
|
|
110
110
|
});
|
|
111
111
|
return object;
|
|
112
112
|
}
|
package/src/consoleLog.js
CHANGED
|
@@ -10,7 +10,7 @@ const cross_1 = require("./cross");
|
|
|
10
10
|
const fs_1 = require("fs");
|
|
11
11
|
const argv_1 = require("./argv");
|
|
12
12
|
exports.consoleLog = [];
|
|
13
|
-
const f = argv_1.argv.consoleFile ? (0, fs_1.createWriteStream)(argv_1.argv.consoleFile, '
|
|
13
|
+
const f = argv_1.argv.consoleFile ? (0, fs_1.createWriteStream)(argv_1.argv.consoleFile, { flags: 'a', encoding: 'utf8' }) : null;
|
|
14
14
|
for (const k of ['log', 'warn', 'error', 'debug']) {
|
|
15
15
|
const original = console[k];
|
|
16
16
|
console[k] = (...args) => {
|
package/src/log.js
CHANGED
|
@@ -102,6 +102,7 @@ const logSpam = (0, config_1.defineConfig)(misc_1.CFG.log_spam, false);
|
|
|
102
102
|
const debounce = lodash_1.default.debounce(cb => cb(), 1000); // with this technique, i'll be able to debounce some code respecting the references in its closure
|
|
103
103
|
const logMw = async (ctx, next) => {
|
|
104
104
|
const now = new Date(); // request start
|
|
105
|
+
const userAtStart = (0, auth_1.getCurrentUsername)(ctx);
|
|
105
106
|
// do it now so it's available for returning plugins
|
|
106
107
|
ctx.state.completed = Promise.race([(0, events_1.once)(ctx.res, 'finish'), (0, events_1.once)(ctx.res, 'close')]);
|
|
107
108
|
await next();
|
|
@@ -151,7 +152,7 @@ const logMw = async (ctx, next) => {
|
|
|
151
152
|
const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s %s\n'; // Apache's Common Log Format
|
|
152
153
|
const a = now.toString().split(' '); // like nginx, our default log contains the time of log writing
|
|
153
154
|
const date = a[2] + '/' + a[1] + '/' + a[3] + ':' + a[4] + ' ' + a[5]?.slice(3);
|
|
154
|
-
const user = (0, auth_1.getCurrentUsername)(ctx);
|
|
155
|
+
const user = (0, auth_1.getCurrentUsername)(ctx) || userAtStart;
|
|
155
156
|
const length = ctx.state.length ?? ctx.length;
|
|
156
157
|
const uri = ctx.originalUrl;
|
|
157
158
|
const duration = (Date.now() - Number(now)) / 1000;
|
package/src/middlewares.js
CHANGED
|
@@ -82,21 +82,30 @@ const someSecurity = (ctx, next) => {
|
|
|
82
82
|
exports.someSecurity = someSecurity;
|
|
83
83
|
// limited to http proxies
|
|
84
84
|
function getProxyDetected() {
|
|
85
|
-
if (proxyDetected?.state.whenProxyDetected < Date.now() - misc_1.DAY) // detection is reset after a day
|
|
85
|
+
if (Number(proxyDetected?.state.whenProxyDetected) < Date.now() - misc_1.DAY) // detection is reset after a day
|
|
86
86
|
proxyDetected = undefined;
|
|
87
87
|
return proxyDetected && { from: proxyDetected.socket.remoteAddress, for: proxyDetected.get('X-Forwarded-For') };
|
|
88
88
|
}
|
|
89
89
|
const prepareState = async (ctx, next) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
const s = ctx.session;
|
|
91
|
+
if (s?.username) {
|
|
92
|
+
if (s.ts < auth_1.invalidateSessionBefore.get(s?.username))
|
|
93
|
+
delete s.username;
|
|
94
|
+
s.maxAge = exports.sessionDuration.compiled();
|
|
94
95
|
}
|
|
95
96
|
// calculate these once and for all
|
|
96
97
|
ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
let a = await urlLogin() || await getHttpAccount();
|
|
99
|
+
const loggedInNotBySession = a;
|
|
100
|
+
ctx.state.account = a ||= (0, perm_1.getAccount)(s?.username, false); // with least precedence, we consider session
|
|
101
|
+
if (a)
|
|
102
|
+
if (!(0, perm_1.accountCanLogin)(a) || failAllowNet(ctx, a)) // enforce allow_net also after login
|
|
103
|
+
await (0, auth_1.setLoggedIn)(ctx, false);
|
|
104
|
+
else if (loggedInNotBySession) {
|
|
105
|
+
if (a.username)
|
|
106
|
+
await (0, auth_1.setLoggedIn)(ctx, a.username);
|
|
107
|
+
ctx.headers['x-username'] = a.username; // give an easier way to determine if the login was successful
|
|
108
|
+
}
|
|
100
109
|
ctx.state.revProxyPath = ctx.get('x-forwarded-prefix');
|
|
101
110
|
(0, connections_1.updateConnectionForCtx)(ctx);
|
|
102
111
|
await next();
|
|
@@ -114,7 +123,7 @@ const prepareState = async (ctx, next) => {
|
|
|
114
123
|
return;
|
|
115
124
|
try {
|
|
116
125
|
const [u, p] = atob(b64).split(':');
|
|
117
|
-
if (!u || u ===
|
|
126
|
+
if (!u || u === s?.username)
|
|
118
127
|
return; // providing credentials, but not needed
|
|
119
128
|
return (0, auth_1.clearTextLogin)(ctx, u, p || '', 'header');
|
|
120
129
|
}
|
package/src/outboundProxy.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const config_1 = require("./config");
|
|
4
|
-
const node_url_1 = require("node:url");
|
|
5
4
|
const util_http_1 = require("./util-http");
|
|
6
5
|
const util_os_1 = require("./util-os");
|
|
7
6
|
const const_1 = require("./const");
|
|
8
7
|
const cross_1 = require("./cross");
|
|
9
8
|
const outboundProxy = (0, config_1.defineConfig)(cross_1.CFG.outbound_proxy, '', v => {
|
|
9
|
+
if (!v)
|
|
10
|
+
return;
|
|
10
11
|
try {
|
|
11
|
-
(0,
|
|
12
|
+
(0, util_http_1.parseHttpUrl)(v); // just validate
|
|
12
13
|
util_http_1.httpStream.defaultProxy = v;
|
|
13
14
|
if (!v || process.env.HFS_SKIP_PROXY_TEST)
|
|
14
15
|
return;
|
package/src/serveFile.js
CHANGED
|
@@ -101,12 +101,22 @@ async function serveFile(ctx, source, mime, content) {
|
|
|
101
101
|
ctx.set('Cache-Control', `max-age=${cc}`);
|
|
102
102
|
const { size } = stats;
|
|
103
103
|
const range = applyRange(ctx, size);
|
|
104
|
-
ctx.
|
|
104
|
+
if (ctx.status >= 400)
|
|
105
|
+
return; // applyRange may have set an error
|
|
106
|
+
ctx.body = (0, fs_1.createReadStream)(source, range || undefined);
|
|
105
107
|
if (ctx.state.vfsNode)
|
|
106
108
|
monitorAsDownload(ctx, size, range?.start);
|
|
107
109
|
}
|
|
108
110
|
catch (e) {
|
|
109
|
-
|
|
111
|
+
const status = {
|
|
112
|
+
ENOENT: const_1.HTTP_NOT_FOUND,
|
|
113
|
+
ENOTDIR: const_1.HTTP_NOT_FOUND,
|
|
114
|
+
EACCES: const_1.HTTP_FORBIDDEN,
|
|
115
|
+
EPERM: const_1.HTTP_FORBIDDEN,
|
|
116
|
+
}[String(e?.code)];
|
|
117
|
+
if (!status)
|
|
118
|
+
throw e;
|
|
119
|
+
ctx.status = status;
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
122
|
function monitorAsDownload(ctx, size, offset) {
|
|
@@ -131,27 +141,32 @@ function applyRange(ctx, totalSize = ctx.response.length) {
|
|
|
131
141
|
}
|
|
132
142
|
const [unit, ranges] = range.split('=');
|
|
133
143
|
if (unit !== 'bytes')
|
|
134
|
-
return
|
|
144
|
+
return badRequest('bad range unit');
|
|
135
145
|
if (ranges?.includes(','))
|
|
136
|
-
return
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
return
|
|
146
|
+
return badRequest('multi-range not supported');
|
|
147
|
+
const bytes = ranges?.split('-');
|
|
148
|
+
if (bytes?.length !== 2)
|
|
149
|
+
return badRequest('bad range');
|
|
140
150
|
const max = totalSize - 1;
|
|
141
|
-
const
|
|
142
|
-
const
|
|
151
|
+
const [startTxt, endTxt] = bytes;
|
|
152
|
+
const start = startTxt ? Number(startTxt) : Math.max(0, totalSize - Number(endTxt)); // a negative start is relative to the end
|
|
153
|
+
const end = (startTxt && endTxt) ? Math.min(max, Number(endTxt)) : max;
|
|
154
|
+
if (isNaN(start) || startTxt && endTxt && isNaN(end))
|
|
155
|
+
return badRequest('bad range');
|
|
143
156
|
// we don't support last-bytes without knowing max
|
|
144
|
-
if (isNaN(end) && isNaN(max) || end > max || start > max) {
|
|
145
|
-
ctx.status = const_1.HTTP_RANGE_NOT_SATISFIABLE;
|
|
157
|
+
if (isNaN(end) && isNaN(max) || end > max || start > max || start > end) {
|
|
146
158
|
ctx.set('Content-Range', `bytes */${totalSize}`);
|
|
147
|
-
|
|
148
|
-
return;
|
|
159
|
+
return badRequest('Requested Range Not Satisfiable', const_1.HTTP_RANGE_NOT_SATISFIABLE);
|
|
149
160
|
}
|
|
150
161
|
ctx.state.includesLastByte = end === max;
|
|
151
162
|
ctx.status = const_1.HTTP_PARTIAL_CONTENT;
|
|
152
163
|
ctx.set('Content-Range', `bytes ${start}-${isNaN(end) ? '' : end}/${isNaN(totalSize) ? '*' : totalSize}`);
|
|
153
164
|
ctx.response.length = end - start + 1;
|
|
154
165
|
return { start, end };
|
|
166
|
+
function badRequest(message, status = const_1.HTTP_BAD_REQUEST) {
|
|
167
|
+
ctx.status = status;
|
|
168
|
+
ctx.body = message;
|
|
169
|
+
}
|
|
155
170
|
}
|
|
156
171
|
function downloadLimiter(configMax, cbKey) {
|
|
157
172
|
const map = new Map();
|
package/src/upload.js
CHANGED
|
@@ -189,7 +189,10 @@ function uploadWriter(base, baseUri, filename, ctx) {
|
|
|
189
189
|
while (fs_1.default.existsSync(dest));
|
|
190
190
|
}
|
|
191
191
|
try {
|
|
192
|
-
await (0,
|
|
192
|
+
const done = await (0, misc_1.waitFor)(// an antivirus may lock the temp file to scan it
|
|
193
|
+
() => (0, promises_1.rename)(tempName, dest).then(() => true, e => e?.code !== 'EBUSY' && Promise.reject(e)), { timeout: 10_000 });
|
|
194
|
+
if (!done)
|
|
195
|
+
throw 'EBUSY';
|
|
193
196
|
if (mtime) // so we use it to touch the file
|
|
194
197
|
await (0, promises_1.utimes)(dest, Date.now() / 1000, mtime / 1000);
|
|
195
198
|
cancelDeletion(tempName); // not necessary, as deletion's failure is silent, but still
|
package/src/util-http.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.stream2string = void 0;
|
|
|
41
41
|
exports.httpString = httpString;
|
|
42
42
|
exports.httpWithBody = httpWithBody;
|
|
43
43
|
exports.httpStream = httpStream;
|
|
44
|
+
exports.parseHttpUrl = parseHttpUrl;
|
|
44
45
|
const node_url_1 = require("node:url");
|
|
45
46
|
const node_https_1 = __importDefault(require("node:https"));
|
|
46
47
|
const node_http_1 = __importDefault(require("node:http"));
|
|
@@ -49,6 +50,7 @@ const lodash_1 = __importDefault(require("lodash"));
|
|
|
49
50
|
const consumers_1 = require("node:stream/consumers");
|
|
50
51
|
Object.defineProperty(exports, "stream2string", { enumerable: true, get: function () { return consumers_1.text; } });
|
|
51
52
|
const tls = __importStar(require("node:tls"));
|
|
53
|
+
const cross_1 = require("./cross");
|
|
52
54
|
async function httpString(url, options) {
|
|
53
55
|
return await (0, consumers_1.text)(await httpStream(url, options));
|
|
54
56
|
}
|
|
@@ -74,11 +76,13 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
|
|
|
74
76
|
if (!(body instanceof node_stream_1.Readable))
|
|
75
77
|
options.headers['content-length'] ??= Buffer.byteLength(body);
|
|
76
78
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
const { auth, ...parsed } = parseHttpUrl(url);
|
|
80
|
+
const hostJar = jar && (jar[parsed.hostname || ''] ||= {});
|
|
81
|
+
if (hostJar) {
|
|
82
|
+
options.headers.cookie = lodash_1.default.map(hostJar, (v, k) => `${k}=${v}; `).join('')
|
|
79
83
|
+ (options.headers.cookie || ''); // preserve parameter
|
|
80
|
-
|
|
81
|
-
const proxyParsed = proxy ? (
|
|
84
|
+
}
|
|
85
|
+
const proxyParsed = proxy ? parseHttpUrl(proxy) : null;
|
|
82
86
|
Object.assign(options, lodash_1.default.pick(proxyParsed || parsed, ['hostname', 'port', 'path', 'protocol']));
|
|
83
87
|
if (auth) {
|
|
84
88
|
options.auth = auth;
|
|
@@ -87,7 +91,7 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
|
|
|
87
91
|
}
|
|
88
92
|
if (proxy) {
|
|
89
93
|
options.path = url; // full url as path
|
|
90
|
-
options.headers.host ??=
|
|
94
|
+
options.headers.host ??= parsed.host || undefined; // keep original host header
|
|
91
95
|
}
|
|
92
96
|
// this needs the prefix "proxy-"
|
|
93
97
|
const proxyAuth = proxyParsed?.auth ? { 'proxy-authorization': `Basic ${Buffer.from(proxyParsed.auth, 'utf8').toString('base64')}` } : undefined;
|
|
@@ -97,15 +101,15 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
|
|
|
97
101
|
const proto = options.protocol === 'https:' ? node_https_1.default : node_http_1.default;
|
|
98
102
|
const req = proto.request(options, res => {
|
|
99
103
|
console.debug("http responded", res.statusCode, "to", url);
|
|
100
|
-
if (
|
|
104
|
+
if (hostJar)
|
|
101
105
|
for (const entry of res.headers['set-cookie'] || []) {
|
|
102
106
|
const [, k, v] = /(.+?)=([^;]+)/.exec(entry) || [];
|
|
103
107
|
if (!k)
|
|
104
108
|
continue;
|
|
105
109
|
if (v)
|
|
106
|
-
|
|
110
|
+
hostJar[k] = v;
|
|
107
111
|
else
|
|
108
|
-
delete
|
|
112
|
+
delete hostJar[k];
|
|
109
113
|
}
|
|
110
114
|
if (!res.statusCode || httpThrow && res.statusCode >= 400)
|
|
111
115
|
return reject(new Error(String(res.statusCode), { cause: res }));
|
|
@@ -161,3 +165,15 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
|
|
|
161
165
|
abort() { controller.abort(); }
|
|
162
166
|
});
|
|
163
167
|
}
|
|
168
|
+
// works the same way as the now deprecated url.parse()
|
|
169
|
+
function parseHttpUrl(url) {
|
|
170
|
+
const parsed = new URL(url);
|
|
171
|
+
const options = (0, node_url_1.urlToHttpOptions)(parsed);
|
|
172
|
+
const withoutHash = url.split('#', 1)[0];
|
|
173
|
+
const authority = /^[a-z][a-z\d+.-]*:\/\/[^/?#]*/i.exec(withoutHash)?.[0];
|
|
174
|
+
return {
|
|
175
|
+
...options,
|
|
176
|
+
host: parsed.host,
|
|
177
|
+
path: !authority ? '/' : (0, cross_1.enforceStarting)('/', withoutHash.slice(authority.length)), // unresolved paths are useful in our tests
|
|
178
|
+
};
|
|
179
|
+
}
|