hfs 0.57.24 → 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-CMArzROW.js → index-DSkhXlWX.js} +129 -129
- package/admin/assets/{sha512-BbjMQS_R.js → sha512-CoddZKeD.js} +1 -1
- package/admin/index.html +1 -1
- package/frontend/assets/{index-legacy-uM-kzXmI.js → index-legacy-C4Gclsbz.js} +4 -4
- package/frontend/assets/{sha512-legacy-CWlrw4EZ.js → sha512-legacy-C-FMk44u.js} +1 -1
- package/frontend/index.html +1 -1
- package/package.json +5 -4
- 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 +7 -9
- package/src/api.auth.js +7 -11
- package/src/api.get_file_list.js +13 -19
- package/src/api.lang.js +1 -1
- package/src/api.log.js +1 -2
- package/src/api.monitor.js +2 -3
- package/src/api.net.js +2 -3
- package/src/api.plugins.js +8 -10
- package/src/api.vfs.js +15 -20
- 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 +10 -10
- 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 +12 -8
- package/src/first.js +13 -8
- package/src/frontEndApis.js +5 -5
- package/src/geo.js +5 -7
- 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 +20 -20
- package/src/middlewares.js +12 -15
- package/src/misc.js +15 -14
- package/src/nat.js +14 -16
- package/src/outboundProxy.js +7 -7
- package/src/perm.js +12 -13
- 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 +7 -8
- package/src/serveGuiAndSharedFiles.js +7 -9
- package/src/serveGuiFiles.js +11 -21
- package/src/stat.js +40 -0
- package/src/statWorker.js +11 -0
- package/src/throttler.js +5 -7
- package/src/update.js +9 -9
- package/src/upload.js +11 -13
- package/src/util-files.js +48 -32
- package/src/util-http.js +80 -29
- package/src/util-os.js +5 -8
- package/src/vfs.js +77 -68
- package/src/walkDir.js +9 -9
- package/src/watchLoad.js +4 -4
- package/src/zip.js +5 -10
package/src/github.js
CHANGED
|
@@ -39,15 +39,13 @@ function getGithubDefaultBranch(repo) {
|
|
|
39
39
|
if (!repo.includes('/'))
|
|
40
40
|
throw 'malformed repo';
|
|
41
41
|
return branchCache.try(repo, async () => {
|
|
42
|
-
var _a;
|
|
43
42
|
for (const b of ['main', 'master']) // try to not consume api quota
|
|
44
43
|
if (await (0, misc_1.httpString)(`https://github.com/${repo}/raw/refs/heads/${b}/dist/plugin.js`, { method: 'HEAD', noRedirect: true }).then(() => 1, () => 0))
|
|
45
44
|
return b;
|
|
46
|
-
return (
|
|
45
|
+
return (await getRepoInfo(repo))?.default_branch;
|
|
47
46
|
});
|
|
48
47
|
}
|
|
49
48
|
async function downloadPlugin(repo, { branch = '', overwrite = false } = {}) {
|
|
50
|
-
var _a, _b, _c;
|
|
51
49
|
if (typeof repo !== 'string')
|
|
52
50
|
repo = repo.main;
|
|
53
51
|
if (exports.downloading[repo])
|
|
@@ -63,19 +61,19 @@ async function downloadPlugin(repo, { branch = '', overwrite = false } = {}) {
|
|
|
63
61
|
if (customRepo) { // custom repo
|
|
64
62
|
if (!pl)
|
|
65
63
|
throw new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, "bad repo");
|
|
66
|
-
const customRepo = (
|
|
67
|
-
let url = customRepo
|
|
64
|
+
const customRepo = (pl.getData?.() || pl).repo;
|
|
65
|
+
let url = customRepo?.zip;
|
|
68
66
|
if (!url)
|
|
69
67
|
throw new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, "bad plugin");
|
|
70
68
|
if (!url.includes('//'))
|
|
71
69
|
url = customRepo.web + url;
|
|
72
|
-
return await go(url, pl
|
|
70
|
+
return await go(url, pl?.id, customRepo.zipRoot ?? DIST_ROOT);
|
|
73
71
|
}
|
|
74
|
-
branch
|
|
72
|
+
branch ||= await getGithubDefaultBranch(repo);
|
|
75
73
|
const short = repo.split('/')[1]; // second part, repo without the owner
|
|
76
74
|
if (!short)
|
|
77
75
|
throw new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, "bad repo");
|
|
78
|
-
const folder = overwrite &&
|
|
76
|
+
const folder = overwrite && pl?.id // use existing folder
|
|
79
77
|
|| (getFolder2repo().hasOwnProperty(short) ? repo.replace('/', '-') // longer form only if another plugin is using short form, to avoid overwriting
|
|
80
78
|
: short.replace(/^hfs-/, ''));
|
|
81
79
|
const GITHUB_ZIP_ROOT = short + '-' + branch; // GitHub puts everything within this folder
|
|
@@ -137,7 +135,7 @@ async function downloadPlugin(repo, { branch = '', overwrite = false } = {}) {
|
|
|
137
135
|
await (0, promises_1.rename)(tempInstallPath, installPath)
|
|
138
136
|
.catch(e => { throw e.code !== 'ENOENT' ? e : new apiMiddleware_1.ApiError(const_1.HTTP_NOT_ACCEPTABLE, "missing main file"); });
|
|
139
137
|
if (wasRunning)
|
|
140
|
-
if (await (0, misc_1.waitFor)(() => (0, plugins_1.getPluginInfo)(folder), { timeout:
|
|
138
|
+
if (await (0, misc_1.waitFor)(() => (0, plugins_1.getPluginInfo)(folder), { timeout: 10_000 }))
|
|
141
139
|
void (0, plugins_1.startPlugin)(folder) // don't wait, in case it fails to start. We still use startPlugin instead of enablePlugin, as it will take care of disabling other themes.
|
|
142
140
|
.catch(console.warn);
|
|
143
141
|
events_1.default.emit('pluginDownloaded', { id: folder, repo });
|
|
@@ -167,7 +165,7 @@ async function readOnlinePlugin(repo, branch = '') {
|
|
|
167
165
|
main = pl.repo.web + main;
|
|
168
166
|
return (0, plugins_1.parsePluginSource)(main, await (0, misc_1.httpString)(main)); // use 'repo' as 'id' client-side
|
|
169
167
|
}
|
|
170
|
-
branch
|
|
168
|
+
branch ||= await getGithubDefaultBranch(repo);
|
|
171
169
|
const res = await readGithubFile(`${repo}/${branch}/${DIST_ROOT}/${plugins_1.PLUGIN_MAIN_FILE}`);
|
|
172
170
|
const pl = (0, plugins_1.parsePluginSource)(repo, res); // use 'repo' as 'id' client-side
|
|
173
171
|
pl.branch = branch;
|
|
@@ -175,13 +173,13 @@ async function readOnlinePlugin(repo, branch = '') {
|
|
|
175
173
|
}
|
|
176
174
|
async function readOnlineCompatiblePlugin(repo, branch = '') {
|
|
177
175
|
const pl = await readOnlinePlugin(repo, branch);
|
|
178
|
-
if (!
|
|
176
|
+
if (!pl?.apiRequired)
|
|
179
177
|
return; // mandatory field
|
|
180
178
|
if (!pl.badApi)
|
|
181
179
|
return pl;
|
|
182
180
|
// we try other branches (starting with 'api')
|
|
183
181
|
const res = await apiGithub('repos/' + repo + '/branches');
|
|
184
|
-
const branches = res.map((x) => x
|
|
182
|
+
const branches = res.map((x) => x?.name)
|
|
185
183
|
.filter((x) => typeof x === 'string' && x.startsWith('api'))
|
|
186
184
|
.sort().reverse();
|
|
187
185
|
for (const branch of branches) {
|
|
@@ -234,7 +232,7 @@ async function* apiGithubPaginated(uri) {
|
|
|
234
232
|
}
|
|
235
233
|
}
|
|
236
234
|
async function isPluginBlacklisted(repo) {
|
|
237
|
-
return (0, exports.getProjectInfo)().then(x =>
|
|
235
|
+
return (0, exports.getProjectInfo)().then(x => x?.repo_blacklist?.[repo]?.message || '', () => undefined);
|
|
238
236
|
}
|
|
239
237
|
async function searchPlugins(text = '', { skipRepos = [''] } = {}) {
|
|
240
238
|
// github doesn't allow complex search, so we have to do it multiple times and merge the results
|
|
@@ -245,7 +243,6 @@ async function searchPlugins(text = '', { skipRepos = [''] } = {}) {
|
|
|
245
243
|
const list = await Promise.all(searches.map(x => (0, misc_1.asyncGeneratorToArray)(apiGithubPaginated(`search/repositories?q=topic:hfs-plugin+${x}`))));
|
|
246
244
|
const deduped = lodash_1.default.uniqBy(list.flat(), x => x.full_name);
|
|
247
245
|
return new misc_1.AsapStream(deduped.map(async (it) => {
|
|
248
|
-
var _a;
|
|
249
246
|
const repo = it.full_name;
|
|
250
247
|
if (skipRepos.includes(repo) || await isPluginBlacklisted(repo))
|
|
251
248
|
return;
|
|
@@ -255,7 +252,7 @@ async function searchPlugins(text = '', { skipRepos = [''] } = {}) {
|
|
|
255
252
|
Object.assign(pl, {
|
|
256
253
|
repo, // overwrite parsed value, that may be wrong
|
|
257
254
|
downloading: exports.downloading[repo],
|
|
258
|
-
license:
|
|
255
|
+
license: it.license?.spdx_id,
|
|
259
256
|
}, lodash_1.default.pick(it, ['pushed_at', 'stargazers_count', 'default_branch']));
|
|
260
257
|
return pl;
|
|
261
258
|
}));
|
|
@@ -273,7 +270,7 @@ exports.getProjectInfo = (0, misc_1.debounceAsync)(() => argv_1.argv.central ===
|
|
|
273
270
|
.then(o => {
|
|
274
271
|
if (o)
|
|
275
272
|
cachedCentralInfo.set(o);
|
|
276
|
-
o
|
|
273
|
+
o ||= { ...cachedCentralInfo.get() || builtIn }; // fall back to built-in
|
|
277
274
|
// merge byVersions info in the main object, but collect alerts separately, to preserve multiple instances
|
|
278
275
|
const allAlerts = [o.alert];
|
|
279
276
|
for (const [ver, more] of Object.entries((0, misc_1.popKey)(o, 'byVersion') || {}))
|
|
@@ -296,4 +293,4 @@ exports.getProjectInfo = (0, misc_1.debounceAsync)(() => argv_1.argv.central ===
|
|
|
296
293
|
(0, plugins_1.enablePlugin)(p.id, false);
|
|
297
294
|
}
|
|
298
295
|
return o;
|
|
299
|
-
}), { retain: misc_1.HOUR, retainFailure:
|
|
296
|
+
}), { retain: misc_1.HOUR, retainFailure: 60_000 });
|
package/src/i18n.js
CHANGED
|
@@ -21,11 +21,10 @@ function i18nFromTranslations(translations, embedded = 'en') {
|
|
|
21
21
|
}
|
|
22
22
|
// If one of the keys is an "id", that should be the first. If one of the keys should work as a fallback, that should be the last. Use 'fallback' parameter if you don't want the fallback to work as a key.
|
|
23
23
|
function t(keyOrTpl, params, fallback) {
|
|
24
|
-
var _a, _b;
|
|
25
24
|
if (!keyOrTpl)
|
|
26
25
|
return '';
|
|
27
26
|
// memoize?
|
|
28
|
-
const keys = isTemplateStringsArray(keyOrTpl) ? [(fallback
|
|
27
|
+
const keys = isTemplateStringsArray(keyOrTpl) ? [(fallback ??= keyOrTpl[0])]
|
|
29
28
|
: Array.isArray(keyOrTpl) ? keyOrTpl : [keyOrTpl];
|
|
30
29
|
if (typeof params === 'string' && !fallback) {
|
|
31
30
|
fallback = params;
|
|
@@ -38,7 +37,7 @@ function i18nFromTranslations(translations, embedded = 'en') {
|
|
|
38
37
|
if (!state.disabled)
|
|
39
38
|
for (const key of keys) {
|
|
40
39
|
for (const lang of searchLangs)
|
|
41
|
-
if (found =
|
|
40
|
+
if (found = state.translations[selectedLang = lang]?.translate?.[key])
|
|
42
41
|
break;
|
|
43
42
|
if (found)
|
|
44
43
|
break;
|
|
@@ -111,7 +110,7 @@ function i18nFromTranslations(translations, embedded = 'en') {
|
|
|
111
110
|
yield [s.slice(ofs), false];
|
|
112
111
|
}
|
|
113
112
|
function isTemplateStringsArray(x) {
|
|
114
|
-
return
|
|
113
|
+
return x?.raw && Array.isArray(x);
|
|
115
114
|
}
|
|
116
115
|
return {
|
|
117
116
|
t,
|
package/src/icons.js
CHANGED
package/src/index.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
5
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
6
|
};
|
|
7
|
-
var _a;
|
|
8
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
8
|
exports.app = void 0;
|
|
10
9
|
const koa_1 = __importDefault(require("koa"));
|
|
@@ -39,7 +38,7 @@ if (new config_1.Version(process.versions.node).olderThan('18.15.0')) {
|
|
|
39
38
|
process.exit(2);
|
|
40
39
|
}
|
|
41
40
|
process.title = 'HFS ' + const_1.VERSION;
|
|
42
|
-
const keys =
|
|
41
|
+
const keys = process.env.COOKIE_SIGN_KEYS?.split(',')
|
|
43
42
|
|| [(0, misc_1.randomId)(30)]; // randomness at start gives some extra security, btu also invalidates existing sessions
|
|
44
43
|
exports.app = new koa_1.default({ keys });
|
|
45
44
|
exports.app.use(middlewares_1.sessionMiddleware)
|
|
@@ -78,7 +77,7 @@ process.on('uncaughtException', (err) => {
|
|
|
78
77
|
try {
|
|
79
78
|
console.error("uncaught:", err);
|
|
80
79
|
}
|
|
81
|
-
catch
|
|
80
|
+
catch { } // in case we are writing to a closed terminal, we may throw with "write eio at afterwritedispatched", causing an infinite loop
|
|
82
81
|
});
|
|
83
82
|
// this warning is scaring users, and has been removed in node 20.12.0 https://github.com/nodejs/node/pull/51204
|
|
84
83
|
const original = process.emitWarning;
|
package/src/lang.js
CHANGED
|
@@ -24,7 +24,7 @@ function code2file(code) {
|
|
|
24
24
|
function file2code(fn) {
|
|
25
25
|
return fn.replace(PREFIX, '').replace(SUFFIX, '');
|
|
26
26
|
}
|
|
27
|
-
const cache = (0, expiringCache_1.expiringCache)(
|
|
27
|
+
const cache = (0, expiringCache_1.expiringCache)(3_000); // 3 seconds for both a good dx and acceptable performance
|
|
28
28
|
async function getLangData(ctxOrLangCsv) {
|
|
29
29
|
if (typeof ctxOrLangCsv !== 'string') {
|
|
30
30
|
const ctx = ctxOrLangCsv;
|
|
@@ -35,7 +35,6 @@ async function getLangData(ctxOrLangCsv) {
|
|
|
35
35
|
}
|
|
36
36
|
const csv = ctxOrLangCsv.toLowerCase();
|
|
37
37
|
return cache.try(csv, async () => {
|
|
38
|
-
var _a;
|
|
39
38
|
const langs = csv.split(',');
|
|
40
39
|
const ret = {};
|
|
41
40
|
let i = 0;
|
|
@@ -47,7 +46,7 @@ async function getLangData(ctxOrLangCsv) {
|
|
|
47
46
|
try {
|
|
48
47
|
ret[k] = JSON.parse(await (0, promises_1.readFile)(fn, 'utf8'));
|
|
49
48
|
} // allow external files to override embedded translations
|
|
50
|
-
catch
|
|
49
|
+
catch {
|
|
51
50
|
if ((0, misc_1.hasProp)(embedded_1.default, k))
|
|
52
51
|
ret[k] = lodash_1.default.cloneDeep(embedded_1.default[k]);
|
|
53
52
|
else {
|
|
@@ -60,9 +59,9 @@ async function getLangData(ctxOrLangCsv) {
|
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
|
-
const fromPlugins = (0, misc_1.onlyTruthy)(await Promise.all((0, plugins_1.mapPlugins)(async (pl) =>
|
|
62
|
+
const fromPlugins = (0, misc_1.onlyTruthy)(await Promise.all((0, plugins_1.mapPlugins)(async (pl) => (0, misc_1.tryJson)(await (0, promises_1.readFile)((0, path_1.join)(pl.folder, fn), 'utf8').catch(() => ''))?.translate)));
|
|
64
63
|
if (fromPlugins.length)
|
|
65
|
-
lodash_1.default.defaults((
|
|
64
|
+
lodash_1.default.defaults((ret[k] ||= {}).translate ||= {}, ...fromPlugins); // be sure we have an entry for k
|
|
66
65
|
i++;
|
|
67
66
|
}
|
|
68
67
|
return ret;
|
|
@@ -71,7 +70,7 @@ async function getLangData(ctxOrLangCsv) {
|
|
|
71
70
|
let forceLangData;
|
|
72
71
|
let undo;
|
|
73
72
|
(0, config_1.defineConfig)(misc_1.CFG.force_lang, '', v => {
|
|
74
|
-
undo
|
|
73
|
+
undo?.();
|
|
75
74
|
if (!v)
|
|
76
75
|
return forceLangData = undefined;
|
|
77
76
|
const translation = embedded_1.default[v];
|
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
|
@@ -43,7 +43,6 @@ const events_1 = require("events");
|
|
|
43
43
|
const config_1 = require("./config");
|
|
44
44
|
const fs_1 = require("fs");
|
|
45
45
|
const util = __importStar(require("util"));
|
|
46
|
-
const promises_1 = require("fs/promises");
|
|
47
46
|
const lodash_1 = __importDefault(require("lodash"));
|
|
48
47
|
const util_files_1 = require("./util-files");
|
|
49
48
|
const auth_1 = require("./auth");
|
|
@@ -55,30 +54,32 @@ const index_1 = require("./index");
|
|
|
55
54
|
const serveGuiFiles_1 = require("./serveGuiFiles");
|
|
56
55
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
57
56
|
class Logger {
|
|
57
|
+
name;
|
|
58
|
+
stream;
|
|
59
|
+
last;
|
|
60
|
+
path = '';
|
|
58
61
|
constructor(name) {
|
|
59
62
|
this.name = name;
|
|
60
|
-
this.path = '';
|
|
61
63
|
}
|
|
62
64
|
async setPath(path) {
|
|
63
|
-
var _a;
|
|
64
65
|
this.path = path;
|
|
65
|
-
|
|
66
|
+
this.stream?.end();
|
|
66
67
|
this.last = undefined;
|
|
67
68
|
if (!path)
|
|
68
69
|
return this.stream = undefined;
|
|
69
70
|
try {
|
|
70
|
-
const stats = await (0,
|
|
71
|
+
const stats = await (0, util_files_1.statWithTimeout)(path);
|
|
71
72
|
this.last = stats.mtime;
|
|
72
73
|
}
|
|
73
|
-
catch
|
|
74
|
-
if (await (0, util_files_1.
|
|
74
|
+
catch {
|
|
75
|
+
if (await (0, util_files_1.ensureParentFolder)(path) === false)
|
|
75
76
|
console.log("cannot create folder for", path);
|
|
76
77
|
}
|
|
77
78
|
this.reopen();
|
|
78
79
|
}
|
|
79
80
|
reopen() {
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
return this.stream = (0, util_files_1.createFileWithPath)(this.path, { flags: 'a' })
|
|
82
|
+
?.on('error', () => this.stream = undefined);
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
// we'll have names same as config keys. These are used also by the get_log api.
|
|
@@ -116,21 +117,20 @@ const logMw = async (ctx, next) => {
|
|
|
116
117
|
const conn = (0, connections_1.getConnection)(ctx); // collect reference before close
|
|
117
118
|
// don't await, as we don't want to hold the middlewares chain
|
|
118
119
|
ctx.state.completed.then(() => {
|
|
119
|
-
var _a, _b, _c, _d;
|
|
120
120
|
if (ctx.state.dontLog || ctx.state.considerAsGui && !serveGuiFiles_1.logGui.get())
|
|
121
121
|
return;
|
|
122
122
|
if (dontLogNet.compiled()(ctx.ip))
|
|
123
123
|
return;
|
|
124
124
|
const isError = ctx.status >= 400;
|
|
125
125
|
const logger = isError && accessErrorLog || accessLogger;
|
|
126
|
-
const rotate =
|
|
126
|
+
const rotate = logRotation.get()?.[0];
|
|
127
127
|
let { stream, last, path } = logger;
|
|
128
128
|
if (!stream)
|
|
129
129
|
return;
|
|
130
130
|
logger.last = now;
|
|
131
131
|
if (rotate && last) { // rotation enabled and a file exists?
|
|
132
132
|
const passed = Number(now) - Number(last)
|
|
133
|
-
-
|
|
133
|
+
- 3600_000; // be pessimistic and count a possible DST change
|
|
134
134
|
if (rotate === 'm' && (passed >= 31 * misc_1.DAY || now.getMonth() !== last.getMonth())
|
|
135
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
|
|
136
136
|
|| rotate === 'w' && (passed >= 7 * misc_1.DAY || now.getDay() < last.getDay())) {
|
|
@@ -150,9 +150,9 @@ const logMw = async (ctx, next) => {
|
|
|
150
150
|
}
|
|
151
151
|
const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s %s\n'; // Apache's Common Log Format
|
|
152
152
|
const a = now.toString().split(' '); // like nginx, our default log contains the time of log writing
|
|
153
|
-
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);
|
|
154
154
|
const user = (0, auth_1.getCurrentUsername)(ctx);
|
|
155
|
-
const length =
|
|
155
|
+
const length = ctx.state.length ?? ctx.length;
|
|
156
156
|
const uri = ctx.originalUrl;
|
|
157
157
|
const duration = (Date.now() - Number(now)) / 1000;
|
|
158
158
|
ctx.logExtra(ctx.vfsNode && {
|
|
@@ -163,7 +163,7 @@ const logMw = async (ctx, next) => {
|
|
|
163
163
|
size: ctx.state.uploadSize,
|
|
164
164
|
speed: Math.round(ctx.state.uploadSize / duration),
|
|
165
165
|
});
|
|
166
|
-
if (conn
|
|
166
|
+
if (conn?.country)
|
|
167
167
|
ctx.logExtra({ country: conn.country });
|
|
168
168
|
if (logUA.get())
|
|
169
169
|
ctx.logExtra({ ua: ctx.get('user-agent') || undefined });
|
|
@@ -171,8 +171,8 @@ const logMw = async (ctx, next) => {
|
|
|
171
171
|
if (events_2.default.anyListener(logger.name)) // small optimization: this event can happen often, while most times there's no listener, and the parameters object is constructed pointlessly. A benchmark measured it 20% faster (just the line), while maybe it was not necessary.
|
|
172
172
|
events_2.default.emit(logger.name, { ctx, length, user, ts: now, uri, extra });
|
|
173
173
|
debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
|
|
174
|
-
(0,
|
|
175
|
-
stream.write(util.format(format, ctx.ip, user || '-', date, ctx.method, uri, ctx.req.httpVersion, ctx.status,
|
|
174
|
+
(0, util_files_1.statWithTimeout)(logger.path).catch(() => logger.reopen())); // async = smoother but we may lose some entries
|
|
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))));
|
|
176
176
|
});
|
|
177
177
|
};
|
|
178
178
|
exports.logMw = logMw;
|
|
@@ -187,7 +187,7 @@ function doubleDigit(n) {
|
|
|
187
187
|
async function getRotatedFiles() {
|
|
188
188
|
return Object.fromEntries(await Promise.all(exports.loggers.map(async (x) => {
|
|
189
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"
|
|
190
|
-
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 }))];
|
|
191
191
|
})));
|
|
192
192
|
}
|
|
193
193
|
// dump console.error to file
|
|
@@ -196,11 +196,11 @@ debugLogFile.once('open', () => {
|
|
|
196
196
|
const was = console.error;
|
|
197
197
|
console.error = function (...args) {
|
|
198
198
|
was.apply(this, args);
|
|
199
|
-
args = args.map(x =>
|
|
199
|
+
args = args.map(x => typeof x === 'string' ? x : ((0, misc_1.tryJson)(x) ?? String(x)));
|
|
200
200
|
debugLogFile.write((0, misc_1.formatTimestamp)(new Date) + ' - ' + args.join(' ') + '\n');
|
|
201
201
|
};
|
|
202
202
|
// limit log size
|
|
203
|
-
const LIMIT =
|
|
203
|
+
const LIMIT = 1_000_000;
|
|
204
204
|
const { path } = debugLogFile;
|
|
205
205
|
(0, misc_1.repeat)(misc_1.DAY, () => {
|
|
206
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)
|