hfs 0.57.26 → 0.57.27-beta2
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/{index-CHMhmNYS.js → index-DSkhXlWX.js} +103 -103
- package/admin/assets/{sha512-Dsv49IAs.js → sha512-CoddZKeD.js} +1 -1
- package/admin/index.html +1 -1
- package/frontend/assets/{index-legacy-BI8icwFp.js → index-legacy-C4Gclsbz.js} +4 -4
- package/frontend/assets/{sha512-legacy-D82alHBq.js → sha512-legacy-C-FMk44u.js} +1 -1
- package/frontend/index.html +1 -1
- package/package.json +4 -3
- package/src/QuickZipStream.js +11 -8
- package/src/SendList.js +10 -8
- package/src/ThrottledStream.js +8 -6
- package/src/acme.js +5 -7
- package/src/adminApis.js +6 -7
- package/src/api.accounts.js +4 -6
- package/src/api.auth.js +7 -11
- package/src/api.get_file_list.js +11 -16
- package/src/api.lang.js +1 -1
- package/src/api.monitor.js +2 -3
- package/src/api.net.js +1 -2
- package/src/api.plugins.js +8 -10
- package/src/api.vfs.js +14 -19
- package/src/apiMiddleware.js +4 -4
- package/src/auth.js +6 -9
- package/src/basicWeb.js +1 -2
- package/src/block.js +1 -1
- package/src/commands.js +9 -9
- package/src/comments.js +4 -5
- package/src/config.js +8 -9
- package/src/connections.js +11 -6
- package/src/consoleLog.js +6 -6
- package/src/const.js +4 -4
- package/src/cross.js +19 -21
- package/src/customHtml.js +3 -4
- package/src/ddns.js +4 -4
- package/src/debounceAsync.js +3 -3
- package/src/errorPages.js +2 -3
- package/src/events.js +7 -10
- package/src/fileAttr.js +6 -6
- package/src/first.js +13 -8
- package/src/frontEndApis.js +5 -5
- package/src/geo.js +3 -5
- package/src/github.js +14 -17
- package/src/i18n.js +3 -4
- package/src/icons.js +1 -1
- package/src/index.js +2 -3
- package/src/lang.js +5 -6
- package/src/listen.js +25 -28
- package/src/log.js +18 -17
- package/src/middlewares.js +12 -15
- package/src/misc.js +9 -11
- package/src/nat.js +14 -16
- package/src/outboundProxy.js +7 -7
- package/src/perm.js +7 -12
- package/src/persistence.js +1 -1
- package/src/plugins.js +62 -73
- package/src/roots.js +1 -2
- package/src/selfCheck.js +5 -4
- package/src/serveFile.js +6 -7
- package/src/serveGuiAndSharedFiles.js +7 -9
- package/src/serveGuiFiles.js +11 -20
- package/src/stat.js +40 -0
- package/src/statWorker.js +11 -0
- package/src/throttler.js +5 -7
- package/src/update.js +7 -7
- package/src/upload.js +11 -13
- package/src/util-files.js +42 -32
- package/src/util-http.js +80 -29
- package/src/util-os.js +2 -4
- package/src/vfs.js +77 -68
- package/src/walkDir.js +9 -9
- package/src/watchLoad.js +4 -4
- package/src/zip.js +5 -6
package/src/listen.js
CHANGED
|
@@ -70,17 +70,19 @@ const consoleLog_1 = require("./consoleLog");
|
|
|
70
70
|
let httpSrv;
|
|
71
71
|
let httpsSrv;
|
|
72
72
|
const openBrowserAtStart = (0, config_1.defineConfig)('open_browser_at_start', true);
|
|
73
|
-
exports.baseUrl = (0, config_1.defineConfig)(misc_1.CFG.base_url, '', x =>
|
|
73
|
+
exports.baseUrl = (0, config_1.defineConfig)(misc_1.CFG.base_url, '', x => /(?<=\/\/)[^\/]+/.exec(x)?.[0]); // compiled is host only
|
|
74
74
|
async function getBaseUrlOrDefault() {
|
|
75
75
|
return exports.baseUrl.get() || await nat_1.defaultBaseUrl.get();
|
|
76
76
|
}
|
|
77
77
|
function getHttpsWorkingPort() {
|
|
78
|
-
|
|
79
|
-
return (httpsSrv === null || httpsSrv === void 0 ? void 0 : httpsSrv.listening) && ((_a = httpsSrv.address()) === null || _a === void 0 ? void 0 : _a.port);
|
|
78
|
+
return httpsSrv?.listening && httpsSrv.address()?.port;
|
|
80
79
|
}
|
|
81
|
-
const commonServerOptions = {
|
|
80
|
+
const commonServerOptions = {
|
|
81
|
+
requestTimeout: 0, // disable timeout on long uploads
|
|
82
|
+
maxHeaderSize: 32768, // allow a larger header for a larger list in ?get=zip
|
|
83
|
+
};
|
|
82
84
|
// these are properties that can be assigned to the server object
|
|
83
|
-
const commonServerAssign = { headersTimeout:
|
|
85
|
+
const commonServerAssign = { headersTimeout: 30_000, timeout: misc_1.MINUTE }; // 'headersTimeout' is not recognized by type lib, and 'timeout' is not effective when passed in parameters
|
|
84
86
|
const readyToListen = Promise.all([persistence_1.storedMap.isOpening(), events_1.default.once('app')]);
|
|
85
87
|
const considerHttp = (0, misc_1.debounceAsync)(async () => {
|
|
86
88
|
await readyToListen;
|
|
@@ -105,7 +107,7 @@ const listenInterface = (0, config_1.defineConfig)('listen_interface', '');
|
|
|
105
107
|
(0, config_1.subMultipleConfigs)(considerHttp, [exports.portCfg, listenInterface]);
|
|
106
108
|
function openAdmin() {
|
|
107
109
|
for (const srv of [httpSrv, httpsSrv]) {
|
|
108
|
-
const a = srv
|
|
110
|
+
const a = srv?.address();
|
|
109
111
|
if (!a || typeof a === 'string')
|
|
110
112
|
continue;
|
|
111
113
|
const i = listenInterface.get();
|
|
@@ -123,21 +125,19 @@ function openAdmin() {
|
|
|
123
125
|
console.log("openAdmin failed");
|
|
124
126
|
}
|
|
125
127
|
function getCertObject() {
|
|
126
|
-
var _a;
|
|
127
128
|
const c = exports.cert.compiled();
|
|
128
129
|
if (!c)
|
|
129
130
|
return;
|
|
130
131
|
const all = new crypto_1.X509Certificate(c);
|
|
131
132
|
const some = lodash_1.default.pick(all, ['subject', 'issuer', 'validFrom', 'validTo']);
|
|
132
|
-
const ret = (0, misc_1.objSameKeys)(some, v =>
|
|
133
|
-
return Object.assign(ret, { altNames:
|
|
133
|
+
const ret = (0, misc_1.objSameKeys)(some, v => v?.includes('=') ? Object.fromEntries(v.split('\n').map(x => x.split('='))) : v);
|
|
134
|
+
return Object.assign(ret, { altNames: all.subjectAltName?.replace(/DNS:/g, '').split(/, */) });
|
|
134
135
|
}
|
|
135
136
|
const considerHttps = (0, misc_1.debounceAsync)(async () => {
|
|
136
|
-
var _a, _b, _c, _d;
|
|
137
137
|
await readyToListen;
|
|
138
138
|
void stopServer(httpsSrv);
|
|
139
139
|
nat_1.defaultBaseUrl.proto = 'http';
|
|
140
|
-
nat_1.defaultBaseUrl.port =
|
|
140
|
+
nat_1.defaultBaseUrl.port = getCurrentPort(httpSrv) ?? 0;
|
|
141
141
|
let port = exports.httpsPortCfg.get();
|
|
142
142
|
try {
|
|
143
143
|
const moreOptions = Object.assign({}, ...await events_1.default.emitAsync('httpsServerOptions') || []); // emitAsync returns an array of objects
|
|
@@ -150,9 +150,9 @@ const considerHttps = (0, misc_1.debounceAsync)(async () => {
|
|
|
150
150
|
if (port >= 0) {
|
|
151
151
|
const certObj = getCertObject();
|
|
152
152
|
if (certObj) {
|
|
153
|
-
const cn =
|
|
153
|
+
const cn = certObj.subject?.CN;
|
|
154
154
|
if (cn)
|
|
155
|
-
console.log("certificate loaded for",
|
|
155
|
+
console.log("certificate loaded for", certObj.altNames?.join(' + ') || cn);
|
|
156
156
|
const now = new Date();
|
|
157
157
|
const from = new Date(certObj.validFrom);
|
|
158
158
|
const to = new Date(certObj.validTo);
|
|
@@ -178,7 +178,7 @@ const considerHttps = (0, misc_1.debounceAsync)(async () => {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
catch (e) {
|
|
181
|
-
httpsSrv
|
|
181
|
+
httpsSrv ||= Object.assign(https.createServer({}), { name: 'https' }); // a dummy container, in case creation failed because of certificate errors
|
|
182
182
|
httpsSrv.error = "bad private key or certificate";
|
|
183
183
|
console.error("failed to create https server: check your private key and certificate", e.message);
|
|
184
184
|
return;
|
|
@@ -196,14 +196,13 @@ const considerHttps = (0, misc_1.debounceAsync)(async () => {
|
|
|
196
196
|
printUrls(httpsSrv.name);
|
|
197
197
|
events_1.default.emit('httpsReady');
|
|
198
198
|
nat_1.defaultBaseUrl.proto = 'https';
|
|
199
|
-
nat_1.defaultBaseUrl.port =
|
|
199
|
+
nat_1.defaultBaseUrl.port = getCurrentPort(httpsSrv) ?? 0;
|
|
200
200
|
}, { wait: 200 }); // give time to have key and cert ready
|
|
201
201
|
exports.cert = (0, config_1.defineConfig)('cert', '', load);
|
|
202
202
|
exports.privateKey = (0, config_1.defineConfig)('private_key', '', load);
|
|
203
203
|
const httpsNeeds = [exports.cert, exports.privateKey];
|
|
204
204
|
function load(v, { object }) {
|
|
205
|
-
|
|
206
|
-
(_a = object.watcher) === null || _a === void 0 ? void 0 : _a.unwatch();
|
|
205
|
+
object.watcher?.unwatch();
|
|
207
206
|
if (!v || v.includes('\n'))
|
|
208
207
|
return v;
|
|
209
208
|
// v is a path, we'll watch the file for changes
|
|
@@ -244,12 +243,12 @@ function startServer(srv, { port, host }) {
|
|
|
244
243
|
});
|
|
245
244
|
async function testIpV4() {
|
|
246
245
|
const res = await listen('0.0.0.0', true);
|
|
247
|
-
await new Promise(res => srv
|
|
246
|
+
await new Promise(res => srv?.close(res)); // close, if any, and wait
|
|
248
247
|
return res > 0;
|
|
249
248
|
}
|
|
250
249
|
function listen(host, silence = false) {
|
|
251
250
|
return new Promise(async (resolve, reject) => {
|
|
252
|
-
srv
|
|
251
|
+
srv?.on('error', onError).listen({ port, host }, () => {
|
|
253
252
|
const ad = srv.address();
|
|
254
253
|
if (!ad)
|
|
255
254
|
return reject('no address');
|
|
@@ -268,7 +267,7 @@ function startServer(srv, { port, host }) {
|
|
|
268
267
|
srv.busy = undefined;
|
|
269
268
|
const { code } = e;
|
|
270
269
|
if (code)
|
|
271
|
-
srv.busy = (0, find_process_1.default)('port', port).then(res => res
|
|
270
|
+
srv.busy = (0, find_process_1.default)('port', port).then(res => res?.map(x => (0, misc_1.prefix)("Service", x.name === 'svchost.exe' && x.cmd.split(x.name)[1]?.trim()) || x.name).join(' + '), () => '');
|
|
272
271
|
if (code === 'EACCES' && port < 1024 && !srv.busy) // on Windows, when port is used by a service, we get EACCES
|
|
273
272
|
srv.error = `lacking permission on port ${port}, try with permission (${const_1.IS_WINDOWS ? 'administrator' : 'sudo'}) or port > 1024`;
|
|
274
273
|
if (code === 'EADDRINUSE' || srv.busy)
|
|
@@ -282,7 +281,7 @@ function startServer(srv, { port, host }) {
|
|
|
282
281
|
}
|
|
283
282
|
function stopServer(srv) {
|
|
284
283
|
return new Promise(resolve => {
|
|
285
|
-
if (!
|
|
284
|
+
if (!srv?.listening)
|
|
286
285
|
return resolve(null);
|
|
287
286
|
const ad = srv.address();
|
|
288
287
|
if (ad && typeof ad !== 'string')
|
|
@@ -295,8 +294,7 @@ function stopServer(srv) {
|
|
|
295
294
|
});
|
|
296
295
|
}
|
|
297
296
|
function getCurrentPort(srv) {
|
|
298
|
-
|
|
299
|
-
return (_a = srv === null || srv === void 0 ? void 0 : srv.address()) === null || _a === void 0 ? void 0 : _a.port;
|
|
297
|
+
return srv?.address()?.port;
|
|
300
298
|
}
|
|
301
299
|
async function getServerStatus(includeSrv = true) {
|
|
302
300
|
return {
|
|
@@ -304,7 +302,7 @@ async function getServerStatus(includeSrv = true) {
|
|
|
304
302
|
https: await serverStatus(httpsSrv, exports.httpsPortCfg.get()),
|
|
305
303
|
};
|
|
306
304
|
async function serverStatus(srv, configuredPort) {
|
|
307
|
-
const busy = await
|
|
305
|
+
const busy = await srv?.busy;
|
|
308
306
|
await (0, misc_1.wait)(0); // simple trick to wait for also .error to be updated. If this trickery becomes necessary elsewhere, then we should make also error a Promise.
|
|
309
307
|
return {
|
|
310
308
|
...lodash_1.default.pick(srv, ['listening', 'error']),
|
|
@@ -329,17 +327,16 @@ async function getIps(external = true) {
|
|
|
329
327
|
x => x !== nat_1.defaultBaseUrl.localIp, // use the "nat" info to put best ip first
|
|
330
328
|
net_1.isIPv6 // false=IPV4 comes first
|
|
331
329
|
]);
|
|
332
|
-
nat_1.defaultBaseUrl.localIp
|
|
330
|
+
nat_1.defaultBaseUrl.localIp ||= ret[0] || '';
|
|
333
331
|
return ret;
|
|
334
332
|
}
|
|
335
333
|
async function getUrls() {
|
|
336
334
|
const on = listenInterface.get();
|
|
337
335
|
const ips = on === renderHost(on) ? [on] : await getIps();
|
|
338
336
|
return Object.fromEntries((0, misc_1.onlyTruthy)([httpSrv, httpsSrv].map(srv => {
|
|
339
|
-
|
|
340
|
-
if (!(srv === null || srv === void 0 ? void 0 : srv.listening))
|
|
337
|
+
if (!srv?.listening)
|
|
341
338
|
return false;
|
|
342
|
-
const port =
|
|
339
|
+
const port = srv?.address()?.port;
|
|
343
340
|
const appendPort = port === (srv.name === 'https' ? 443 : 80) ? '' : ':' + port;
|
|
344
341
|
const urls = ips.map(ip => `${srv.name}://${(0, misc_1.ipForUrl)(ip)}${appendPort}`);
|
|
345
342
|
return urls.length && [srv.name, urls];
|
package/src/log.js
CHANGED
|
@@ -54,14 +54,16 @@ const index_1 = require("./index");
|
|
|
54
54
|
const serveGuiFiles_1 = require("./serveGuiFiles");
|
|
55
55
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
56
56
|
class Logger {
|
|
57
|
+
name;
|
|
58
|
+
stream;
|
|
59
|
+
last;
|
|
60
|
+
path = '';
|
|
57
61
|
constructor(name) {
|
|
58
62
|
this.name = name;
|
|
59
|
-
this.path = '';
|
|
60
63
|
}
|
|
61
64
|
async setPath(path) {
|
|
62
|
-
var _a;
|
|
63
65
|
this.path = path;
|
|
64
|
-
|
|
66
|
+
this.stream?.end();
|
|
65
67
|
this.last = undefined;
|
|
66
68
|
if (!path)
|
|
67
69
|
return this.stream = undefined;
|
|
@@ -69,15 +71,15 @@ class Logger {
|
|
|
69
71
|
const stats = await (0, util_files_1.statWithTimeout)(path);
|
|
70
72
|
this.last = stats.mtime;
|
|
71
73
|
}
|
|
72
|
-
catch
|
|
73
|
-
if (await (0, util_files_1.
|
|
74
|
+
catch {
|
|
75
|
+
if (await (0, util_files_1.ensureParentFolder)(path) === false)
|
|
74
76
|
console.log("cannot create folder for", path);
|
|
75
77
|
}
|
|
76
78
|
this.reopen();
|
|
77
79
|
}
|
|
78
80
|
reopen() {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
return this.stream = (0, util_files_1.createFileWithPath)(this.path, { flags: 'a' })
|
|
82
|
+
?.on('error', () => this.stream = undefined);
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
// we'll have names same as config keys. These are used also by the get_log api.
|
|
@@ -115,21 +117,20 @@ const logMw = async (ctx, next) => {
|
|
|
115
117
|
const conn = (0, connections_1.getConnection)(ctx); // collect reference before close
|
|
116
118
|
// don't await, as we don't want to hold the middlewares chain
|
|
117
119
|
ctx.state.completed.then(() => {
|
|
118
|
-
var _a, _b, _c, _d;
|
|
119
120
|
if (ctx.state.dontLog || ctx.state.considerAsGui && !serveGuiFiles_1.logGui.get())
|
|
120
121
|
return;
|
|
121
122
|
if (dontLogNet.compiled()(ctx.ip))
|
|
122
123
|
return;
|
|
123
124
|
const isError = ctx.status >= 400;
|
|
124
125
|
const logger = isError && accessErrorLog || accessLogger;
|
|
125
|
-
const rotate =
|
|
126
|
+
const rotate = logRotation.get()?.[0];
|
|
126
127
|
let { stream, last, path } = logger;
|
|
127
128
|
if (!stream)
|
|
128
129
|
return;
|
|
129
130
|
logger.last = now;
|
|
130
131
|
if (rotate && last) { // rotation enabled and a file exists?
|
|
131
132
|
const passed = Number(now) - Number(last)
|
|
132
|
-
-
|
|
133
|
+
- 3600_000; // be pessimistic and count a possible DST change
|
|
133
134
|
if (rotate === 'm' && (passed >= 31 * misc_1.DAY || now.getMonth() !== last.getMonth())
|
|
134
135
|
|| rotate === 'd' && (passed >= misc_1.DAY || now.getDate() !== last.getDate()) // checking passed will solve the case when the day of the month is the same but a month has passed
|
|
135
136
|
|| rotate === 'w' && (passed >= 7 * misc_1.DAY || now.getDay() < last.getDay())) {
|
|
@@ -149,9 +150,9 @@ const logMw = async (ctx, next) => {
|
|
|
149
150
|
}
|
|
150
151
|
const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s %s\n'; // Apache's Common Log Format
|
|
151
152
|
const a = now.toString().split(' '); // like nginx, our default log contains the time of log writing
|
|
152
|
-
const date = a[2] + '/' + a[1] + '/' + a[3] + ':' + a[4] + ' ' +
|
|
153
|
+
const date = a[2] + '/' + a[1] + '/' + a[3] + ':' + a[4] + ' ' + a[5]?.slice(3);
|
|
153
154
|
const user = (0, auth_1.getCurrentUsername)(ctx);
|
|
154
|
-
const length =
|
|
155
|
+
const length = ctx.state.length ?? ctx.length;
|
|
155
156
|
const uri = ctx.originalUrl;
|
|
156
157
|
const duration = (Date.now() - Number(now)) / 1000;
|
|
157
158
|
ctx.logExtra(ctx.vfsNode && {
|
|
@@ -162,7 +163,7 @@ const logMw = async (ctx, next) => {
|
|
|
162
163
|
size: ctx.state.uploadSize,
|
|
163
164
|
speed: Math.round(ctx.state.uploadSize / duration),
|
|
164
165
|
});
|
|
165
|
-
if (conn
|
|
166
|
+
if (conn?.country)
|
|
166
167
|
ctx.logExtra({ country: conn.country });
|
|
167
168
|
if (logUA.get())
|
|
168
169
|
ctx.logExtra({ ua: ctx.get('user-agent') || undefined });
|
|
@@ -171,7 +172,7 @@ const logMw = async (ctx, next) => {
|
|
|
171
172
|
events_2.default.emit(logger.name, { ctx, length, user, ts: now, uri, extra });
|
|
172
173
|
debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
|
|
173
174
|
(0, util_files_1.statWithTimeout)(logger.path).catch(() => logger.reopen())); // async = smoother but we may lose some entries
|
|
174
|
-
stream.write(util.format(format, ctx.ip, user || '-', date, ctx.method, uri, ctx.req.httpVersion, ctx.status,
|
|
175
|
+
stream.write(util.format(format, ctx.ip, user || '-', date, ctx.method, uri, ctx.req.httpVersion, ctx.status, length?.toString() ?? '-', lodash_1.default.isEmpty(extra) ? '' : JSON.stringify(JSON.stringify(extra))));
|
|
175
176
|
});
|
|
176
177
|
};
|
|
177
178
|
exports.logMw = logMw;
|
|
@@ -186,7 +187,7 @@ function doubleDigit(n) {
|
|
|
186
187
|
async function getRotatedFiles() {
|
|
187
188
|
return Object.fromEntries(await Promise.all(exports.loggers.map(async (x) => {
|
|
188
189
|
const mask = (0, misc_1.strinsert)(x.path, x.path.length - (0, path_1.extname)(x.path).length, '-2*'); // including 2, initial digit of the year, will only take rotated files and not "-error"
|
|
189
|
-
return [x.name, (await (0, fast_glob_1.default)(mask, { stats: true })).map(x =>
|
|
190
|
+
return [x.name, (await (0, fast_glob_1.default)(mask, { stats: true })).map(x => ({ path: x.path, size: x.stats?.size }))];
|
|
190
191
|
})));
|
|
191
192
|
}
|
|
192
193
|
// dump console.error to file
|
|
@@ -195,11 +196,11 @@ debugLogFile.once('open', () => {
|
|
|
195
196
|
const was = console.error;
|
|
196
197
|
console.error = function (...args) {
|
|
197
198
|
was.apply(this, args);
|
|
198
|
-
args = args.map(x =>
|
|
199
|
+
args = args.map(x => typeof x === 'string' ? x : ((0, misc_1.tryJson)(x) ?? String(x)));
|
|
199
200
|
debugLogFile.write((0, misc_1.formatTimestamp)(new Date) + ' - ' + args.join(' ') + '\n');
|
|
200
201
|
};
|
|
201
202
|
// limit log size
|
|
202
|
-
const LIMIT =
|
|
203
|
+
const LIMIT = 1_000_000;
|
|
203
204
|
const { path } = debugLogFile;
|
|
204
205
|
(0, misc_1.repeat)(misc_1.DAY, () => {
|
|
205
206
|
if ((0, fs_1.statSync)(path).size < LIMIT)
|
package/src/middlewares.js
CHANGED
|
@@ -53,7 +53,7 @@ let proxyDetected;
|
|
|
53
53
|
const someSecurity = (ctx, next) => {
|
|
54
54
|
ctx.request.ip = (0, connections_1.normalizeIp)(ctx.ip);
|
|
55
55
|
const ss = ctx.session;
|
|
56
|
-
if (
|
|
56
|
+
if (ss?.username && !ss?.[misc_1.ALLOW_SESSION_IP_CHANGE])
|
|
57
57
|
if (!ss.ip)
|
|
58
58
|
ss.ip = ctx.ip;
|
|
59
59
|
else if (ss.ip !== ctx.ip) {
|
|
@@ -61,7 +61,7 @@ const someSecurity = (ctx, next) => {
|
|
|
61
61
|
ss.ip = ctx.ip;
|
|
62
62
|
}
|
|
63
63
|
try {
|
|
64
|
-
if ((0, misc_1.
|
|
64
|
+
if ((0, misc_1.hasDirTraversal)(decodeURI(ctx.path)))
|
|
65
65
|
return ctx.status = const_1.HTTP_FOOL;
|
|
66
66
|
if (!ctx.state.skipFilters && (0, block_1.applyBlock)(ctx.socket, ctx.ip))
|
|
67
67
|
return;
|
|
@@ -74,7 +74,7 @@ const someSecurity = (ctx, next) => {
|
|
|
74
74
|
if (ctx.get('cf-ray'))
|
|
75
75
|
exports.cloudflareDetected = new Date();
|
|
76
76
|
}
|
|
77
|
-
catch
|
|
77
|
+
catch {
|
|
78
78
|
return ctx.status = const_1.HTTP_FOOL;
|
|
79
79
|
}
|
|
80
80
|
if (!ctx.secure && forceHttps.get() && (0, listen_1.getHttpsWorkingPort)() && !(0, misc_1.isLocalHost)(ctx)) {
|
|
@@ -89,20 +89,19 @@ const someSecurity = (ctx, next) => {
|
|
|
89
89
|
exports.someSecurity = someSecurity;
|
|
90
90
|
// limited to http proxies
|
|
91
91
|
function getProxyDetected() {
|
|
92
|
-
if (
|
|
92
|
+
if (proxyDetected?.state.whenProxyDetected < Date.now() - misc_1.DAY) // detection is reset after a day
|
|
93
93
|
proxyDetected = undefined;
|
|
94
94
|
return proxyDetected && { from: proxyDetected.socket.remoteAddress, for: proxyDetected.get('X-Forwarded-For') };
|
|
95
95
|
}
|
|
96
96
|
const prepareState = async (ctx, next) => {
|
|
97
|
-
|
|
98
|
-
if ((_a = ctx.session) === null || _a === void 0 ? void 0 : _a.username) {
|
|
97
|
+
if (ctx.session?.username) {
|
|
99
98
|
if (ctx.session.ts < auth_1.invalidateSessionBefore.get(ctx.session.username))
|
|
100
99
|
delete ctx.session.username;
|
|
101
100
|
ctx.session.maxAge = exports.sessionDuration.compiled();
|
|
102
101
|
}
|
|
103
102
|
// calculate these once and for all
|
|
104
103
|
ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
|
|
105
|
-
const a = ctx.state.account = await urlLogin() || await getHttpAccount() || (0, perm_1.getAccount)(
|
|
104
|
+
const a = ctx.state.account = await urlLogin() || await getHttpAccount() || (0, perm_1.getAccount)(ctx.session?.username, false);
|
|
106
105
|
if (a && (!(0, perm_1.accountCanLogin)(a) || failAllowNet(ctx, a))) // enforce allow_net also after login
|
|
107
106
|
ctx.state.account = undefined;
|
|
108
107
|
ctx.state.revProxyPath = ctx.get('x-forwarded-prefix');
|
|
@@ -117,25 +116,23 @@ const prepareState = async (ctx, next) => {
|
|
|
117
116
|
return u && (0, auth_1.clearTextLogin)(ctx, u, p, 'url');
|
|
118
117
|
}
|
|
119
118
|
function getHttpAccount() {
|
|
120
|
-
|
|
121
|
-
const b64 = allowAuthorizationHeader.get() && ((_a = ctx.get('authorization')) === null || _a === void 0 ? void 0 : _a.split(' ')[1]);
|
|
119
|
+
const b64 = allowAuthorizationHeader.get() && ctx.get('authorization')?.split(' ')[1];
|
|
122
120
|
if (!b64)
|
|
123
121
|
return;
|
|
124
122
|
try {
|
|
125
123
|
const [u, p] = atob(b64).split(':');
|
|
126
|
-
if (!u || u ===
|
|
124
|
+
if (!u || u === ctx.session?.username)
|
|
127
125
|
return; // providing credentials, but not needed
|
|
128
126
|
return (0, auth_1.clearTextLogin)(ctx, u, p || '', 'header');
|
|
129
127
|
}
|
|
130
|
-
catch
|
|
128
|
+
catch { }
|
|
131
129
|
}
|
|
132
130
|
};
|
|
133
131
|
exports.prepareState = prepareState;
|
|
134
132
|
function failAllowNet(ctx, a) {
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
if (!cached && mask && ((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username))
|
|
133
|
+
const cached = ctx.session?.allowNet; // won't reflect changes until session is terminated
|
|
134
|
+
const mask = cached ?? (0, perm_1.getFromAccount)(a || '', a => a.allow_net);
|
|
135
|
+
if (!cached && mask && ctx.session?.username)
|
|
139
136
|
ctx.session.allowNet = mask; // must be deleted on logout by setLoggedIn
|
|
140
137
|
const ret = mask && !(0, misc_1.netMatches)(ctx.ip, mask, true);
|
|
141
138
|
if (ret)
|
package/src/misc.js
CHANGED
|
@@ -51,12 +51,10 @@ function isLocalHost(c) {
|
|
|
51
51
|
}
|
|
52
52
|
// this will memory-leak over mask, so be careful with what you use this. Object is 3x faster than _.memoize
|
|
53
53
|
function netMatches(ip, mask, emptyMaskReturns = false) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return (cache[_b = mask + (emptyMaskReturns ? '1' : '0')] || (cache[_b] = makeNetMatcher(mask, emptyMaskReturns)))(ip); // cache the matcher
|
|
54
|
+
const cache = netMatches.cache ||= {};
|
|
55
|
+
return (cache[mask + (emptyMaskReturns ? '1' : '0')] ||= makeNetMatcher(mask, emptyMaskReturns))(ip); // cache the matcher
|
|
57
56
|
}
|
|
58
57
|
function makeNetMatcher(mask, emptyMaskReturns = false) {
|
|
59
|
-
var _a;
|
|
60
58
|
if (!mask)
|
|
61
59
|
return () => emptyMaskReturns;
|
|
62
60
|
mask = mask.replaceAll(' ', '');
|
|
@@ -68,7 +66,7 @@ function makeNetMatcher(mask, emptyMaskReturns = false) {
|
|
|
68
66
|
return (0, cross_1.makeMatcher)(mask);
|
|
69
67
|
}
|
|
70
68
|
const all = mask.split('|');
|
|
71
|
-
const neg =
|
|
69
|
+
const neg = all[0]?.[0] === '!';
|
|
72
70
|
if (neg)
|
|
73
71
|
all[0] = all[0].slice(1);
|
|
74
72
|
const bl = new node_net_1.BlockList();
|
|
@@ -83,14 +81,14 @@ function makeNetMatcher(mask, emptyMaskReturns = false) {
|
|
|
83
81
|
try {
|
|
84
82
|
bl.addSubnet(address, Number(m[2]));
|
|
85
83
|
}
|
|
86
|
-
catch
|
|
84
|
+
catch {
|
|
87
85
|
throw x;
|
|
88
86
|
}
|
|
89
87
|
else if (m[3])
|
|
90
88
|
try {
|
|
91
89
|
bl.addRange(address, parseAddress(m[3]));
|
|
92
90
|
}
|
|
93
|
-
catch
|
|
91
|
+
catch {
|
|
94
92
|
throw m[3];
|
|
95
93
|
}
|
|
96
94
|
else
|
|
@@ -100,7 +98,7 @@ function makeNetMatcher(mask, emptyMaskReturns = false) {
|
|
|
100
98
|
try {
|
|
101
99
|
return neg !== bl.check(parseAddress(ip));
|
|
102
100
|
}
|
|
103
|
-
catch
|
|
101
|
+
catch {
|
|
104
102
|
console.error("invalid address ", ip);
|
|
105
103
|
return false;
|
|
106
104
|
}
|
|
@@ -122,8 +120,7 @@ function asyncGeneratorToReadable(generator) {
|
|
|
122
120
|
return new stream_1.Readable({
|
|
123
121
|
objectMode: true,
|
|
124
122
|
destroy() {
|
|
125
|
-
|
|
126
|
-
void ((_a = iterator.return) === null || _a === void 0 ? void 0 : _a.call(iterator));
|
|
123
|
+
void iterator.return?.();
|
|
127
124
|
},
|
|
128
125
|
read() {
|
|
129
126
|
iterator.next().then(it => {
|
|
@@ -136,10 +133,11 @@ function asyncGeneratorToReadable(generator) {
|
|
|
136
133
|
}
|
|
137
134
|
// produces as promises resolve, not sequentially
|
|
138
135
|
class AsapStream extends stream_1.Readable {
|
|
136
|
+
promises;
|
|
137
|
+
finished = false;
|
|
139
138
|
constructor(promises) {
|
|
140
139
|
super({ objectMode: true });
|
|
141
140
|
this.promises = promises;
|
|
142
|
-
this.finished = false;
|
|
143
141
|
}
|
|
144
142
|
_read() {
|
|
145
143
|
if (this.finished)
|
package/src/nat.js
CHANGED
|
@@ -30,10 +30,10 @@ exports.defaultBaseUrl = (0, valtio_1.proxy)({
|
|
|
30
30
|
return `${this.proto}://${(0, cross_1.ipForUrl)(ip || 'localhost')}${!port || port === defPort ? '' : ':' + port}`;
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
|
-
exports.upnpClient = new nat_upnp_rejetto_1.Client({ timeout:
|
|
33
|
+
exports.upnpClient = new nat_upnp_rejetto_1.Client({ timeout: 4_000 });
|
|
34
34
|
const originalMethod = exports.upnpClient.getGateway;
|
|
35
35
|
// other client methods call getGateway too, so this will ensure they reuse this same result
|
|
36
|
-
exports.upnpClient.getGateway = (0, debounceAsync_1.debounceAsync)(() => originalMethod.apply(exports.upnpClient), { retain: cross_1.HOUR, retainFailure:
|
|
36
|
+
exports.upnpClient.getGateway = (0, debounceAsync_1.debounceAsync)(() => originalMethod.apply(exports.upnpClient), { retain: cross_1.HOUR, retainFailure: 30_000 });
|
|
37
37
|
exports.upnpClient.getGateway().then(res => {
|
|
38
38
|
console.log("upnp found", res.gateway.description);
|
|
39
39
|
}, e => console.debug('upnp failed:', e.message || String(e)));
|
|
@@ -46,16 +46,16 @@ exports.upnpClient.getGateway().then(res => {
|
|
|
46
46
|
}, () => { }));
|
|
47
47
|
exports.getPublicIps = (0, debounceAsync_1.debounceAsync)(async () => {
|
|
48
48
|
const res = await (0, github_1.getProjectInfo)();
|
|
49
|
-
const groupedByVersion = Object.values(lodash_1.default.groupBy(res.publicIpServices, x =>
|
|
49
|
+
const groupedByVersion = Object.values(lodash_1.default.groupBy(res.publicIpServices, x => x.v ?? 4));
|
|
50
50
|
const ips = await (0, cross_1.promiseBestEffort)(groupedByVersion.map(singleVersion => Promise.any(singleVersion.map(async (svc) => {
|
|
51
51
|
if (typeof svc === 'string')
|
|
52
52
|
svc = { type: 'http', url: svc };
|
|
53
53
|
console.debug("trying ip service", svc.url || svc.name);
|
|
54
54
|
if (svc.type === 'http')
|
|
55
|
-
return (0, util_http_1.httpString)(svc.url, { timeout:
|
|
55
|
+
return (0, util_http_1.httpString)(svc.url, { timeout: 5_000 });
|
|
56
56
|
if (svc.type !== 'dns')
|
|
57
57
|
throw "unsupported";
|
|
58
|
-
const resolver = new promises_1.Resolver({ timeout:
|
|
58
|
+
const resolver = new promises_1.Resolver({ timeout: 2_000 });
|
|
59
59
|
resolver.setServers(svc.ips);
|
|
60
60
|
return resolver.resolve(svc.name, svc.dnsRecord);
|
|
61
61
|
}).map(async (ret) => {
|
|
@@ -67,18 +67,17 @@ exports.getPublicIps = (0, debounceAsync_1.debounceAsync)(async () => {
|
|
|
67
67
|
return exports.defaultBaseUrl.publicIps = lodash_1.default.uniq(ips.flat());
|
|
68
68
|
}, { retain: 10 * cross_1.MINUTE });
|
|
69
69
|
exports.getNatInfo = (0, debounceAsync_1.debounceAsync)(async () => {
|
|
70
|
-
var _a, _b, _c, _d;
|
|
71
70
|
const gatewayIpPromise = findGateway().catch(() => undefined);
|
|
72
|
-
const res = await (0, cross_1.haveTimeout)(
|
|
71
|
+
const res = await (0, cross_1.haveTimeout)(10_000, exports.upnpClient.getGateway()).catch(() => null);
|
|
73
72
|
const status = await (0, listen_1.getServerStatus)();
|
|
74
|
-
const mappings = res && await (0, cross_1.haveTimeout)(
|
|
75
|
-
console.debug("mappings found:",
|
|
73
|
+
const mappings = res && await (0, cross_1.haveTimeout)(5_000, exports.upnpClient.getMappings()).catch(() => null);
|
|
74
|
+
console.debug("mappings found:", mappings?.map(x => x.description).join(', ') || "none");
|
|
76
75
|
const localIps = await (0, listen_1.getIps)(false);
|
|
77
76
|
const gatewayIp = await gatewayIpPromise;
|
|
78
|
-
const localIp =
|
|
79
|
-
const internalPort =
|
|
77
|
+
const localIp = res?.address || (gatewayIp ? lodash_1.default.maxBy(localIps, x => (0, cross_1.inCommon)(x, gatewayIp)) : localIps[0]);
|
|
78
|
+
const internalPort = status?.https?.listening && status.https.port || status?.http?.listening && status.http.port || undefined;
|
|
80
79
|
const mapped = lodash_1.default.find(mappings, x => x.private.host === localIp && x.private.port === internalPort);
|
|
81
|
-
const externalPort = mapped
|
|
80
|
+
const externalPort = mapped?.public.port;
|
|
82
81
|
if (localIp)
|
|
83
82
|
exports.defaultBaseUrl.localIp = localIp;
|
|
84
83
|
exports.defaultBaseUrl.port = externalPort || internalPort || 0;
|
|
@@ -91,18 +90,17 @@ exports.getNatInfo = (0, debounceAsync_1.debounceAsync)(async () => {
|
|
|
91
90
|
mapped80: lodash_1.default.find(mappings, x => x.private.host === localIp && x.private.port === 80 && x.public.port === 80),
|
|
92
91
|
internalPort,
|
|
93
92
|
externalPort,
|
|
94
|
-
proto:
|
|
93
|
+
proto: status?.https?.listening ? 'https' : status?.http?.listening ? 'http' : '',
|
|
95
94
|
};
|
|
96
95
|
}, { reuseRunning: true });
|
|
97
96
|
(0, exports.getNatInfo)();
|
|
98
97
|
function findGateway() {
|
|
99
98
|
return new Promise((resolve, reject) => (0, child_process_1.exec)(const_1.IS_WINDOWS || const_1.IS_MAC ? 'netstat -rn' : 'route -n', (err, out) => {
|
|
100
|
-
var _a, _b;
|
|
101
99
|
if (err)
|
|
102
100
|
return reject(err);
|
|
103
101
|
if (!const_1.IS_WINDOWS)
|
|
104
|
-
return resolve(
|
|
102
|
+
return resolve(out.match(const_1.IS_MAC ? /default +([\d.]+)/ : /^0\.0\.0\.0 +([\d.]+)/)?.[1]);
|
|
105
103
|
const sortedByMetric = lodash_1.default.sortBy([...out.matchAll(/(?:0\.0\.0\.0 +){2}([\d.]+)\s+[\d.]+\s+(\d+)/g)], x => Number(x[2]));
|
|
106
|
-
resolve(
|
|
104
|
+
resolve(sortedByMetric[0]?.[1]); // take ip with lowest metric
|
|
107
105
|
}));
|
|
108
106
|
}
|
package/src/outboundProxy.js
CHANGED
|
@@ -13,27 +13,27 @@ const outboundProxy = (0, config_1.defineConfig)(cross_1.CFG.outbound_proxy, '',
|
|
|
13
13
|
if (!v || process.env.HFS_SKIP_PROXY_TEST)
|
|
14
14
|
return;
|
|
15
15
|
const test = 'https://google.com';
|
|
16
|
-
|
|
16
|
+
console.debug("testing proxy using", test);
|
|
17
|
+
(0, util_http_1.httpString)(test, { noRedirect: true }).catch(e => console.error(`proxy test failed on ${test} : ${e?.errors?.[0] || e}`)); // `.errors` in case of AggregateError
|
|
17
18
|
}
|
|
18
|
-
catch
|
|
19
|
+
catch {
|
|
19
20
|
console.warn("invalid URL", v);
|
|
20
21
|
return '';
|
|
21
22
|
}
|
|
22
23
|
});
|
|
23
24
|
config_1.configReady.then(async ([startedWithoutConfig]) => {
|
|
24
|
-
var _a, _b, _c, _d;
|
|
25
25
|
if (!const_1.IS_WINDOWS || !startedWithoutConfig)
|
|
26
26
|
return;
|
|
27
27
|
// try to read Windows system setting for proxy
|
|
28
28
|
const out = await (0, util_os_1.reg)('query', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings');
|
|
29
|
-
if (!Number(
|
|
29
|
+
if (!Number(/ProxyEnable\s+\w+\s+(.+)/.exec(out)?.[1]))
|
|
30
30
|
return;
|
|
31
|
-
const read =
|
|
31
|
+
const read = /ProxyServer\s+\w+\s+(.+)/.exec(out)?.[1];
|
|
32
32
|
if (!read)
|
|
33
33
|
return;
|
|
34
34
|
// it can be like "IP:PORT" or "http=IP:PORT;https=IP:PORT;ftp=IP:PORT"
|
|
35
|
-
const url = (0, cross_1.prefix)('https://',
|
|
36
|
-
|| (0, cross_1.prefix)('http://',
|
|
35
|
+
const url = (0, cross_1.prefix)('https://', /https=([\d:.]+)/.exec(out)?.[1]) // prefer https
|
|
36
|
+
|| (0, cross_1.prefix)('http://', /http=([\d:.]+)/.exec(out)?.[1])
|
|
37
37
|
|| !read.includes('=') && 'http://' + read; // simpler form
|
|
38
38
|
if (!url)
|
|
39
39
|
return;
|
package/src/perm.js
CHANGED
|
@@ -46,8 +46,7 @@ function expandUsername(who) {
|
|
|
46
46
|
}
|
|
47
47
|
// check if current username or any ancestor match the provided usernames
|
|
48
48
|
function ctxBelongsTo(ctx, usernames) {
|
|
49
|
-
|
|
50
|
-
return ((_a = ctx.state).usernames || (_a.usernames = expandUsername((0, auth_1.getCurrentUsername)(ctx)))) // cache ancestors' usernames inside context state
|
|
49
|
+
return (ctx.state.usernames ||= expandUsername((0, auth_1.getCurrentUsername)(ctx))) // cache ancestors' usernames inside context state
|
|
51
50
|
.some((u) => usernames.includes(u));
|
|
52
51
|
}
|
|
53
52
|
function getUsernames() {
|
|
@@ -78,7 +77,7 @@ async function updateAccount(account, change) {
|
|
|
78
77
|
const jsonWas = JSON.stringify(account);
|
|
79
78
|
const { username: usernameWas } = account;
|
|
80
79
|
if (typeof change === 'function')
|
|
81
|
-
await
|
|
80
|
+
await change?.(account);
|
|
82
81
|
else {
|
|
83
82
|
const u = normalizeUsername(change.username || '');
|
|
84
83
|
if (u && u !== usernameWas && getAccount(u))
|
|
@@ -106,7 +105,7 @@ async function updateAccount(account, change) {
|
|
|
106
105
|
if (!account.belongs.length)
|
|
107
106
|
delete account.belongs;
|
|
108
107
|
}
|
|
109
|
-
account.expire
|
|
108
|
+
account.expire &&= new Date(account.expire);
|
|
110
109
|
if (username !== usernameWas)
|
|
111
110
|
renameAccount(usernameWas, username);
|
|
112
111
|
if (jsonWas !== JSON.stringify(account)) // this test will miss the 'username' field, because hidden, but renameAccount is already calling saveAccountsASAP
|
|
@@ -118,7 +117,7 @@ exports.accounts.sub(lodash_1.default.debounce(obj => {
|
|
|
118
117
|
// consider some validation here, in case of manual edit of the config
|
|
119
118
|
lodash_1.default.each(obj, (rec, k) => {
|
|
120
119
|
const norm = normalizeUsername(k);
|
|
121
|
-
if (
|
|
120
|
+
if (rec?.username !== norm) {
|
|
122
121
|
if (!rec) // an empty object in yaml is parsed as null
|
|
123
122
|
rec = obj[norm] = { username: norm };
|
|
124
123
|
else if ((0, misc_1.objRenameKey)(obj, k, norm))
|
|
@@ -132,7 +131,6 @@ function normalizeUsername(username) {
|
|
|
132
131
|
return username.toLocaleLowerCase();
|
|
133
132
|
}
|
|
134
133
|
function renameAccount(from, to) {
|
|
135
|
-
var _a;
|
|
136
134
|
from = normalizeUsername(from);
|
|
137
135
|
const as = exports.accounts.get();
|
|
138
136
|
to = normalizeUsername(to);
|
|
@@ -144,7 +142,7 @@ function renameAccount(from, to) {
|
|
|
144
142
|
(0, misc_1.setHidden)(as[to], { username: to });
|
|
145
143
|
// update references
|
|
146
144
|
for (const a of Object.values(as)) {
|
|
147
|
-
const idx =
|
|
145
|
+
const idx = a.belongs?.indexOf(from);
|
|
148
146
|
if (idx !== undefined && idx >= 0)
|
|
149
147
|
a.belongs[idx] = to;
|
|
150
148
|
}
|
|
@@ -190,15 +188,12 @@ function accountHasPassword(account) {
|
|
|
190
188
|
return Boolean(account.password || account.srp);
|
|
191
189
|
}
|
|
192
190
|
function accountCanLogin(account) {
|
|
193
|
-
|
|
194
|
-
return (accountHasPassword(account) || ((_a = account.plugin) === null || _a === void 0 ? void 0 : _a.auth)) && !accountIsDisabled(account);
|
|
191
|
+
return (accountHasPassword(account) || account.plugin?.auth) && !accountIsDisabled(account);
|
|
195
192
|
}
|
|
196
193
|
function accountIsDisabled(account) {
|
|
197
|
-
var _a;
|
|
198
194
|
return Boolean(account.disabled
|
|
199
195
|
|| account.expire < Date.now()
|
|
200
|
-
||
|
|
201
|
-
)
|
|
196
|
+
|| account.belongs?.length // don't every() on empty array, as it returns true
|
|
202
197
|
&& account.belongs.map(u => getAccount(u, false)).every(a => a && accountIsDisabled(a)));
|
|
203
198
|
}
|
|
204
199
|
function accountCanLoginAdmin(account) {
|
package/src/persistence.js
CHANGED
|
@@ -9,7 +9,7 @@ exports.storedMap = new kvstorage_1.KvStorage({
|
|
|
9
9
|
maxPutDelay: misc_1.MINUTE,
|
|
10
10
|
maxPutDelayCreate: 1000,
|
|
11
11
|
rewriteLater: true,
|
|
12
|
-
bucketThreshold:
|
|
12
|
+
bucketThreshold: 10_000,
|
|
13
13
|
});
|
|
14
14
|
exports.storedMap.open('data.kv');
|
|
15
15
|
(0, first_1.onProcessExit)(() => exports.storedMap.flush());
|