hfs 3.3.3 → 3.4.0-alpha1
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/admin/assets/AccountsPage-C6J2hRcY.js +4 -0
- package/admin/assets/ConfigFilePage-D5TZZyMR.js +4 -0
- package/admin/assets/CustomHtmlPage-DV2-ufWZ.js +2 -0
- package/admin/assets/InternetPage-DT3wSQYQ.js +6 -0
- package/admin/assets/LangPage-DbXCv7wR.js +2 -0
- package/admin/assets/LogsPage-DmAxUZpu.js +3 -0
- package/admin/assets/MonitorPage-D7ydOeq_.js +2 -0
- package/admin/assets/OptionsPage-DpN3ajRm.js +3 -0
- package/admin/assets/PluginsPage-BYzfwuvg.js +1 -0
- package/admin/assets/icons-CfBG9vvB.js +1 -0
- package/admin/assets/index-CVCnYMoB.js +755 -0
- package/admin/assets/index-mI9kZlpb.css +1 -0
- package/admin/assets/routing-C4l8rnk_.js +1 -0
- package/admin/index.html +2 -2
- package/central.json +100 -1
- package/frontend/assets/{dist-legacy-CSuZ0eoA.js → dist-legacy-CKczq_fx.js} +1 -1
- package/frontend/assets/index-legacy-ZYae30AY.js +1 -0
- package/frontend/assets/{lib-legacy-I77jyH6j.js → lib-legacy-D2tyILlj.js} +1 -1
- package/frontend/assets/{sha512-legacy-DxvM1xc4.js → sha512-legacy-CFcDeiiP.js} +1 -1
- package/frontend/index.html +1 -1
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
- package/src/acme.js +143 -50
- package/src/acmeConfig.js +43 -0
- package/src/acmeDns.js +285 -0
- package/src/admin-langs/embedded.js +33 -0
- package/src/admin-langs/hfs-admin-lang-ar.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-bg.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-de.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-el.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-en.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-es.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-fi.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-fr.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-hu.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-it.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-ja.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-ko.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-lt.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-ms.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-nl.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-pl.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-pt-br.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-ro.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-ru.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-sr-latn.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-sr.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-th.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-tr.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-uk.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-vi.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-zh-tw.json +1141 -0
- package/src/admin-langs/hfs-admin-lang-zh.json +1141 -0
- package/src/adminApis.js +2 -2
- package/src/adminLang.js +50 -0
- package/src/api.lang.js +22 -13
- package/src/api.log.js +5 -3
- package/src/api.net.js +20 -3
- package/src/api.plugins.js +53 -19
- package/src/argv.js +4 -0
- package/src/auth.js +0 -2
- package/src/basicWeb.js +6 -2
- package/src/block.js +1 -1
- package/src/config.js +1 -0
- package/src/connections.js +4 -7
- package/src/const.js +1 -1
- package/src/cross-const.js +7 -5
- package/src/cross.js +15 -1
- package/src/frontEndApis.js +44 -7
- package/src/github.js +6 -37
- package/src/i18n.js +16 -4
- package/src/index.js +4 -2
- package/src/lang.js +15 -8
- package/src/langs/embedded.js +2 -1
- package/src/langs/hfs-lang-ar.json +5 -1
- package/src/langs/hfs-lang-bg.json +5 -1
- package/src/langs/hfs-lang-de.json +5 -1
- package/src/langs/hfs-lang-el.json +5 -1
- package/src/langs/hfs-lang-en.json +188 -0
- package/src/langs/hfs-lang-es.json +7 -3
- package/src/langs/hfs-lang-fi.json +6 -2
- package/src/langs/hfs-lang-fr.json +10 -6
- package/src/langs/hfs-lang-hu.json +5 -1
- package/src/langs/hfs-lang-it.json +9 -4
- package/src/langs/hfs-lang-ja.json +5 -1
- package/src/langs/hfs-lang-ko.json +5 -1
- package/src/langs/hfs-lang-lt.json +5 -1
- package/src/langs/hfs-lang-ms.json +7 -1
- package/src/langs/hfs-lang-nl.json +5 -1
- package/src/langs/hfs-lang-pl.json +5 -1
- package/src/langs/hfs-lang-pt-br.json +7 -3
- package/src/langs/hfs-lang-ro.json +5 -1
- package/src/langs/hfs-lang-ru.json +7 -3
- package/src/langs/hfs-lang-sr-latn.json +5 -1
- package/src/langs/hfs-lang-sr.json +5 -1
- package/src/langs/hfs-lang-th.json +6 -2
- package/src/langs/hfs-lang-tr.json +6 -2
- package/src/langs/hfs-lang-uk.json +14 -10
- package/src/langs/hfs-lang-vi.json +5 -1
- package/src/langs/hfs-lang-zh-tw.json +5 -1
- package/src/langs/hfs-lang-zh.json +5 -1
- package/src/log.js +7 -1
- package/src/middlewares.js +85 -14
- package/src/multipartUpload.js +21 -0
- package/src/perm.js +38 -20
- package/src/pluginCatalog.js +66 -0
- package/src/pluginUsage.js +64 -0
- package/src/plugins.js +12 -0
- package/src/roots.js +22 -13
- package/src/serveFile.js +26 -5
- package/src/serveGuiFiles.js +5 -2
- package/src/update.js +51 -18
- package/src/upload.js +49 -8
- package/src/util-http.js +31 -5
- package/src/vfs.js +84 -4
- package/src/webdav.js +12 -6
- package/admin/assets/AccountsPage-C_ayTmn7.js +0 -3
- package/admin/assets/ArrayField-B20NUH0C.js +0 -2
- package/admin/assets/ConfigFilePage-Dw0-gus3.js +0 -4
- package/admin/assets/CustomHtmlPage-CLGyZfZ8.js +0 -2
- package/admin/assets/DataTable-BwyFEGxv.js +0 -119
- package/admin/assets/DateTimeField-ZUy9IBhM.js +0 -1
- package/admin/assets/EventManager-DomWgRty.js +0 -1
- package/admin/assets/FileField-Bj3P7JUc.js +0 -117
- package/admin/assets/InternetPage-CGMQCcBM.js +0 -8
- package/admin/assets/LangPage-DGFrOItT.js +0 -2
- package/admin/assets/LogsPage-CvCYNhID.js +0 -4
- package/admin/assets/MenuButton-BTRAUH7H.js +0 -1
- package/admin/assets/MonitorPage-CVa2ILH2.js +0 -2
- package/admin/assets/OptionsPage-C3cprG9u.js +0 -3
- package/admin/assets/PluginsPage-CIOnncph.js +0 -1
- package/admin/assets/Tabs-zcHwsK2s.js +0 -1
- package/admin/assets/TextEditor-BeS-mR2I.css +0 -1
- package/admin/assets/TextEditor-CDOjRy40.js +0 -34
- package/admin/assets/VfsPathField-DHifSPJi.js +0 -1
- package/admin/assets/agentIcons-DB1m4pGa.js +0 -1
- package/admin/assets/baseUrl-Bt9R8Da2.js +0 -2
- package/admin/assets/icons-DR_Rvxtn.js +0 -1
- package/admin/assets/index-11n5z0Ez.css +0 -1
- package/admin/assets/index-B3difDBD.js +0 -477
- package/admin/assets/routing-CBrDVeFB.js +0 -1
- package/frontend/assets/index-legacy-tk5ZmBDE.js +0 -1
package/src/log.js
CHANGED
|
@@ -48,6 +48,7 @@ const lodash_1 = __importDefault(require("lodash"));
|
|
|
48
48
|
const util_files_1 = require("./util-files");
|
|
49
49
|
const auth_1 = require("./auth");
|
|
50
50
|
const misc_1 = require("./misc");
|
|
51
|
+
const vfs_1 = require("./vfs");
|
|
51
52
|
const path_1 = require("path");
|
|
52
53
|
const yazl_1 = __importDefault(require("yazl"));
|
|
53
54
|
const promises_1 = require("stream/promises");
|
|
@@ -109,6 +110,7 @@ const logRotation = (0, config_1.defineConfig)(misc_1.CFG.log_rotation, 'weekly'
|
|
|
109
110
|
const dontLogNet = (0, config_1.defineConfig)(misc_1.CFG.dont_log_net, '127.0.0.1|::1', v => (0, misc_1.makeNetMatcher)(v));
|
|
110
111
|
const logUA = (0, config_1.defineConfig)(misc_1.CFG.log_ua, false);
|
|
111
112
|
const logSpam = (0, config_1.defineConfig)(misc_1.CFG.log_spam, false);
|
|
113
|
+
const logSpamRegex = (0, config_1.defineConfig)(misc_1.CFG.log_spam_regex, /wlwmanifest.xml$|\.(php|asp)$|cgi|robots.txt$/.source, v => v ? new RegExp(v) : null);
|
|
112
114
|
const logHost = (0, config_1.defineConfig)(misc_1.CFG.log_host, false);
|
|
113
115
|
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
|
|
114
116
|
const logMw = async (ctx, next) => {
|
|
@@ -122,7 +124,9 @@ const logMw = async (ctx, next) => {
|
|
|
122
124
|
if (!logSpam.get()
|
|
123
125
|
&& (ctx.querystring.includes('{.exec|') // v2's bug
|
|
124
126
|
// requests generated by security scanners, other bots, and macos' finder
|
|
125
|
-
||
|
|
127
|
+
|| logSpamRegex.compiled()?.test(ctx.path)
|
|
128
|
+
&& (ctx.status === misc_1.HTTP_NOT_FOUND
|
|
129
|
+
|| ctx.status === misc_1.HTTP_TEMPORARY_REDIRECT_KEEP_METHOD && !await (0, vfs_1.urlToNode)(ctx.path, ctx)))) {
|
|
126
130
|
events_2.default.emit('spam', ctx);
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
@@ -140,6 +144,8 @@ const logMw = async (ctx, next) => {
|
|
|
140
144
|
let { stream, last, path } = logger;
|
|
141
145
|
if (!stream)
|
|
142
146
|
return;
|
|
147
|
+
if (events_2.default.emit('beforeLog', { ctx })?.isDefaultPrevented())
|
|
148
|
+
return;
|
|
143
149
|
const rotate = logRotation.get()?.[0];
|
|
144
150
|
const reqEnd = logger.last = new Date();
|
|
145
151
|
if (rotate && last) { // rotation enabled and a file exists?
|
package/src/middlewares.js
CHANGED
|
@@ -10,6 +10,9 @@ exports.failAllowNet = failAllowNet;
|
|
|
10
10
|
const koa_compress_1 = __importDefault(require("koa-compress"));
|
|
11
11
|
const const_1 = require("./const");
|
|
12
12
|
const misc_1 = require("./misc");
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const lang_1 = require("./lang");
|
|
15
|
+
const i18n_1 = require("./i18n");
|
|
13
16
|
const stream_1 = require("stream");
|
|
14
17
|
const block_1 = require("./block");
|
|
15
18
|
const perm_1 = require("./perm");
|
|
@@ -64,12 +67,50 @@ const someSecurity = (ctx, next) => {
|
|
|
64
67
|
const { URL } = ctx;
|
|
65
68
|
URL.protocol = 'https';
|
|
66
69
|
URL.port = (0, listen_1.getHttpsWorkingPort)();
|
|
67
|
-
ctx.status =
|
|
70
|
+
ctx.status = const_1.HTTP_TEMPORARY_REDIRECT_KEEP_METHOD; // this ensures the client doesn't switch to a simpler GET request
|
|
68
71
|
return ctx.redirect(URL.href);
|
|
69
72
|
}
|
|
73
|
+
const page = ctx.state.urlLoginPage;
|
|
74
|
+
if (page)
|
|
75
|
+
return sendUrlLoginPage(page.cleanUrl, page.username);
|
|
70
76
|
return next();
|
|
77
|
+
function sendUrlLoginPage(cleanUrl, username) {
|
|
78
|
+
return sendPage(ctx, username === undefined ? 'login_bad_credentials' : 'Confirm', t => {
|
|
79
|
+
if (username === undefined)
|
|
80
|
+
return `<script>history.replaceState(null,'',location.pathname)</script>
|
|
81
|
+
<p><a href="${(0, misc_1.escapeHTML)(cleanUrl)}">${(0, misc_1.escapeHTML)(t('Continue'))}</a></p>`;
|
|
82
|
+
const token = (0, node_crypto_1.randomUUID)();
|
|
83
|
+
(ctx.session ??= {}).urlLoginConfirmation = {
|
|
84
|
+
token,
|
|
85
|
+
username,
|
|
86
|
+
expires: Date.now() + 5 * 60_000,
|
|
87
|
+
};
|
|
88
|
+
return `<p>${(0, misc_1.escapeHTML)(t('confirm_url_login', { username }))}</p>
|
|
89
|
+
<p><strong><a class="confirm" href="${(0, misc_1.escapeHTML)(cleanUrl)}">${(0, misc_1.escapeHTML)(t('Continue'))}</a></strong>
|
|
90
|
+
<a href="${(0, misc_1.escapeHTML)(cleanUrl)}">${(0, misc_1.escapeHTML)(t('Cancel'))}</a></p>
|
|
91
|
+
<script>
|
|
92
|
+
const loginUrl=new URL(location.href);
|
|
93
|
+
history.replaceState(null,'',loginUrl.pathname);
|
|
94
|
+
document.querySelector('.confirm').onclick=ev=>{ev.preventDefault();loginUrl.searchParams.set('login_confirm','${token}');location.replace(loginUrl)}
|
|
95
|
+
</script>`;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
71
98
|
};
|
|
72
99
|
exports.someSecurity = someSecurity;
|
|
100
|
+
async function sendPage(ctx, titleKey, content) {
|
|
101
|
+
const { t } = (0, i18n_1.i18nFromTranslations)(await (0, lang_1.getLangData)(ctx));
|
|
102
|
+
const title = t(titleKey);
|
|
103
|
+
ctx.status = 200;
|
|
104
|
+
ctx.type = 'html';
|
|
105
|
+
ctx.set('cache-control', 'no-store');
|
|
106
|
+
ctx.set('referrer-policy', 'no-referrer');
|
|
107
|
+
ctx.set('content-security-policy', "frame-ancestors 'none'");
|
|
108
|
+
ctx.set('x-frame-options', 'DENY');
|
|
109
|
+
ctx.body = `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
110
|
+
<title>${(0, misc_1.escapeHTML)(title)}</title><style>
|
|
111
|
+
body{margin-top:30vh;text-align:center;font-family:sans-serif}a{margin:1em}
|
|
112
|
+
</style></head><body><h1>${(0, misc_1.escapeHTML)(title)}</h1>${content(t)}</body></html>`;
|
|
113
|
+
}
|
|
73
114
|
function enforceSessionIp(ctx) {
|
|
74
115
|
const s = ctx.session;
|
|
75
116
|
if (!s?.username || s[misc_1.ALLOW_SESSION_IP_CHANGE])
|
|
@@ -89,7 +130,7 @@ function getProxyDetected() {
|
|
|
89
130
|
}
|
|
90
131
|
const prepareState = async (ctx, next) => {
|
|
91
132
|
// normalize once so auth, filters and logging agree on the same client address
|
|
92
|
-
ctx.request.ip = (0,
|
|
133
|
+
ctx.request.ip = (0, misc_1.normalizeIp)(ctx.ip);
|
|
93
134
|
// invalidate before account resolution; someSecurity calls again to bind logins made below
|
|
94
135
|
enforceSessionIp(ctx);
|
|
95
136
|
// rootsMiddleware consults proxy-aware admin access before someSecurity runs
|
|
@@ -115,7 +156,9 @@ const prepareState = async (ctx, next) => {
|
|
|
115
156
|
ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
|
|
116
157
|
// explicit credentials and existing sessions must take precedence, so a matching IP cannot override a chosen account
|
|
117
158
|
let via;
|
|
118
|
-
|
|
159
|
+
const urlAccount = await urlLogin();
|
|
160
|
+
let a = ctx.state.urlLoginPage ? undefined
|
|
161
|
+
: urlAccount || await getHttpAccount() || !s?.username && autoLogin();
|
|
119
162
|
const loggedInNotBySession = a;
|
|
120
163
|
ctx.state.account = a ||= (0, perm_1.getAccount)(s?.username, false); // with least precedence, we consider session
|
|
121
164
|
if (a)
|
|
@@ -137,14 +180,43 @@ const prepareState = async (ctx, next) => {
|
|
|
137
180
|
ctx.state.revProxyPath = ctx.get('x-forwarded-prefix');
|
|
138
181
|
(0, connections_1.updateConnectionForCtx)(ctx);
|
|
139
182
|
await next();
|
|
140
|
-
function urlLogin() {
|
|
183
|
+
async function urlLogin() {
|
|
141
184
|
via = 'url';
|
|
142
185
|
const { login } = ctx.query;
|
|
143
186
|
if (!login)
|
|
144
187
|
return;
|
|
145
188
|
const [u, p] = (0, misc_1.splitAt)(':', String(login));
|
|
146
|
-
ctx.
|
|
147
|
-
|
|
189
|
+
const cleanUrl = ctx.originalUrl.slice(0, -ctx.querystring.length - 1);
|
|
190
|
+
const token = String(ctx.query.login_confirm || '');
|
|
191
|
+
const pending = s?.urlLoginConfirmation;
|
|
192
|
+
const confirmed = Boolean(token && pending?.token === token && pending.username === u
|
|
193
|
+
&& pending.expires > Date.now());
|
|
194
|
+
if (token && s)
|
|
195
|
+
delete s.urlLoginConfirmation; // confirmation tokens are single-use, including failed attempts
|
|
196
|
+
if (!confirmed && needsConfirmation(u)) {
|
|
197
|
+
ctx.state.urlLoginPage = { username: u, cleanUrl };
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (s)
|
|
201
|
+
delete s.urlLoginConfirmation;
|
|
202
|
+
const account = u && await (0, auth_1.clearTextLogin)(ctx, u, p, 'url');
|
|
203
|
+
if (!account && confirmed)
|
|
204
|
+
ctx.state.urlLoginPage = { cleanUrl };
|
|
205
|
+
else
|
|
206
|
+
ctx.redirect(cleanUrl); // redirect to hide credentials
|
|
207
|
+
return account;
|
|
208
|
+
function needsConfirmation(username) {
|
|
209
|
+
if (ctx.get('sec-fetch-mode') !== 'navigate' && !/^Mozilla\//.test(ctx.get('user-agent')))
|
|
210
|
+
return false;
|
|
211
|
+
const current = s?.username;
|
|
212
|
+
if (current)
|
|
213
|
+
return (0, perm_1.getAccount)(username)?.username !== (0, perm_1.getAccount)(current)?.username;
|
|
214
|
+
const fetchSite = ctx.get('sec-fetch-site');
|
|
215
|
+
if (fetchSite)
|
|
216
|
+
return fetchSite === 'same-site' || fetchSite === 'cross-site';
|
|
217
|
+
const referer = ctx.get('referer');
|
|
218
|
+
return !referer || (0, misc_1.try_)(() => new URL(referer).host.toLowerCase() !== ctx.host.toLowerCase(), () => true);
|
|
219
|
+
}
|
|
148
220
|
}
|
|
149
221
|
function getHttpAccount() {
|
|
150
222
|
via = 'header';
|
|
@@ -167,20 +239,19 @@ const prepareState = async (ctx, next) => {
|
|
|
167
239
|
};
|
|
168
240
|
exports.prepareState = prepareState;
|
|
169
241
|
function failAllowNet(ctx, a) {
|
|
170
|
-
|
|
171
|
-
const sameAccount = ctx.session?.username === (0, perm_1.normalizeUsername)(a?.username || '');
|
|
172
|
-
const cached = sameAccount ? ctx.session?.allowNet : undefined;
|
|
173
|
-
const mask = cached ?? (0, perm_1.getFromAccount)(a || '', a => a.allow_net);
|
|
174
|
-
if (sameAccount && !cached && mask)
|
|
175
|
-
ctx.session.allowNet = mask; // must be deleted on logout by setLoggedIn
|
|
242
|
+
const mask = (0, perm_1.getFromAccount)(a || '', a => a.allow_net);
|
|
176
243
|
const ret = mask && !(0, misc_1.netMatches)(ctx.ip, mask, true);
|
|
177
244
|
if (ret)
|
|
178
245
|
console.debug("Login failed: allow_net");
|
|
179
246
|
return ret;
|
|
180
247
|
}
|
|
181
248
|
const paramsDecoder = async (ctx, next) => {
|
|
182
|
-
|
|
183
|
-
|
|
249
|
+
if (ctx.method !== 'POST' || !ctx.originalUrl.startsWith(const_1.API_URI))
|
|
250
|
+
return next();
|
|
251
|
+
const body = await (0, misc_1.readRequestBodyLimited)(ctx, 10 * 1024 * 1024);
|
|
252
|
+
if (!body)
|
|
253
|
+
return;
|
|
254
|
+
ctx.state.params = (0, misc_1.tryJson)(body.text) || {};
|
|
184
255
|
await next();
|
|
185
256
|
};
|
|
186
257
|
exports.paramsDecoder = paramsDecoder;
|
package/src/multipartUpload.js
CHANGED
|
@@ -11,9 +11,15 @@ const upload_1 = require("./upload");
|
|
|
11
11
|
const cross_const_1 = require("./cross-const");
|
|
12
12
|
const first_1 = require("./first");
|
|
13
13
|
const cross_1 = require("./cross");
|
|
14
|
+
const config_1 = require("./config");
|
|
14
15
|
async function handleMultipartUpload(ctx, node) {
|
|
15
16
|
if (ctx.request.type !== 'multipart/form-data')
|
|
16
17
|
return ctx.status = cross_const_1.HTTP_BAD_REQUEST;
|
|
18
|
+
if (isCrossOriginBrowserRequest()) {
|
|
19
|
+
ctx.set('Connection', 'close'); // the rejected multipart body is intentionally left unread
|
|
20
|
+
ctx.body = "Cross-origin multipart upload blocked";
|
|
21
|
+
return ctx.status = cross_const_1.HTTP_FOOL;
|
|
22
|
+
}
|
|
17
23
|
ctx.state.uploads = [];
|
|
18
24
|
const locks = [];
|
|
19
25
|
const fileJobs = [];
|
|
@@ -73,6 +79,20 @@ async function handleMultipartUpload(ctx, node) {
|
|
|
73
79
|
stream.resume();
|
|
74
80
|
return new Promise(res => (0, first_1.onFirstEvent)(stream, ['end', 'close', 'error'], res));
|
|
75
81
|
}
|
|
82
|
+
function isCrossOriginBrowserRequest() {
|
|
83
|
+
if (ctx.get('x-hfs-anti-csrf'))
|
|
84
|
+
return false;
|
|
85
|
+
const origin = ctx.get('origin');
|
|
86
|
+
if (origin && origin !== 'null' && allowedUploadOrigin.compiled()(origin))
|
|
87
|
+
return false;
|
|
88
|
+
const fetchSite = ctx.get('sec-fetch-site');
|
|
89
|
+
if (fetchSite)
|
|
90
|
+
return fetchSite !== 'same-origin' && fetchSite !== 'none';
|
|
91
|
+
if (!origin)
|
|
92
|
+
return false; // missing browser metadata preserves non-browser and legacy upload clients
|
|
93
|
+
// compare hosts because TLS termination can make the public and internal protocols differ
|
|
94
|
+
return (0, cross_1.try_)(() => new URL(origin).host.toLowerCase() !== ctx.host.toLowerCase(), () => true);
|
|
95
|
+
}
|
|
76
96
|
async function subfolderBlocksUpload(fn) {
|
|
77
97
|
const prefix = (0, path_1.dirname)(fn.replaceAll('\\', '/'));
|
|
78
98
|
if (prefix === '.') // no subdir
|
|
@@ -81,3 +101,4 @@ async function handleMultipartUpload(ctx, node) {
|
|
|
81
101
|
return subfolderNode && !(0, vfs_1.hasPermission)(subfolderNode, 'can_upload', ctx);
|
|
82
102
|
}
|
|
83
103
|
}
|
|
104
|
+
const allowedUploadOrigin = (0, config_1.defineConfig)('allowed_upload_origin', '', mask => (0, cross_1.makeMatcher)(mask, false, false));
|
package/src/perm.js
CHANGED
|
@@ -31,17 +31,7 @@ const events_1 = __importDefault(require("./events"));
|
|
|
31
31
|
const auth_1 = require("./auth");
|
|
32
32
|
// provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
|
|
33
33
|
function expandUsername(who) {
|
|
34
|
-
|
|
35
|
-
const q = [who];
|
|
36
|
-
for (const u of q) {
|
|
37
|
-
const a = getAccount(u);
|
|
38
|
-
if (!a || a.disabled)
|
|
39
|
-
continue;
|
|
40
|
-
ret.add(u);
|
|
41
|
-
if (a.belongs)
|
|
42
|
-
q.push(...a.belongs);
|
|
43
|
-
}
|
|
44
|
-
return ret;
|
|
34
|
+
return new Set(Array.from(walkActiveAccounts([who]), a => a.username));
|
|
45
35
|
}
|
|
46
36
|
// check if current username or any ancestor match the provided usernames
|
|
47
37
|
function ctxBelongsTo(ctx, usernames) {
|
|
@@ -213,14 +203,29 @@ function delAccount(username) {
|
|
|
213
203
|
}
|
|
214
204
|
// get some property from account, searching in its groups if necessary. Search is breadth-first, and this determines priority of inheritance.
|
|
215
205
|
function getFromAccount(account, getter) {
|
|
216
|
-
const
|
|
206
|
+
const root = typeof account === 'string' ? getAccount(account) : account;
|
|
207
|
+
if (!root)
|
|
208
|
+
return;
|
|
209
|
+
let res = getter(root); // always consider direct properties; only inherited groups are filtered by active status
|
|
210
|
+
if (res !== undefined)
|
|
211
|
+
return res;
|
|
212
|
+
for (const a of walkActiveAccounts(root.belongs || [])) {
|
|
213
|
+
res = getter(a);
|
|
214
|
+
if (res !== undefined)
|
|
215
|
+
return res;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function* walkActiveAccounts(roots) {
|
|
219
|
+
const search = [...roots];
|
|
220
|
+
const visited = new Set();
|
|
217
221
|
for (const accountOrUsername of search) {
|
|
218
222
|
const a = typeof accountOrUsername === 'string' ? getAccount(accountOrUsername) : accountOrUsername;
|
|
219
|
-
if (!a)
|
|
223
|
+
if (!a || visited.has(a.username))
|
|
220
224
|
continue;
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
|
|
225
|
+
visited.add(a.username);
|
|
226
|
+
if (accountIsDisabled(a))
|
|
227
|
+
continue;
|
|
228
|
+
yield a;
|
|
224
229
|
if (a.belongs)
|
|
225
230
|
search.push(...a.belongs);
|
|
226
231
|
}
|
|
@@ -235,10 +240,23 @@ function accountCanLogin(account) {
|
|
|
235
240
|
return accountHasLoginMethod(account) && !accountIsDisabled(account);
|
|
236
241
|
}
|
|
237
242
|
function accountIsDisabled(account) {
|
|
238
|
-
return
|
|
239
|
-
|
|
240
|
-
|| account.
|
|
241
|
-
|
|
243
|
+
return check(account, new Set());
|
|
244
|
+
function check(account, visiting) {
|
|
245
|
+
if (account.disabled || account.expire < Date.now())
|
|
246
|
+
return true;
|
|
247
|
+
if (!account.belongs?.length)
|
|
248
|
+
return false;
|
|
249
|
+
// consider invalid membership cycles disabled so they cannot grant privileges
|
|
250
|
+
if (visiting.has(account.username))
|
|
251
|
+
return true;
|
|
252
|
+
visiting.add(account.username);
|
|
253
|
+
const ret = account.belongs.every(u => {
|
|
254
|
+
const a = getAccount(u, false);
|
|
255
|
+
return Boolean(a && check(a, visiting));
|
|
256
|
+
});
|
|
257
|
+
visiting.delete(account.username);
|
|
258
|
+
return ret;
|
|
259
|
+
}
|
|
242
260
|
}
|
|
243
261
|
function accountCanLoginAdmin(account) {
|
|
244
262
|
return accountCanLogin(account) && getFromAccount(account, a => a.admin) || false;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getPluginCatalog = getPluginCatalog;
|
|
7
|
+
exports.searchPluginCatalog = searchPluginCatalog;
|
|
8
|
+
const github_1 = require("./github");
|
|
9
|
+
const persistence_1 = require("./persistence");
|
|
10
|
+
const const_1 = require("./const");
|
|
11
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
12
|
+
const CACHE_KEY = 'pluginCatalog';
|
|
13
|
+
let refreshing;
|
|
14
|
+
async function* getPluginCatalog() {
|
|
15
|
+
await persistence_1.storedMap.ready();
|
|
16
|
+
const saved = await persistence_1.storedMap.get(CACHE_KEY);
|
|
17
|
+
// compatibility is determined by this HFS version, including fallback api branches
|
|
18
|
+
const previous = saved?.version === const_1.VERSION ? saved : undefined;
|
|
19
|
+
const age = previous ? Date.now() - previous.updatedAt : Infinity;
|
|
20
|
+
if (previous && age < 24 * 60 * 60_000)
|
|
21
|
+
yield { ...previous, refreshing: age >= 5 * 60_000 };
|
|
22
|
+
if (age < 5 * 60_000)
|
|
23
|
+
return;
|
|
24
|
+
// all searches share the same refresh, even if the initiating client disconnects
|
|
25
|
+
refreshing ??= refresh().finally(() => { refreshing = undefined; });
|
|
26
|
+
yield { ...await refreshing, refreshing: false };
|
|
27
|
+
async function refresh() {
|
|
28
|
+
const repositories = [];
|
|
29
|
+
for await (const repo of (0, github_1.apiGithubPaginated)('search/repositories?q=topic:hfs-plugin', { requireComplete: true }))
|
|
30
|
+
repositories.push(lodash_1.default.pick(repo, ['full_name', 'description', 'pushed_at', 'default_branch', 'stargazers_count', 'license']));
|
|
31
|
+
const old = new Map(previous?.entries.map(entry => [entry.repository.full_name, entry]));
|
|
32
|
+
const entries = await Promise.all(repositories.map(async (repository) => {
|
|
33
|
+
const cached = old.get(repository.full_name);
|
|
34
|
+
let plugin = cached?.plugin;
|
|
35
|
+
if (!cached || cached.repository.pushed_at !== repository.pushed_at
|
|
36
|
+
|| cached.repository.default_branch !== repository.default_branch) {
|
|
37
|
+
plugin = await (0, github_1.readOnlineCompatiblePlugin)(repository.full_name, repository.default_branch)
|
|
38
|
+
.catch((error) => {
|
|
39
|
+
// repositories can carry the topic without providing a plugin at the expected path
|
|
40
|
+
if (error.message !== '404')
|
|
41
|
+
throw error;
|
|
42
|
+
}) ?? null;
|
|
43
|
+
}
|
|
44
|
+
return { repository, plugin: plugin ?? null };
|
|
45
|
+
}));
|
|
46
|
+
const next = { version: const_1.VERSION, updatedAt: Date.now(), entries };
|
|
47
|
+
// never replace a complete catalog with the result of a failed refresh
|
|
48
|
+
await persistence_1.storedMap.put(CACHE_KEY, next);
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function searchPluginCatalog(catalog, text = '', skipRepos = []) {
|
|
53
|
+
const central = await (0, github_1.getProjectInfo)();
|
|
54
|
+
const terms = text.toLowerCase().split(/\s+/).filter(Boolean);
|
|
55
|
+
return catalog.entries.flatMap(({ repository, plugin }) => {
|
|
56
|
+
const repo = repository.full_name;
|
|
57
|
+
const searchable = `${repo} ${repository.description || ''} ${plugin?.description || ''}`.toLowerCase();
|
|
58
|
+
if (!plugin || skipRepos.includes(repo) || central?.repo_blacklist?.[repo]?.message
|
|
59
|
+
|| !terms.every(term => searchable.includes(term)))
|
|
60
|
+
return [];
|
|
61
|
+
// API responses add live state and dependency errors; keep the persisted metadata untouched
|
|
62
|
+
return [{ ...plugin, id: repo, repo, pushed_at: repository.pushed_at,
|
|
63
|
+
stargazers_count: repository.stargazers_count, default_branch: repository.default_branch,
|
|
64
|
+
license: repository.license?.spdx_id }];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_crypto_1 = require("node:crypto");
|
|
4
|
+
const misc_1 = require("./misc");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const plugins_1 = require("./plugins");
|
|
7
|
+
const persistence_1 = require("./persistence");
|
|
8
|
+
const util_http_1 = require("./util-http");
|
|
9
|
+
const debounceAsync_1 = require("./debounceAsync");
|
|
10
|
+
const shareUsageStats = (0, config_1.defineConfig)(misc_1.CFG.share_usage_stats, false);
|
|
11
|
+
const installationId = persistence_1.storedMap.singleSync('pluginUsageId', '');
|
|
12
|
+
const lastReport = persistence_1.storedMap.singleSync('lastPluginUsageReport', 0);
|
|
13
|
+
const ready = Promise.all([config_1.configReady, plugins_1.pluginsScanned, installationId.ready(), lastReport.ready()]);
|
|
14
|
+
const syncPluginUsage = (0, debounceAsync_1.debounceAsync)(async () => {
|
|
15
|
+
await ready;
|
|
16
|
+
let uuid = installationId.get();
|
|
17
|
+
if (!shareUsageStats.get()) {
|
|
18
|
+
if (!uuid)
|
|
19
|
+
return;
|
|
20
|
+
await send('DELETE', { uuid });
|
|
21
|
+
installationId.set('');
|
|
22
|
+
lastReport.set(0);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (Date.now() < lastReport.get() + misc_1.DAY)
|
|
26
|
+
return;
|
|
27
|
+
if (!uuid) {
|
|
28
|
+
uuid = (0, node_crypto_1.randomUUID)();
|
|
29
|
+
installationId.set(uuid);
|
|
30
|
+
}
|
|
31
|
+
const enabled = plugins_1.enablePlugins.get();
|
|
32
|
+
const plugins = selectReportedPlugins([
|
|
33
|
+
...(0, plugins_1.mapPlugins)((plugin, id) => ({ ...plugin.getData(), id, enabled: enabled.includes(id) }), false),
|
|
34
|
+
...(0, plugins_1.getInactivePlugins)().map(plugin => ({ ...plugin, enabled: enabled.includes(plugin.id) })),
|
|
35
|
+
]);
|
|
36
|
+
await send('POST', { uuid, hfsVersion: String(config_1.currentVersion), plugins });
|
|
37
|
+
lastReport.set(Date.now());
|
|
38
|
+
function selectReportedPlugins(installed) {
|
|
39
|
+
const selected = new Map();
|
|
40
|
+
for (const plugin of installed) {
|
|
41
|
+
const repo = typeof plugin.repo === 'string' ? plugin.repo : plugin.repo?.main;
|
|
42
|
+
if (!repo)
|
|
43
|
+
continue;
|
|
44
|
+
const current = selected.get(repo);
|
|
45
|
+
if (!current || better(plugin, current))
|
|
46
|
+
selected.set(repo, plugin);
|
|
47
|
+
}
|
|
48
|
+
return Array.from(selected, ([repo, plugin]) => ({
|
|
49
|
+
repo,
|
|
50
|
+
version: plugin.version,
|
|
51
|
+
enabled: plugin.enabled,
|
|
52
|
+
})).sort((a, b) => a.repo.localeCompare(b.repo));
|
|
53
|
+
function better(a, b) {
|
|
54
|
+
return Number(a.enabled) > Number(b.enabled)
|
|
55
|
+
|| a.enabled === b.enabled && (a.id.length < b.id.length
|
|
56
|
+
|| a.id.length === b.id.length && a.id.localeCompare(b.id) < 0);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
shareUsageStats.sub(() => void syncPluginUsage().catch(() => { }));
|
|
61
|
+
(0, misc_1.repeat)(misc_1.HOUR, syncPluginUsage);
|
|
62
|
+
async function send(method, body) {
|
|
63
|
+
await (0, util_http_1.httpWithBody)(misc_1.PLUGIN_USAGE_URL + '/report', { method, body, timeout: 5000 });
|
|
64
|
+
}
|
package/src/plugins.js
CHANGED
|
@@ -53,6 +53,7 @@ exports.getInactivePlugins = getInactivePlugins;
|
|
|
53
53
|
exports.rescan = rescan;
|
|
54
54
|
exports.parsePluginSource = parsePluginSource;
|
|
55
55
|
exports.getMissingDependencies = getMissingDependencies;
|
|
56
|
+
const acmeDns_1 = require("./acmeDns");
|
|
56
57
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
57
58
|
const watchLoad_1 = require("./watchLoad");
|
|
58
59
|
const lodash_1 = __importDefault(require("lodash"));
|
|
@@ -232,6 +233,13 @@ async function initPlugin(pl, morePassedToInit, onInitError) {
|
|
|
232
233
|
return result;
|
|
233
234
|
}
|
|
234
235
|
},
|
|
236
|
+
registerAcmeDnsProvider(...args) {
|
|
237
|
+
if (unloading)
|
|
238
|
+
throw Error("Plugin is unloading");
|
|
239
|
+
const unregister = (0, acmeDns_1.registerAcmeDnsProvider)(...args);
|
|
240
|
+
cleanups.push(unregister);
|
|
241
|
+
return unregister;
|
|
242
|
+
},
|
|
235
243
|
misc, _: lodash_1.default,
|
|
236
244
|
customApiCall, notifyClient: frontEndApis_1.notifyClient, addBlock: block_1.addBlock, ctxBelongsTo: perm_1.ctxBelongsTo, getConnections: connections_1.getConnections, normalizeFilename: vfs_1.normalizeFilename,
|
|
237
245
|
getCurrentUsername: auth_1.getCurrentUsername, getAccount: perm_1.getAccount, getUsernames: perm_1.getUsernames, addAccount: perm_1.addAccount, delAccount: perm_1.delAccount, updateAccount: perm_1.updateAccount, renameAccount: perm_1.renameAccount,
|
|
@@ -734,6 +742,10 @@ function parsePluginSource(id, source) {
|
|
|
734
742
|
pl.depend = (0, misc_1.tryJson)(/exports.depend\s*=\s*(\[[\s\S]*?])/m.exec(source)?.[1])?.filter((x) => typeof x.repo === 'string' && x.version === undefined || typeof x.version === 'number'
|
|
735
743
|
|| console.warn("Plugin dependency discarded", x));
|
|
736
744
|
pl.changelog = (0, misc_1.tryJson)(/exports.changelog\s*=\s*(\[[\s\S]*?])/m.exec(source)?.[1]);
|
|
745
|
+
if (!Array.isArray(pl.changelog))
|
|
746
|
+
delete pl.changelog;
|
|
747
|
+
else if (lodash_1.default.remove(pl.changelog, entry => !Number.isFinite(entry?.version) || typeof entry?.message !== 'string').length)
|
|
748
|
+
console.warn("Invalid plugin changelog entries discarded", id);
|
|
737
749
|
if (Array.isArray(pl.apiRequired) && (pl.apiRequired.length !== 2 || !pl.apiRequired.every(lodash_1.default.isFinite))) // validate [from,to] form
|
|
738
750
|
pl.apiRequired = undefined;
|
|
739
751
|
calculateBadApi(pl);
|
package/src/roots.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.rootsMiddleware = exports.roots = void 0;
|
|
6
|
+
exports.rootsMiddleware = exports.rootsHostGuard = exports.roots = void 0;
|
|
7
7
|
const config_1 = require("./config");
|
|
8
8
|
const misc_1 = require("./misc");
|
|
9
9
|
const connections_1 = require("./connections");
|
|
@@ -17,6 +17,26 @@ exports.roots = (0, config_1.defineConfig)(misc_1.CFG.roots, {}, map => {
|
|
|
17
17
|
return (host) => values[matchers.findIndex(m => m(host))];
|
|
18
18
|
});
|
|
19
19
|
const forceAddress = (0, config_1.defineConfig)(misc_1.CFG.force_address, false);
|
|
20
|
+
const rootsHostGuard = (ctx, next) => {
|
|
21
|
+
if (!ctx.path || lodash_1.default.isEmpty(exports.roots.get()))
|
|
22
|
+
return next();
|
|
23
|
+
if (ctx.path.startsWith(misc_1.SPECIAL_URI)) {
|
|
24
|
+
if (!ctx.path.startsWith(misc_1.API_URI))
|
|
25
|
+
return next();
|
|
26
|
+
const { referer } = ctx.headers;
|
|
27
|
+
const fromAdmin = referer && (0, misc_1.try_)(() => new URL(referer).pathname.startsWith(ctx.state.revProxyPath + misc_1.ADMIN_URI));
|
|
28
|
+
// authentication must remain reachable before ctxAdminAccess can succeed
|
|
29
|
+
if (fromAdmin && ((0, adminApis_1.ctxAdminAccess)(ctx)
|
|
30
|
+
|| ['login', 'loginSrp1', 'loginSrp2'].some(x => ctx.path === misc_1.API_URI + x)))
|
|
31
|
+
return next();
|
|
32
|
+
}
|
|
33
|
+
const root = ctx.state.root = exports.roots.compiled()(ctx.host);
|
|
34
|
+
return !ctx.state.skipFilters && forceAddress.get()
|
|
35
|
+
&& root === undefined && !(0, misc_1.isLocalHost)(ctx) && ctx.host !== listen_1.baseUrl.compiled()
|
|
36
|
+
? (0, connections_1.disconnect)(ctx, forceAddress.key())
|
|
37
|
+
: next();
|
|
38
|
+
};
|
|
39
|
+
exports.rootsHostGuard = rootsHostGuard;
|
|
20
40
|
const rootsMiddleware = (ctx, next) => (() => {
|
|
21
41
|
if (!ctx.path) // it was once reported "Cannot read properties of null (reading 'startsWith')" but I can't reproduce it, and it shouldn't happen anyway
|
|
22
42
|
return (0, connections_1.disconnect)(ctx, 'no path'); // let's assume that such requests can't be served anyway. This will leave a trace with an ip, anyway
|
|
@@ -27,19 +47,8 @@ const rootsMiddleware = (ctx, next) => (() => {
|
|
|
27
47
|
return; // ...unless it's an api
|
|
28
48
|
params = ctx.state.params || ctx.query; // for api we'll translate params
|
|
29
49
|
changeUriParams(v => (0, misc_1.removeStarting)(ctx.state.revProxyPath, v)); // this removal must be done before adding the root; this operation doesn't conceptually belong to "roots", and it may be placed in different middleware, but it's convenient to do it here
|
|
30
|
-
const { referer } = ctx.headers;
|
|
31
|
-
const fromAdmin = referer && (0, misc_1.try_)(() => new URL(referer).pathname.startsWith(ctx.state.revProxyPath + misc_1.ADMIN_URI));
|
|
32
|
-
// authentication must remain reachable before ctxAdminAccess can succeed
|
|
33
|
-
if (fromAdmin && ((0, adminApis_1.ctxAdminAccess)(ctx)
|
|
34
|
-
|| ['login', 'loginSrp1', 'loginSrp2'].some(x => ctx.path === misc_1.API_URI + x)))
|
|
35
|
-
return;
|
|
36
50
|
}
|
|
37
|
-
|
|
38
|
-
return;
|
|
39
|
-
const root = ctx.state.root = exports.roots.compiled()(ctx.host);
|
|
40
|
-
if (!ctx.state.skipFilters && forceAddress.get()
|
|
41
|
-
&& root === undefined && !(0, misc_1.isLocalHost)(ctx) && ctx.host !== listen_1.baseUrl.compiled())
|
|
42
|
-
return (0, connections_1.disconnect)(ctx, forceAddress.key()); // returning truthy will not call next
|
|
51
|
+
const { root } = ctx.state;
|
|
43
52
|
if (!root || root === '/')
|
|
44
53
|
return; // no transformation is required
|
|
45
54
|
changeUriParams(v => (0, misc_1.join)(root, v));
|
package/src/serveFile.js
CHANGED
|
@@ -8,6 +8,9 @@ exports.forceDownload = forceDownload;
|
|
|
8
8
|
exports.disposition = disposition;
|
|
9
9
|
exports.serveFileNode = serveFileNode;
|
|
10
10
|
exports.enforceDownloadLimits = enforceDownloadLimits;
|
|
11
|
+
exports.resolveFileMime = resolveFileMime;
|
|
12
|
+
exports.getNodeMime = getNodeMime;
|
|
13
|
+
exports.isActiveContentMime = isActiveContentMime;
|
|
11
14
|
exports.serveFile = serveFile;
|
|
12
15
|
exports.monitorAsDownload = monitorAsDownload;
|
|
13
16
|
exports.applyRange = applyRange;
|
|
@@ -26,6 +29,7 @@ const errorPages_1 = require("./errorPages");
|
|
|
26
29
|
const stream_1 = require("stream");
|
|
27
30
|
const crypto_1 = require("crypto");
|
|
28
31
|
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
|
32
|
+
const upload_1 = require("./upload");
|
|
29
33
|
const allowedReferer = (0, config_1.defineConfig)(misc_1.CFG.allowed_referer, '');
|
|
30
34
|
const maxDownloads = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads, 0), () => true);
|
|
31
35
|
const maxDownloadsPerIp = downloadLimiter((0, config_1.defineConfig)(misc_1.CFG.max_downloads_per_ip, 0), ctx => ctx.ip);
|
|
@@ -49,6 +53,7 @@ async function serveFileNode(ctx, node) {
|
|
|
49
53
|
const name = (0, vfs_1.getNodeName)(node);
|
|
50
54
|
const mimeString = typeof mime === 'string' ? mime
|
|
51
55
|
: lodash_1.default.find(mime, (_val, mask) => (0, misc_1.matches)(name, mask));
|
|
56
|
+
const effectiveMime = resolveFileMime(source || '', mimeString);
|
|
52
57
|
if (allowedReferer.get()) {
|
|
53
58
|
const ref = (0, misc_1.try_)(() => new URL(ctx.get('referer') || '').host);
|
|
54
59
|
if (ref && ref !== ctx.host // automatically accept if the referer is basically the hosting domain
|
|
@@ -58,12 +63,17 @@ async function serveFileNode(ctx, node) {
|
|
|
58
63
|
ctx.state.vfsNode = node; // useful to tell service files from files shared by the user
|
|
59
64
|
const download = 'dl' in ctx.query;
|
|
60
65
|
disposition(ctx, name, download);
|
|
66
|
+
if (!download && source && isActiveContentMime(effectiveMime)) {
|
|
67
|
+
const upload = await (0, upload_1.getUploadMeta)(source);
|
|
68
|
+
if ((0, upload_1.isUploading)(source) || upload && upload.approved !== true)
|
|
69
|
+
return (0, errorPages_1.sendErrorPage)(ctx, const_1.HTTP_FORBIDDEN);
|
|
70
|
+
}
|
|
61
71
|
const fetchDest = ctx.get('sec-fetch-dest');
|
|
62
72
|
ctx.state.considerAsGui ??= !download && ctx.get('referer')?.endsWith('/')
|
|
63
73
|
&& (fetchDest ? fetchDest !== 'document' && fetchDest !== 'empty' // modern clients
|
|
64
74
|
// legacy clients often send Accept: */* for archive downloads, so the served mime is a safer signal than request headers here
|
|
65
|
-
: GUI_ASSET_MIME.test(
|
|
66
|
-
await serveFile(ctx, source || '',
|
|
75
|
+
: GUI_ASSET_MIME.test(effectiveMime));
|
|
76
|
+
await serveFile(ctx, source || '', effectiveMime);
|
|
67
77
|
await enforceDownloadLimits(ctx);
|
|
68
78
|
}
|
|
69
79
|
async function enforceDownloadLimits(ctx) {
|
|
@@ -75,14 +85,25 @@ const mimeCfg = (0, config_1.defineConfig)(misc_1.CFG.mime, {}, obj => {
|
|
|
75
85
|
const values = Object.values(obj);
|
|
76
86
|
return (name) => values[matchers.findIndex(matcher => matcher(name))];
|
|
77
87
|
});
|
|
88
|
+
function resolveFileMime(filePath, mime) {
|
|
89
|
+
mime ??= mimeCfg.compiled()((0, path_1.basename)(filePath));
|
|
90
|
+
return mime === undefined || mime === const_1.MIME_AUTO ? mime_types_1.default.lookup(filePath) || '' : mime;
|
|
91
|
+
}
|
|
92
|
+
function getNodeMime(node) {
|
|
93
|
+
const name = (0, vfs_1.getNodeName)(node);
|
|
94
|
+
const mime = typeof node.mime === 'string' ? node.mime
|
|
95
|
+
: lodash_1.default.find(node.mime, (_val, mask) => (0, misc_1.matches)(name, mask));
|
|
96
|
+
return resolveFileMime(node.source || '', mime);
|
|
97
|
+
}
|
|
98
|
+
function isActiveContentMime(mime) {
|
|
99
|
+
return /^(?:text\/html|application\/xhtml\+xml|image\/svg\+xml)(?:;|$)/i.test(mime);
|
|
100
|
+
}
|
|
78
101
|
// after this number of seconds, the browser should check the server to see if there's a newer version of the file
|
|
79
102
|
const cacheControlDiskFiles = (0, config_1.defineConfig)(misc_1.CFG.cache_control_disk_files, 5);
|
|
80
103
|
async function serveFile(ctx, filePath, mime, cached) {
|
|
81
104
|
if (!filePath)
|
|
82
105
|
return;
|
|
83
|
-
mime
|
|
84
|
-
if (mime === undefined || mime === const_1.MIME_AUTO)
|
|
85
|
-
mime = mime_types_1.default.lookup(filePath) || '';
|
|
106
|
+
mime = resolveFileMime(filePath, mime);
|
|
86
107
|
if (mime)
|
|
87
108
|
ctx.type = mime;
|
|
88
109
|
if (ctx.method === 'OPTIONS') {
|
package/src/serveGuiFiles.js
CHANGED
|
@@ -19,6 +19,7 @@ const customHtml_1 = require("./customHtml");
|
|
|
19
19
|
const lodash_1 = __importDefault(require("lodash"));
|
|
20
20
|
const config_1 = require("./config");
|
|
21
21
|
const lang_1 = require("./lang");
|
|
22
|
+
const adminLang_1 = require("./adminLang");
|
|
22
23
|
const upload_1 = require("./upload");
|
|
23
24
|
const icons_1 = require("./icons");
|
|
24
25
|
const middlewares_1 = require("./middlewares");
|
|
@@ -107,7 +108,8 @@ async function treatIndex(ctx, filesUri, body) {
|
|
|
107
108
|
return !lodash_1.default.isEmpty(configs) && [name, configs];
|
|
108
109
|
})));
|
|
109
110
|
const timestamp = await getFaviconTimestamp();
|
|
110
|
-
const
|
|
111
|
+
const adminLangs = isFrontend ? [] : await (0, adminLang_1.getAdminLangs)();
|
|
112
|
+
const lang = isFrontend ? await (0, lang_1.getLangData)(ctx) : await (0, adminLang_1.getAdminLangData)(ctx, adminLangs);
|
|
111
113
|
let group = 0; // this represents the loading-group: all plugins with the same group can be loaded concurrently
|
|
112
114
|
const loadScripts = (0, misc_1.onlyTruthy)((0, plugins_1.mapPlugins)((p, id) => {
|
|
113
115
|
const js = p.frontend_js?.map(f => f.includes('//') ? f : pub + id + '/' + f);
|
|
@@ -148,7 +150,8 @@ async function treatIndex(ctx, filesUri, body) {
|
|
|
148
150
|
customHtml: lodash_1.default.omit((0, customHtml_1.getAllSections)(), ['top', 'bottom', 'htmlHead', 'style']), // exclude the sections we already apply in this phase
|
|
149
151
|
...(0, misc_1.newObj)(misc_1.FRONTEND_OPTIONS, (v, k) => (0, config_1.getConfig)(k)),
|
|
150
152
|
icons: Object.assign({}, ...(0, plugins_1.mapPlugins)(p => iconsToObj(p.icons, p.id + '/')), iconsToObj(icons_1.customizedIcons)), // name-to-uri
|
|
151
|
-
lang
|
|
153
|
+
lang,
|
|
154
|
+
...(isFrontend ? {} : { adminLangs, adminLang: adminLang_1.adminLang.get() })
|
|
152
155
|
}, null, 4).replace(/<(\/script)/g, '<"+"$1') /*avoid breaking our script container*/}
|
|
153
156
|
document.documentElement.setAttribute('ver', HFS.VERSION.split('-')[0])
|
|
154
157
|
document.documentElement.setAttribute('browser', ${JSON.stringify((0, misc_1.shortenAgent)(ctx.get('user-agent')))})
|