hfs 3.3.2 → 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-zQdkj0Z9.js → dist-legacy-CKczq_fx.js} +1 -1
- package/frontend/assets/index-legacy-ZYae30AY.js +1 -0
- package/frontend/assets/{lib-legacy-CRrFEEqm.js → lib-legacy-D2tyILlj.js} +1 -1
- package/frontend/assets/{sha512-legacy-BB7Mdt0y.js → sha512-legacy-CFcDeiiP.js} +1 -1
- package/frontend/index.html +1 -1
- package/npm-shrinkwrap.json +60 -60
- package/package.json +3 -3
- 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 +22 -4
- package/src/api.plugins.js +53 -19
- package/src/api.vfs.js +2 -1
- 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 +16 -3
- 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/nat.js +10 -7
- package/src/outboundProxy.js +1 -1
- 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-Cvi9DSc1.js +0 -3
- package/admin/assets/ArrayField-ChS9pNra.js +0 -2
- package/admin/assets/ConfigFilePage-6jCYVkbr.js +0 -4
- package/admin/assets/CustomHtmlPage-DaEXsEcQ.js +0 -2
- package/admin/assets/DataTable-Cf5S5yWr.js +0 -119
- package/admin/assets/DateTimeField--mTsSB1l.js +0 -1
- package/admin/assets/EventManager-BJWba-fL.js +0 -1
- package/admin/assets/FileField-B0WhrAuk.js +0 -117
- package/admin/assets/InternetPage-BICQDG_K.js +0 -8
- package/admin/assets/LangPage-D42UPSxC.js +0 -2
- package/admin/assets/LogsPage-DQTrf5GE.js +0 -4
- package/admin/assets/MenuButton-DDfB3119.js +0 -1
- package/admin/assets/MonitorPage-CH06c8aW.js +0 -2
- package/admin/assets/OptionsPage-BvXLA_R0.js +0 -3
- package/admin/assets/PluginsPage-CyaVJZkM.js +0 -1
- package/admin/assets/Tabs-CGfV3BK0.js +0 -1
- package/admin/assets/TextEditor-BeS-mR2I.css +0 -1
- package/admin/assets/TextEditor-Df9LSzm6.js +0 -34
- package/admin/assets/VfsPathField-6Kyy5vuQ.js +0 -1
- package/admin/assets/agentIcons-DwT9U-lh.js +0 -1
- package/admin/assets/baseUrl-D7S0KsqZ.js +0 -2
- package/admin/assets/icons-DWkdo8hc.js +0 -1
- package/admin/assets/index-11n5z0Ez.css +0 -1
- package/admin/assets/index-BDdPG215.js +0 -477
- package/admin/assets/routing-CxxvyWME.js +0 -1
- package/frontend/assets/index-legacy-B8EvbIOB.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/nat.js
CHANGED
|
@@ -82,24 +82,25 @@ exports.getPublicIps = (0, debounceAsync_1.debounceAsync)(async () => {
|
|
|
82
82
|
return ret;
|
|
83
83
|
}, { retain: 10 * cross_1.MINUTE });
|
|
84
84
|
exports.getNatInfo = (0, debounceAsync_1.debounceAsync)(async () => {
|
|
85
|
+
const MAPPINGS_TIMEOUT = 15_000; // routers enumerate rules sequentially; a few dozen mappings can take over 5 seconds
|
|
85
86
|
const upnp = await exports.upnpEnabled.getWhenReady() ? getUpnpClient() : null;
|
|
86
87
|
const gatewayIpPromise = findGateway().catch(() => undefined);
|
|
87
88
|
const gw = upnp && await (0, cross_1.haveTimeout)(10_000, upnp.getGateway()).catch(() => null);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.debug(gw ? "Mappings found:" : "Mappings not queried:", mappings?.map(x => x.description).join(', ') || (gw ? "none" : upnp ? "gateway not found" : "UPnP disabled"));
|
|
89
|
+
let mappings = gw && await (0, cross_1.haveTimeout)(MAPPINGS_TIMEOUT, upnp.getMappings())?.catch(() => null);
|
|
90
|
+
console.debug(gw ? "Mappings found:" : "Mappings not queried:", lodash_1.default.uniq(mappings?.map(x => x.description)).join(', ') || (gw ? "none" : upnp ? "gateway not found" : "UPnP disabled"));
|
|
91
91
|
const localIps = await (0, listen_1.getIps)(false);
|
|
92
92
|
const gatewayIp = await gatewayIpPromise;
|
|
93
93
|
const localIp = gw?.address || (gatewayIp ? lodash_1.default.maxBy(localIps, x => (0, cross_1.inCommon)(x, gatewayIp)) : localIps[0]);
|
|
94
|
+
const status = await (0, listen_1.getServerStatus)();
|
|
94
95
|
const internalPort = status?.https?.listening && status.https.port || status?.http?.listening && status.http.port || undefined;
|
|
95
96
|
let mapped = lodash_1.default.find(mappings, x => x.private.host === localIp && x.private.port === internalPort);
|
|
96
97
|
if (upnp && mappings && localIp && internalPort && !mapped && exports.mappedPort.get())
|
|
97
98
|
// restore the HFS-created mapping after routers that forget UPnP state across reboots
|
|
98
99
|
await (0, cross_1.haveTimeout)(5_000, upnp.createMapping(upnpMappingParam(internalPort, exports.mappedPort.get()))).then(async () => {
|
|
99
100
|
// confirm router state after restore instead of trusting the AddPortMapping result
|
|
100
|
-
mappings = await (0, cross_1.haveTimeout)(
|
|
101
|
+
mappings = await (0, cross_1.haveTimeout)(MAPPINGS_TIMEOUT, upnp.getMappings());
|
|
101
102
|
mapped = lodash_1.default.find(mappings, x => x.private.host === localIp && x.private.port === internalPort);
|
|
102
|
-
}).catch(e => console.warn('UPnP mapping restore failed:', e?.message || String(e)));
|
|
103
|
+
}).catch(e => console.warn('UPnP mapping restore failed:', e?.message || e?.errorDescription || String(e)));
|
|
103
104
|
const externalPort = mapped?.public.port;
|
|
104
105
|
if (localIp)
|
|
105
106
|
exports.defaultBaseUrl.localIp = localIp;
|
|
@@ -134,9 +135,11 @@ function getUpnpClient() {
|
|
|
134
135
|
throw Error("UPnP disabled");
|
|
135
136
|
if (!upnpClient) {
|
|
136
137
|
upnpClient = new nat_upnp_1.Client({ timeout: 4_000 });
|
|
137
|
-
const originalMethod = upnpClient.getGateway;
|
|
138
138
|
// other client methods call getGateway too, so this will ensure they reuse this same result
|
|
139
|
-
upnpClient
|
|
139
|
+
(0, cross_1.patchKey)(upnpClient, 'getGateway', was => (0, debounceAsync_1.debounceAsync)(() => was.apply(upnpClient), { retain: cross_1.HOUR, retainFailure: 30_000 }));
|
|
140
|
+
const f = (0, cross_1.patchKey)(upnpClient, 'getMappings', was => (0, debounceAsync_1.debounceAsync)(() => was.apply(upnpClient), { retain: cross_1.MINUTE }));
|
|
141
|
+
for (const k of ['createMapping', 'removeMapping']) // these methods must invalidate the cache
|
|
142
|
+
(0, cross_1.patchKey)(upnpClient, k, (was) => (...args) => was.apply(upnpClient, args).finally(f.clearRetain));
|
|
140
143
|
}
|
|
141
144
|
return upnpClient;
|
|
142
145
|
}
|
package/src/outboundProxy.js
CHANGED
|
@@ -44,7 +44,7 @@ config_1.configReady.then(async (startedWithoutConfig) => {
|
|
|
44
44
|
// it can be like "IP:PORT" or "http=IP:PORT;https=IP:PORT;ftp=IP:PORT"
|
|
45
45
|
const url = (0, cross_1.prefix)('https://', /https=([\d:.]+)/.exec(out)?.[1]) // prefer https
|
|
46
46
|
|| (0, cross_1.prefix)('http://', /http=([\d:.]+)/.exec(out)?.[1])
|
|
47
|
-
|| !read.includes('=') && 'http://' + read; // simpler form
|
|
47
|
+
|| !read.includes('=') && (read.includes('://') ? read : 'http://' + read); // simpler form
|
|
48
48
|
if (!url)
|
|
49
49
|
return;
|
|
50
50
|
outboundProxy.set(url);
|
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));
|