hfs 0.1.6 → 0.26.2
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/LICENSE.txt +674 -0
- package/README.md +102 -8
- package/admin/assets/index.dcc78777.css +1 -0
- package/admin/assets/index.f056db34.js +282 -0
- package/admin/assets/sha512.3c0e384c.js +8 -0
- package/admin/index.html +17 -0
- package/admin/logo.svg +36 -0
- package/frontend/assets/index.55c710c2.js +85 -0
- package/frontend/assets/index.ee805a6c.css +1 -0
- package/frontend/assets/sha512.634b743e.js +8 -0
- package/frontend/fontello.css +77 -0
- package/frontend/fontello.woff2 +0 -0
- package/frontend/index.html +18 -0
- package/package.json +93 -28
- package/plugins/antibrute/plugin.js +38 -0
- package/plugins/download-counter/plugin.js +47 -0
- package/plugins/download-counter/public/hits.js +5 -0
- package/plugins/updater-disabled/plugin.js +44 -0
- package/plugins/vhosting/plugin.js +42 -0
- package/src/QuickZipStream.js +285 -0
- package/src/ThrottledStream.js +93 -0
- package/src/adminApis.js +169 -0
- package/src/api.accounts.js +59 -0
- package/src/api.auth.js +130 -0
- package/src/api.file_list.js +103 -0
- package/src/api.helpers.js +32 -0
- package/src/api.monitor.js +102 -0
- package/src/api.plugins.js +125 -0
- package/src/api.vfs.js +164 -0
- package/src/apiMiddleware.js +136 -0
- package/src/block.js +33 -0
- package/src/commands.js +105 -0
- package/src/config.js +172 -0
- package/src/connections.js +57 -0
- package/src/const.js +83 -0
- package/src/crypt.js +21 -0
- package/src/debounceAsync.js +48 -0
- package/src/events.js +9 -0
- package/src/frontEndApis.js +38 -0
- package/src/github.js +102 -0
- package/src/index.js +53 -0
- package/src/listen.js +226 -0
- package/src/log.js +137 -0
- package/src/middlewares.js +154 -0
- package/src/misc.js +160 -0
- package/src/pbkdf2.js +74 -0
- package/src/perm.js +176 -0
- package/src/plugins.js +338 -0
- package/src/serveFile.js +104 -0
- package/src/serveGuiFiles.js +113 -0
- package/src/sse.js +29 -0
- package/src/throttler.js +91 -0
- package/src/update.js +69 -0
- package/src/util-files.js +141 -0
- package/src/util-generators.js +30 -0
- package/src/util-http.js +30 -0
- package/src/vfs.js +227 -0
- package/src/watchLoad.js +73 -0
- package/src/zip.js +69 -0
- package/.npmignore +0 -19
- package/admin-server.js +0 -212
- package/cli.js +0 -33
- package/file-server.js +0 -100
- package/lib/common.js +0 -10
- package/lib/extending.js +0 -158
- package/lib/mime.js +0 -19
- package/lib/misc.js +0 -75
- package/lib/serving.js +0 -81
- package/lib/vfs.js +0 -403
- package/main.js +0 -24
- package/note.txt +0 -104
- package/speedtest.js +0 -21
- package/static/backend.css +0 -14
- package/static/backend.html +0 -32
- package/static/backend.js +0 -694
- package/static/extending.js +0 -187
- package/static/frontend.css +0 -29
- package/static/frontend.html +0 -23
- package/static/frontend.js +0 -230
- package/static/icons/files/archive.png +0 -0
- package/static/icons/files/audio.png +0 -0
- package/static/icons/files/file.png +0 -0
- package/static/icons/files/folder.png +0 -0
- package/static/icons/files/image.png +0 -0
- package/static/icons/files/link.png +0 -0
- package/static/icons/files/video.png +0 -0
- package/static/jquery.js +0 -4
- package/static/jquery.rule-1.0.2.js +0 -273
- package/static/misc.js +0 -194
- package/static/tpl.js +0 -17
- package/todo.txt +0 -25
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.prepareState = exports.getProxyDetected = exports.someSecurity = exports.serveGuiAndSharedFiles = exports.sessions = exports.headRequests = exports.gzipper = void 0;
|
|
8
|
+
const koa_compress_1 = __importDefault(require("koa-compress"));
|
|
9
|
+
const koa_session_1 = __importDefault(require("koa-session"));
|
|
10
|
+
const const_1 = require("./const");
|
|
11
|
+
const const_2 = require("./const");
|
|
12
|
+
const vfs_1 = require("./vfs");
|
|
13
|
+
const misc_1 = require("./misc");
|
|
14
|
+
const zip_1 = require("./zip");
|
|
15
|
+
const serveFile_1 = require("./serveFile");
|
|
16
|
+
const serveGuiFiles_1 = require("./serveGuiFiles");
|
|
17
|
+
const koa_mount_1 = __importDefault(require("koa-mount"));
|
|
18
|
+
const stream_1 = require("stream");
|
|
19
|
+
const block_1 = require("./block");
|
|
20
|
+
const perm_1 = require("./perm");
|
|
21
|
+
const connections_1 = require("./connections");
|
|
22
|
+
const basic_auth_1 = __importDefault(require("basic-auth"));
|
|
23
|
+
const tssrp6a_1 = require("tssrp6a");
|
|
24
|
+
const api_auth_1 = require("./api.auth");
|
|
25
|
+
exports.gzipper = (0, koa_compress_1.default)({
|
|
26
|
+
threshold: 2048,
|
|
27
|
+
gzip: { flush: require('zlib').constants.Z_SYNC_FLUSH },
|
|
28
|
+
deflate: { flush: require('zlib').constants.Z_SYNC_FLUSH },
|
|
29
|
+
br: false,
|
|
30
|
+
filter(type) {
|
|
31
|
+
return /text|javascript|style/i.test(type);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const headRequests = async (ctx, next) => {
|
|
35
|
+
const head = ctx.method === 'HEAD';
|
|
36
|
+
if (head)
|
|
37
|
+
ctx.method = 'GET'; // let other middlewares work, so we can collect the size at the end
|
|
38
|
+
await next();
|
|
39
|
+
if (!head || ctx.body === undefined)
|
|
40
|
+
return;
|
|
41
|
+
const { length, status } = ctx.response;
|
|
42
|
+
if (ctx.body)
|
|
43
|
+
ctx.body = stream_1.Readable.from(''); // empty the body for this is a HEAD request. Using Readable avoids koa from trying to set length to 0
|
|
44
|
+
ctx.status = status;
|
|
45
|
+
if (length)
|
|
46
|
+
ctx.response.length = length;
|
|
47
|
+
};
|
|
48
|
+
exports.headRequests = headRequests;
|
|
49
|
+
const sessions = (app) => (0, koa_session_1.default)({
|
|
50
|
+
key: 'hfs_$id',
|
|
51
|
+
signed: true,
|
|
52
|
+
rolling: true,
|
|
53
|
+
maxAge: const_1.SESSION_DURATION,
|
|
54
|
+
}, app);
|
|
55
|
+
exports.sessions = sessions;
|
|
56
|
+
const serveFrontendFiles = (0, serveGuiFiles_1.serveGuiFiles)(process.env.FRONTEND_PROXY, const_2.FRONTEND_URI);
|
|
57
|
+
const serveFrontendPrefixed = (0, koa_mount_1.default)(const_2.FRONTEND_URI.slice(0, -1), serveFrontendFiles);
|
|
58
|
+
const serveAdminPrefixed = (0, koa_mount_1.default)(const_1.ADMIN_URI.slice(0, -1), (0, serveGuiFiles_1.serveGuiFiles)(process.env.ADMIN_PROXY, const_1.ADMIN_URI));
|
|
59
|
+
const serveGuiAndSharedFiles = async (ctx, next) => {
|
|
60
|
+
const { path } = ctx;
|
|
61
|
+
if (ctx.body)
|
|
62
|
+
return next();
|
|
63
|
+
if (path.startsWith(const_2.FRONTEND_URI))
|
|
64
|
+
return serveFrontendPrefixed(ctx, next);
|
|
65
|
+
if (path + '/' === const_1.ADMIN_URI)
|
|
66
|
+
return ctx.redirect(const_1.ADMIN_URI);
|
|
67
|
+
if (path.startsWith(const_1.ADMIN_URI))
|
|
68
|
+
return serveAdminPrefixed(ctx, next);
|
|
69
|
+
const node = await (0, vfs_1.urlToNode)(path, ctx);
|
|
70
|
+
if (!node)
|
|
71
|
+
return ctx.status = 404;
|
|
72
|
+
const canRead = (0, vfs_1.hasPermission)(node, 'can_read', ctx);
|
|
73
|
+
const isFolder = await (0, vfs_1.nodeIsDirectory)(node);
|
|
74
|
+
if (isFolder && !path.endsWith('/'))
|
|
75
|
+
return ctx.redirect(path + '/');
|
|
76
|
+
if (canRead && !isFolder)
|
|
77
|
+
return node.source ? (0, serveFile_1.serveFileNode)(node)(ctx, next)
|
|
78
|
+
: next();
|
|
79
|
+
if (!canRead) {
|
|
80
|
+
ctx.status = (0, vfs_1.cantReadStatusCode)(node);
|
|
81
|
+
if (ctx.status === const_1.FORBIDDEN)
|
|
82
|
+
return;
|
|
83
|
+
const browserDetected = ctx.get('Upgrade-Insecure-Requests') || ctx.get('Sec-Fetch-Mode'); // ugh, heuristics
|
|
84
|
+
if (!browserDetected) // we don't want to trigger basic authentication on browsers, it's meant for download managers only
|
|
85
|
+
ctx.set('WWW-Authenticate', 'Basic'); // we support basic authentication
|
|
86
|
+
ctx.state.serveApp = true;
|
|
87
|
+
return serveFrontendFiles(ctx, next);
|
|
88
|
+
}
|
|
89
|
+
ctx.set({ server: 'HFS ' + const_1.BUILD_TIMESTAMP });
|
|
90
|
+
const { get } = ctx.query;
|
|
91
|
+
if (get === 'zip')
|
|
92
|
+
return await (0, zip_1.zipStreamFromFolder)(node, ctx);
|
|
93
|
+
if (node.default) {
|
|
94
|
+
const def = await (0, vfs_1.urlToNode)(path + node.default, ctx);
|
|
95
|
+
return !def ? next()
|
|
96
|
+
: (0, vfs_1.hasPermission)(def, 'can_read', ctx) ? (0, serveFile_1.serveFileNode)(def)(ctx, next)
|
|
97
|
+
: ctx.status = (0, vfs_1.cantReadStatusCode)(def);
|
|
98
|
+
}
|
|
99
|
+
return serveFrontendFiles(ctx, next);
|
|
100
|
+
};
|
|
101
|
+
exports.serveGuiAndSharedFiles = serveGuiAndSharedFiles;
|
|
102
|
+
let proxyDetected = false;
|
|
103
|
+
const someSecurity = async (ctx, next) => {
|
|
104
|
+
ctx.request.ip = (0, connections_1.normalizeIp)(ctx.ip);
|
|
105
|
+
try {
|
|
106
|
+
let proxy = ctx.get('X-Forwarded-For');
|
|
107
|
+
// we have some dev-proxies to ignore
|
|
108
|
+
if (const_1.DEV && proxy && [process.env.FRONTEND_PROXY, process.env.ADMIN_PROXY].includes(ctx.get('X-Forwarded-port')))
|
|
109
|
+
proxy = '';
|
|
110
|
+
if ((0, misc_1.dirTraversal)(decodeURI(ctx.path)))
|
|
111
|
+
return ctx.status = 418;
|
|
112
|
+
if ((0, block_1.applyBlock)(ctx.socket, ctx.ip))
|
|
113
|
+
return;
|
|
114
|
+
proxyDetected || (proxyDetected = proxy > '');
|
|
115
|
+
ctx.state.proxiedFor = proxy;
|
|
116
|
+
}
|
|
117
|
+
catch (_a) {
|
|
118
|
+
return ctx.status = 418;
|
|
119
|
+
}
|
|
120
|
+
return next();
|
|
121
|
+
};
|
|
122
|
+
exports.someSecurity = someSecurity;
|
|
123
|
+
// this is only about http proxies
|
|
124
|
+
function getProxyDetected() {
|
|
125
|
+
return proxyDetected;
|
|
126
|
+
}
|
|
127
|
+
exports.getProxyDetected = getProxyDetected;
|
|
128
|
+
const prepareState = async (ctx, next) => {
|
|
129
|
+
var _a;
|
|
130
|
+
// calculate these once and for all
|
|
131
|
+
ctx.state.account = (_a = await getHttpAccount(ctx)) !== null && _a !== void 0 ? _a : (0, perm_1.getAccount)((0, perm_1.getCurrentUsername)(ctx));
|
|
132
|
+
const conn = ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
|
|
133
|
+
await next();
|
|
134
|
+
if (conn)
|
|
135
|
+
(0, connections_1.updateConnection)(conn, { ctx });
|
|
136
|
+
};
|
|
137
|
+
exports.prepareState = prepareState;
|
|
138
|
+
async function getHttpAccount(ctx) {
|
|
139
|
+
const credentials = (0, basic_auth_1.default)(ctx.req);
|
|
140
|
+
const account = (0, perm_1.getAccount)((credentials === null || credentials === void 0 ? void 0 : credentials.name) || '');
|
|
141
|
+
if (account && await srpCheck(account.username, credentials.pass))
|
|
142
|
+
return account;
|
|
143
|
+
}
|
|
144
|
+
async function srpCheck(username, password) {
|
|
145
|
+
username = username.toLocaleLowerCase();
|
|
146
|
+
const account = (0, perm_1.getAccount)(username);
|
|
147
|
+
if (!(account === null || account === void 0 ? void 0 : account.srp) || !password)
|
|
148
|
+
return false;
|
|
149
|
+
const { step1, salt, pubKey } = await (0, api_auth_1.srpStep1)(account);
|
|
150
|
+
const client = new tssrp6a_1.SRPClientSession(new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters()));
|
|
151
|
+
const clientRes1 = await client.step1(username, password);
|
|
152
|
+
const clientRes2 = await clientRes1.step2(BigInt(salt), BigInt(pubKey));
|
|
153
|
+
return await step1.step2(clientRes2.A, clientRes2.M1).then(() => true, () => false);
|
|
154
|
+
}
|
package/src/misc.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
15
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
16
|
+
};
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.tryJson = exports.same = exports.isLocalHost = exports.with_ = exports.typedKeys = exports.objRenameKey = exports.onOff = exports.pendingPromise = exports.onlyTruthy = exports.truthy = exports.pattern2filter = exports.onFirstEvent = exports.onProcessExit = exports.randomId = exports.getOrSet = exports.wantArray = exports.wait = exports.objSameKeys = exports.setHidden = exports.prefix = exports.enforceFinal = exports.debounceAsync = void 0;
|
|
22
|
+
const path_1 = require("path");
|
|
23
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
24
|
+
const assert_1 = __importDefault(require("assert"));
|
|
25
|
+
__exportStar(require("./util-http"), exports);
|
|
26
|
+
__exportStar(require("./util-generators"), exports);
|
|
27
|
+
__exportStar(require("./util-files"), exports);
|
|
28
|
+
const debounceAsync_1 = __importDefault(require("./debounceAsync"));
|
|
29
|
+
exports.debounceAsync = debounceAsync_1.default;
|
|
30
|
+
function enforceFinal(sub, s) {
|
|
31
|
+
return s.endsWith(sub) ? s : s + sub;
|
|
32
|
+
}
|
|
33
|
+
exports.enforceFinal = enforceFinal;
|
|
34
|
+
function prefix(pre, v, post = '') {
|
|
35
|
+
return v ? pre + v + post : '';
|
|
36
|
+
}
|
|
37
|
+
exports.prefix = prefix;
|
|
38
|
+
function setHidden(dest, src) {
|
|
39
|
+
return Object.defineProperties(dest, objSameKeys(src, value => ({
|
|
40
|
+
enumerable: false,
|
|
41
|
+
writable: true,
|
|
42
|
+
value,
|
|
43
|
+
})));
|
|
44
|
+
}
|
|
45
|
+
exports.setHidden = setHidden;
|
|
46
|
+
function objSameKeys(src, newValue) {
|
|
47
|
+
return Object.fromEntries(Object.entries(src).map(([k, v]) => [k, newValue(v, k)]));
|
|
48
|
+
}
|
|
49
|
+
exports.objSameKeys = objSameKeys;
|
|
50
|
+
function wait(ms) {
|
|
51
|
+
return new Promise(res => setTimeout(res, ms));
|
|
52
|
+
}
|
|
53
|
+
exports.wait = wait;
|
|
54
|
+
function wantArray(x) {
|
|
55
|
+
return x == null ? [] : Array.isArray(x) ? x : [x];
|
|
56
|
+
}
|
|
57
|
+
exports.wantArray = wantArray;
|
|
58
|
+
function getOrSet(o, k, creator) {
|
|
59
|
+
return k in o ? o[k]
|
|
60
|
+
: (o[k] = creator());
|
|
61
|
+
}
|
|
62
|
+
exports.getOrSet = getOrSet;
|
|
63
|
+
function randomId(len = 10) {
|
|
64
|
+
// 10 chars is 51+bits, the max we can give. 8 is 41+bits
|
|
65
|
+
if (len > 10)
|
|
66
|
+
throw Error('bad length');
|
|
67
|
+
return Math.random()
|
|
68
|
+
.toString(36)
|
|
69
|
+
.substring(2, 2 + len)
|
|
70
|
+
.replace(/l/g, 'L'); // avoid confusion reading l1
|
|
71
|
+
}
|
|
72
|
+
exports.randomId = randomId;
|
|
73
|
+
const cbs = new Set();
|
|
74
|
+
function onProcessExit(cb) {
|
|
75
|
+
cbs.add(cb);
|
|
76
|
+
return () => cbs.delete(cb);
|
|
77
|
+
}
|
|
78
|
+
exports.onProcessExit = onProcessExit;
|
|
79
|
+
onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], signal => Promise.allSettled(Array.from(cbs).map(cb => cb(signal))).then(() => process.exit(0)));
|
|
80
|
+
function onFirstEvent(emitter, events, cb) {
|
|
81
|
+
let already = false;
|
|
82
|
+
for (const e of events)
|
|
83
|
+
emitter.on(e, (...args) => {
|
|
84
|
+
if (already)
|
|
85
|
+
return;
|
|
86
|
+
already = true;
|
|
87
|
+
cb(...args);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
exports.onFirstEvent = onFirstEvent;
|
|
91
|
+
function pattern2filter(pattern) {
|
|
92
|
+
const re = new RegExp(lodash_1.default.escapeRegExp(pattern), 'i');
|
|
93
|
+
return (s) => !s || !pattern || re.test((0, path_1.basename)(s));
|
|
94
|
+
}
|
|
95
|
+
exports.pattern2filter = pattern2filter;
|
|
96
|
+
function truthy(value) {
|
|
97
|
+
return Boolean(value);
|
|
98
|
+
}
|
|
99
|
+
exports.truthy = truthy;
|
|
100
|
+
function onlyTruthy(arr) {
|
|
101
|
+
return arr.filter(truthy);
|
|
102
|
+
}
|
|
103
|
+
exports.onlyTruthy = onlyTruthy;
|
|
104
|
+
function pendingPromise() {
|
|
105
|
+
let takeOut;
|
|
106
|
+
const ret = new Promise((resolve, reject) => takeOut = { resolve, reject });
|
|
107
|
+
return Object.assign(ret, takeOut);
|
|
108
|
+
}
|
|
109
|
+
exports.pendingPromise = pendingPromise;
|
|
110
|
+
// install multiple handlers and returns a handy 'uninstall' function which requires no parameter. Pass a map {event:handler}
|
|
111
|
+
function onOff(em, events) {
|
|
112
|
+
events = { ...events }; // avoid later modifications, as we need this later for uninstallation
|
|
113
|
+
for (const [k, cb] of Object.entries(events))
|
|
114
|
+
for (const e of k.split(' '))
|
|
115
|
+
em.on(e, cb);
|
|
116
|
+
return () => {
|
|
117
|
+
for (const [k, cb] of Object.entries(events))
|
|
118
|
+
for (const e of k.split(' '))
|
|
119
|
+
em.off(e, cb);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
exports.onOff = onOff;
|
|
123
|
+
function objRenameKey(o, from, to) {
|
|
124
|
+
if (!o || !o.hasOwnProperty(from) || from === to)
|
|
125
|
+
return;
|
|
126
|
+
o[to] = o[from];
|
|
127
|
+
delete o[from];
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
exports.objRenameKey = objRenameKey;
|
|
131
|
+
function typedKeys(o) {
|
|
132
|
+
return Object.keys(o);
|
|
133
|
+
}
|
|
134
|
+
exports.typedKeys = typedKeys;
|
|
135
|
+
function with_(par, cb) {
|
|
136
|
+
return cb(par);
|
|
137
|
+
}
|
|
138
|
+
exports.with_ = with_;
|
|
139
|
+
function isLocalHost(c) {
|
|
140
|
+
const ip = c.socket.remoteAddress; // don't use Context.ip as it is subject to proxied ips, and that's no use for localhost detection
|
|
141
|
+
return ip && (ip === '::1' || ip.endsWith('127.0.0.1'));
|
|
142
|
+
}
|
|
143
|
+
exports.isLocalHost = isLocalHost;
|
|
144
|
+
function same(a, b) {
|
|
145
|
+
try {
|
|
146
|
+
assert_1.default.deepStrictEqual(a, b);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
catch (_a) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.same = same;
|
|
154
|
+
function tryJson(s) {
|
|
155
|
+
try {
|
|
156
|
+
return s && JSON.parse(s);
|
|
157
|
+
}
|
|
158
|
+
catch (_a) { }
|
|
159
|
+
}
|
|
160
|
+
exports.tryJson = tryJson;
|
package/src/pbkdf2.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pbkdf2Verify = exports.pbkdf2 = void 0;
|
|
4
|
+
// @ts-nocheck
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
// FROM https://gist.github.com/chrisveness/770ee96945ec12ac84f134bf538d89fb
|
|
7
|
+
/**
|
|
8
|
+
* Returns PBKDF2 derived key from supplied password.
|
|
9
|
+
*
|
|
10
|
+
* Stored key can subsequently be used to verify that a password matches the original password used
|
|
11
|
+
* to derive the key, using pbkdf2Verify().
|
|
12
|
+
*
|
|
13
|
+
* @param {String} password - Password to be hashed using key derivation function.
|
|
14
|
+
* @param {Number} [iterations=1e6] - Number of iterations of HMAC function to apply.
|
|
15
|
+
* @returns {String} Derived key as base64 string.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const key = await pbkdf2('pāşšŵōřđ'); // eg 'djAxBRKXWNWPyXgpKWHld8SWJA9CQFmLyMbNet7Rle5RLKJAkBCllLfM6tPFa7bAis0lSTiB'
|
|
19
|
+
*/
|
|
20
|
+
async function pbkdf2(password, iterations = 1e6) {
|
|
21
|
+
const pwUtf8 = new TextEncoder().encode(password); // encode pw as UTF-8
|
|
22
|
+
const pwKey = await node_crypto_1.webcrypto.subtle.importKey('raw', pwUtf8, 'PBKDF2', false, ['deriveBits']); // create pw key
|
|
23
|
+
const saltUint8 = node_crypto_1.webcrypto.getRandomValues(new Uint8Array(16)); // get random salt
|
|
24
|
+
const params = { name: 'PBKDF2', hash: 'SHA-256', salt: saltUint8, iterations: iterations }; // pbkdf2 params
|
|
25
|
+
const keyBuffer = await node_crypto_1.webcrypto.subtle.deriveBits(params, pwKey, 256); // derive key
|
|
26
|
+
const keyArray = Array.from(new Uint8Array(keyBuffer)); // key as byte array
|
|
27
|
+
const saltArray = Array.from(new Uint8Array(saltUint8)); // salt as byte array
|
|
28
|
+
const iterHex = ('000000' + iterations.toString(16)).slice(-6); // iter’n count as hex
|
|
29
|
+
const iterArray = iterHex.match(/.{2}/g).map(byte => parseInt(byte, 16)); // iter’ns as byte array
|
|
30
|
+
const compositeArray = [].concat(saltArray, iterArray, keyArray); // combined array
|
|
31
|
+
const compositeStr = compositeArray.map(byte => String.fromCharCode(byte)).join(''); // combined as string
|
|
32
|
+
// encode as base64
|
|
33
|
+
return btoa('v01' + compositeStr); // return composite key
|
|
34
|
+
}
|
|
35
|
+
exports.pbkdf2 = pbkdf2;
|
|
36
|
+
/**
|
|
37
|
+
* Verifies whether the supplied password matches the password previously used to generate the key.
|
|
38
|
+
*
|
|
39
|
+
* @param {String} key - Key previously generated with pbkdf2().
|
|
40
|
+
* @param {String} password - Password to be matched against previously derived key.
|
|
41
|
+
* @returns {boolean} Whether password matches key.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const match = await pbkdf2Verify(key, 'pāşšŵōřđ'); // true
|
|
45
|
+
*/
|
|
46
|
+
async function pbkdf2Verify(key, password) {
|
|
47
|
+
let compositeStr = null; // composite key is salt, iteration count, and derived key
|
|
48
|
+
try {
|
|
49
|
+
compositeStr = atob(key);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
throw new Error('Invalid key');
|
|
53
|
+
} // decode from base64
|
|
54
|
+
const version = compositeStr.slice(0, 3); // 3 bytes
|
|
55
|
+
const saltStr = compositeStr.slice(3, 19); // 16 bytes (128 bits)
|
|
56
|
+
const iterStr = compositeStr.slice(19, 22); // 3 bytes
|
|
57
|
+
const keyStr = compositeStr.slice(22, 54); // 32 bytes (256 bits)
|
|
58
|
+
if (version !== 'v01')
|
|
59
|
+
throw new Error('Invalid key');
|
|
60
|
+
// -- recover salt & iterations from stored (composite) key
|
|
61
|
+
const saltUint8 = new Uint8Array(saltStr.match(/./g).map(ch => ch.charCodeAt(0))); // salt as Uint8Array
|
|
62
|
+
// note: cannot use TextEncoder().encode(saltStr) as it generates UTF-8
|
|
63
|
+
const iterHex = iterStr.match(/./g).map(ch => ch.charCodeAt(0).toString(16)).join(''); // iter’n count as hex
|
|
64
|
+
const iterations = parseInt(iterHex, 16); // iter’ns
|
|
65
|
+
// -- generate new key from stored salt & iterations and supplied password
|
|
66
|
+
const pwUtf8 = new TextEncoder().encode(password); // encode pw as UTF-8
|
|
67
|
+
const pwKey = await node_crypto_1.webcrypto.subtle.importKey('raw', pwUtf8, 'PBKDF2', false, ['deriveBits']); // create pw key
|
|
68
|
+
const params = { name: 'PBKDF2', hash: 'SHA-256', salt: saltUint8, iterations: iterations }; // pbkdf params
|
|
69
|
+
const keyBuffer = await node_crypto_1.webcrypto.subtle.deriveBits(params, pwKey, 256); // derive key
|
|
70
|
+
const keyArray = Array.from(new Uint8Array(keyBuffer)); // key as byte array
|
|
71
|
+
const keyStrNew = keyArray.map(byte => String.fromCharCode(byte)).join(''); // key as string
|
|
72
|
+
return keyStrNew === keyStr; // test if newly generated key matches stored key
|
|
73
|
+
}
|
|
74
|
+
exports.pbkdf2Verify = pbkdf2Verify;
|
package/src/perm.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.anyAccountCanLoginAdmin = exports.accountCanLoginAdmin = exports.accountCanLogin = exports.accountHasPassword = exports.getFromAccount = exports.delAccount = exports.setAccount = exports.addAccount = exports.renameAccount = exports.updateAccount = exports.allowClearTextLogin = exports.saveSrpInfo = exports.getAccount = exports.getCurrentUsernameExpanded = exports.getCurrentUsername = exports.getAccounts = void 0;
|
|
8
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
9
|
+
const crypt_1 = require("./crypt");
|
|
10
|
+
const misc_1 = require("./misc");
|
|
11
|
+
const config_1 = require("./config");
|
|
12
|
+
const tssrp6a_1 = require("tssrp6a");
|
|
13
|
+
const events_1 = __importDefault(require("./events"));
|
|
14
|
+
let accounts = {};
|
|
15
|
+
function getAccounts() {
|
|
16
|
+
return accounts;
|
|
17
|
+
}
|
|
18
|
+
exports.getAccounts = getAccounts;
|
|
19
|
+
function getCurrentUsername(ctx) {
|
|
20
|
+
var _a, _b;
|
|
21
|
+
return ((_a = ctx.state.account) === null || _a === void 0 ? void 0 : _a.username) || ((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username) || '';
|
|
22
|
+
}
|
|
23
|
+
exports.getCurrentUsername = getCurrentUsername;
|
|
24
|
+
// provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
|
|
25
|
+
function getCurrentUsernameExpanded(ctx) {
|
|
26
|
+
const who = getCurrentUsername(ctx);
|
|
27
|
+
if (!who)
|
|
28
|
+
return [];
|
|
29
|
+
const ret = [who];
|
|
30
|
+
for (const u of ret) {
|
|
31
|
+
const a = getAccount(u);
|
|
32
|
+
if (a === null || a === void 0 ? void 0 : a.belongs)
|
|
33
|
+
ret.push(...a.belongs);
|
|
34
|
+
}
|
|
35
|
+
return ret;
|
|
36
|
+
}
|
|
37
|
+
exports.getCurrentUsernameExpanded = getCurrentUsernameExpanded;
|
|
38
|
+
function getAccount(username) {
|
|
39
|
+
return username ? accounts[username] : undefined;
|
|
40
|
+
}
|
|
41
|
+
exports.getAccount = getAccount;
|
|
42
|
+
function saveSrpInfo(account, salt, verifier) {
|
|
43
|
+
account.srp = String(salt) + '|' + String(verifier);
|
|
44
|
+
}
|
|
45
|
+
exports.saveSrpInfo = saveSrpInfo;
|
|
46
|
+
exports.allowClearTextLogin = (0, config_1.defineConfig)('allow_clear_text_login');
|
|
47
|
+
const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
|
|
48
|
+
async function updateAccount(account, changer) {
|
|
49
|
+
const was = JSON.stringify(account);
|
|
50
|
+
await (changer === null || changer === void 0 ? void 0 : changer(account));
|
|
51
|
+
const { username } = account;
|
|
52
|
+
if (account.password) {
|
|
53
|
+
console.debug('hashing password for', username);
|
|
54
|
+
if (exports.allowClearTextLogin.get())
|
|
55
|
+
account.hashed_password = await (0, crypt_1.hashPassword)(account.password);
|
|
56
|
+
const res = await (0, tssrp6a_1.createVerifierAndSalt)(srp6aNimbusRoutines, username, account.password);
|
|
57
|
+
saveSrpInfo(account, res.s, res.v);
|
|
58
|
+
delete account.password;
|
|
59
|
+
}
|
|
60
|
+
else if (!account.srp && account.hashed_password) {
|
|
61
|
+
console.log('please reset password for account', username);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (account.belongs)
|
|
65
|
+
account.belongs = (0, misc_1.wantArray)(account.belongs).filter(b => b in accounts // at this stage the group record may still be null if specified later in the file
|
|
66
|
+
|| console.error(`account ${username} belongs to non-existing ${b}`));
|
|
67
|
+
if (was !== JSON.stringify(account))
|
|
68
|
+
saveAccountsAsap();
|
|
69
|
+
}
|
|
70
|
+
exports.updateAccount = updateAccount;
|
|
71
|
+
const saveAccountsAsap = config_1.saveConfigAsap;
|
|
72
|
+
const accountsConfig = (0, config_1.defineConfig)('accounts', {});
|
|
73
|
+
accountsConfig.sub(async (v) => {
|
|
74
|
+
// we should validate content here
|
|
75
|
+
accounts = v; // keep local reference
|
|
76
|
+
await Promise.all(lodash_1.default.map(accounts, async (rec, k) => {
|
|
77
|
+
const norm = normalizeUsername(k);
|
|
78
|
+
if (!rec) // an empty object in yaml is stored as null
|
|
79
|
+
rec = accounts[norm] = { username: norm };
|
|
80
|
+
else
|
|
81
|
+
(0, misc_1.objRenameKey)(accounts, k, norm);
|
|
82
|
+
(0, misc_1.setHidden)(rec, { username: norm });
|
|
83
|
+
await updateAccount(rec);
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
function normalizeUsername(username) {
|
|
87
|
+
return username.toLocaleLowerCase();
|
|
88
|
+
}
|
|
89
|
+
function renameAccount(from, to) {
|
|
90
|
+
from = normalizeUsername(from);
|
|
91
|
+
to = normalizeUsername(to);
|
|
92
|
+
if (!to || !accounts[from] || accounts[to])
|
|
93
|
+
return false;
|
|
94
|
+
if (to === from)
|
|
95
|
+
return true;
|
|
96
|
+
(0, misc_1.objRenameKey)(accounts, from, to);
|
|
97
|
+
updateReferences();
|
|
98
|
+
saveAccountsAsap();
|
|
99
|
+
return true;
|
|
100
|
+
function updateReferences() {
|
|
101
|
+
var _a;
|
|
102
|
+
(0, misc_1.setHidden)(accounts[to], { username: to });
|
|
103
|
+
for (const a of Object.values(accounts)) {
|
|
104
|
+
const idx = (_a = a.belongs) === null || _a === void 0 ? void 0 : _a.indexOf(from);
|
|
105
|
+
if (idx !== undefined && idx >= 0)
|
|
106
|
+
a.belongs[idx] = to;
|
|
107
|
+
}
|
|
108
|
+
events_1.default.emit('accountRenamed', from, to); // everybody, take care of your stuff
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.renameAccount = renameAccount;
|
|
112
|
+
// we consider all the following fields, when falsy, as equivalent to be missing. If this changes in the future, please adjust addAccount and setAccount
|
|
113
|
+
const assignableProps = ['redirect', 'ignore_limits', 'belongs', 'admin'];
|
|
114
|
+
function addAccount(username, props) {
|
|
115
|
+
if (!username || accounts[username])
|
|
116
|
+
return;
|
|
117
|
+
const copy = (0, misc_1.setHidden)(lodash_1.default.pickBy(lodash_1.default.pick(props, assignableProps), Boolean), { username }); // have the field in the object but hidden so that stringification won't include it
|
|
118
|
+
accountsConfig.set(accounts => Object.assign(accounts, { [username]: copy }));
|
|
119
|
+
saveAccountsAsap().then();
|
|
120
|
+
return copy;
|
|
121
|
+
}
|
|
122
|
+
exports.addAccount = addAccount;
|
|
123
|
+
function setAccount(username, changes) {
|
|
124
|
+
const acc = getAccount(username);
|
|
125
|
+
if (!acc)
|
|
126
|
+
return false;
|
|
127
|
+
const rest = lodash_1.default.pick(changes, assignableProps);
|
|
128
|
+
for (const [k, v] of Object.entries(rest))
|
|
129
|
+
if (!v)
|
|
130
|
+
rest[k] = undefined;
|
|
131
|
+
Object.assign(acc, rest);
|
|
132
|
+
if (changes.username)
|
|
133
|
+
renameAccount(username, changes.username);
|
|
134
|
+
saveAccountsAsap().then();
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
exports.setAccount = setAccount;
|
|
138
|
+
function delAccount(username) {
|
|
139
|
+
if (!getAccount(username))
|
|
140
|
+
return false;
|
|
141
|
+
accountsConfig.set(accounts => Object.assign(accounts, { [username]: undefined }));
|
|
142
|
+
saveAccountsAsap().then();
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
exports.delAccount = delAccount;
|
|
146
|
+
// get some property from account, searching in its groups if necessary. Search is breadth-first, and this determines priority of inheritance.
|
|
147
|
+
function getFromAccount(account, getter) {
|
|
148
|
+
const search = [account];
|
|
149
|
+
for (const accountOrUsername of search) {
|
|
150
|
+
const a = typeof accountOrUsername === 'string' ? getAccount(accountOrUsername) : accountOrUsername;
|
|
151
|
+
if (!a)
|
|
152
|
+
continue;
|
|
153
|
+
const res = getter(a);
|
|
154
|
+
if (res !== undefined)
|
|
155
|
+
return res;
|
|
156
|
+
if (a.belongs)
|
|
157
|
+
search.push(...a.belongs);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.getFromAccount = getFromAccount;
|
|
161
|
+
function accountHasPassword(account) {
|
|
162
|
+
return Boolean(account.password || account.hashed_password || account.srp);
|
|
163
|
+
}
|
|
164
|
+
exports.accountHasPassword = accountHasPassword;
|
|
165
|
+
function accountCanLogin(account) {
|
|
166
|
+
return accountHasPassword(account);
|
|
167
|
+
}
|
|
168
|
+
exports.accountCanLogin = accountCanLogin;
|
|
169
|
+
function accountCanLoginAdmin(account) {
|
|
170
|
+
return accountCanLogin(account) && getFromAccount(account, a => a.admin);
|
|
171
|
+
}
|
|
172
|
+
exports.accountCanLoginAdmin = accountCanLoginAdmin;
|
|
173
|
+
function anyAccountCanLoginAdmin() {
|
|
174
|
+
return Object.values(accounts).find(accountCanLoginAdmin);
|
|
175
|
+
}
|
|
176
|
+
exports.anyAccountCanLoginAdmin = anyAccountCanLoginAdmin;
|