system check & setting_secret

This commit is contained in:
undefined committed 2020-07-03 13:37:04 +08:00
1 parent c52ddd7512
commit 57f711ed4f
18 files changed
+295 -134

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
// TODO check more
const os = require('os');
const superagent = require('superagent');
const system = require('./model/system');
const db = require('./service/db');
const c = {};
async function checkDb(log, warn, error) {
try {
const coll = db.collection('check');
const d = await coll.findOne({ _id: 'check' });
if (!d) await coll.insertOne({ _id: 'check', value: 'check' });
await coll.createIndex('check');
await coll.deleteOne({ _id: 'check' });
} catch (e) {
error(`Mongo Error: Database read-write failed.\n${e.message}`);
}
}
async function checkPerm(log, warn) {
const { username } = os.userInfo();
if (username === 'root') warn('Hydro should not be run as root.');
// TODO check cwd read-write
}
async function checkMail(log, warn) {
const from = await system.get('smtp.from');
if (!from) warn('SMTP account was not provided, email verification disabled.');
}
async function checkProxy(log, warn) {
const [github, google, proxy] = await system.getMany([
'oauth.githubappid', 'oauth.googleappid', 'proxy',
]);
if (proxy) {
superagent.get('https://www.google.com/').timeout(10000).proxy(proxy)
.catch(() => warn('The proxy configured seem to be invalid.'));
} else if (github || google) {
warn('OAuth enabled. But for well-known reasons, a proxy is required.');
}
}
const checks = [
checkDb,
checkPerm,
checkMail,
checkProxy,
];
async function start(log, warn, error, cb) {
const id = String.random(6);
cb(id);
for (const check of checks) {
if (c[id]) {
delete c[id];
return;
}
// eslint-disable-next-line no-await-in-loop
await check(log, warn, error);
}
}
async function cancel(id) {
c[id] = true;
}
module.exports = { start, cancel };
+6 -2
View File
@@ -132,8 +132,12 @@ class HomeSecurityHandler extends Handler {
);
}
}
const path = [
['Hydro', 'homepage'],
['home_security', null],
];
this.response.template = 'home_security.html';
this.response.body = { sessions, geoipProvider: (geoip || {}).provider };
this.response.body = { sessions, geoipProvider: (geoip || {}).provider, path };
if (useragent) this.response.body.icon = useragent.icon;
}
@@ -202,7 +206,7 @@ class HomeSettingsHandler extends Handler {
async post(args) {
const $set = {};
for (const key in args) {
if (setting.SETTINGS_BY_KEY[key]) {
if (setting.SETTINGS_BY_KEY[key] && !setting.SETTINGS_BY_KEY[key].disabled) {
$set[key] = args[key];
}
}
+27 -5
View File
@@ -3,9 +3,12 @@ const system = require('../model/system');
const { STATUS, PRIV } = require('../model/builtin');
const record = require('../model/record');
const judge = require('./judge');
const { Route, Handler } = require('../service/server');
const {
Route, Connection, Handler, ConnectionHandler,
} = require('../service/server');
const hpm = require('../lib/hpm');
const loader = require('../loader');
const check = require('../check');
class SystemHandler extends Handler {
async prepare() {
@@ -24,6 +27,24 @@ class SystemMainHandler extends SystemHandler {
}
}
class SystemCheckConnHandler extends ConnectionHandler {
async prepare() {
this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
await this.check();
}
async check() {
const log = (payload) => this.send({ type: 'log', payload });
const warn = (payload) => this.send({ type: 'warn', payload });
const error = (payload) => this.send({ type: 'error', payload });
await check.start(log, warn, error, (id) => { this.id = id; });
}
async cleanup() {
check.cancel(this.token);
}
}
class SystemDashboardHandler extends SystemHandler {
async get() {
this.response.template = 'manage_dashboard.html';
@@ -111,8 +132,8 @@ class SystemScriptHandler extends SystemHandler {
class SystemSettingHandler extends SystemHandler {
async get() {
this.response.template = 'manage_settings.html';
this.response.body.path.push(['manage_settings', null]);
this.response.template = 'manage_setting.html';
this.response.body.path.push(['manage_setting', null]);
this.response.body.current = {};
this.response.body.settings = setting.SYSTEM_SETTINGS;
for (const s of this.response.body.settings) {
@@ -129,7 +150,7 @@ class SystemSettingHandler extends SystemHandler {
const subtasks = [];
for (const sub in args[key]) {
const s = setting.SYSTEM_SETTINGS_BY_KEY[`${key}.${sub}`];
if (s) {
if (s && !((s.flag & setting.FLAG_SECRET) && args[key][sub] === '')) {
if (s.ui === 'number') args[key][sub] = Number(args[key][sub]);
subtasks.push(system.set(`${key}.${sub}`, args[key][sub]));
}
@@ -137,7 +158,7 @@ class SystemSettingHandler extends SystemHandler {
tasks.push(Promise.all(subtasks));
} else {
const s = setting.SYSTEM_SETTINGS_BY_KEY[key];
if (s) {
if (s && !((s.flag & setting.FLAG_SECRET) && args[key] === '')) {
if (s.ui === 'number') args[key] = Number(args[key]);
tasks.push(system.set(key, args[key]));
}
@@ -154,6 +175,7 @@ async function apply() {
Route('manage_script', '/manage/script', SystemScriptHandler);
Route('manage_module', '/manage/module', SystemModuleHandler);
Route('manage_setting', '/manage/setting', SystemSettingHandler);
Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler);
}
global.Hydro.handler.manage = module.exports = apply;
+7 -1
View File
@@ -1,10 +1,16 @@
/* eslint-disable no-empty-function */
/* eslint-disable class-methods-use-this */
const { Route, Handler } = require('../service/server');
const { NotFoundError } = require('../error');
class NotFoundHandler extends Handler {
async prepare() { // eslint-disable-line class-methods-use-this
async prepare() {
throw new NotFoundError();
}
async get() { }
async post() { }
}
async function apply() {
+1 -1
View File
@@ -1,7 +1,7 @@
// From https://github.com/pvorb/node-md5/blob/master/md5.js
// BSD 3-Clause "New" or "Revised" License
/* eslint-disable no-bitwise */ /* eslint-disable max-len */
/* eslint-disable max-len */
function safeAdd(x, y) {
const lsw = (x & 0xffff) + (y & 0xffff);
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+1
View File
@@ -23,6 +23,7 @@ class Nunjucks extends nunjucks.Environment {
});
this.addFilter('ansi', (self) => misc.ansiToHtml(self));
this.addFilter('base64_encode', (s) => Buffer.from(s).toString('base64'));
this.addFilter('bitand', (self, val) => self & val);
}
}
const env = new Nunjucks();
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-bitwise */
const PERM = {
PERM_NONE: '',
+17 -6
View File
@@ -9,6 +9,10 @@ for (const country of countries) {
}
const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]);
const FLAG_HIDDEN = 1;
const FLAG_DISABLED = 2;
const FLAG_SECRET = 4;
const PREFERENCE_SETTINGS = [];
const ACCOUNT_SETTINGS = [];
const DOMAIN_SETTINGS = [];
@@ -23,9 +27,9 @@ const SYSTEM_SETTINGS_BY_KEY = {};
const Setting = (
family, key, range = null,
value = null, type = 'text', name = '',
desc = '', hidden = false, disabled = false,
desc = '', flag = 0,
) => ({
family, key, range, value, type, name, desc, hidden, disabled,
family, key, range, value, type, name, desc, flag,
});
const PreferenceSetting = (...settings) => {
@@ -93,11 +97,15 @@ DomainSetting(
Setting('setting_domain', 'name', null, 'New domain', 'text', 'name'),
Setting('setting_domain', 'gravatar', null, '', 'text', 'gravatar', 'Will be used as the domain icon.'),
Setting('setting_domain', 'bulletin', null, '', 'markdown', 'Bulletin'),
Setting('storage', 'nAccept', null, 0, 'number', 'nAccept', null, FLAG_HIDDEN & FLAG_DISABLED),
Setting('storage', 'nSubmit', null, 0, 'number', 'nSubmit', null, FLAG_HIDDEN & FLAG_DISABLED),
Setting('storage', 'nLike', null, 0, 'number', 'nLike', null, FLAG_HIDDEN & FLAG_DISABLED),
Setting('storage', 'rating', null, 1500, 'number', 'rating', null, FLAG_HIDDEN & FLAG_DISABLED),
);
SystemSetting(
Setting('setting_smtp', 'smtp.user', null, null, 'text', 'SMTP Username'),
Setting('setting_smtp', 'smtp.pass', null, null, 'password', 'SMTP Password'),
Setting('setting_smtp', 'smtp.pass', null, null, 'password', 'SMTP Password', null, FLAG_SECRET),
Setting('setting_smtp', 'smtp.host', null, null, 'text', 'SMTP Server Host'),
Setting('setting_smtp', 'smtp.port', null, 465, 'number', 'SMTP Server Port'),
Setting('setting_smtp', 'smtp.from', null, null, 'text', 'Mail From'),
@@ -109,9 +117,9 @@ SystemSetting(
Setting('setting_server', 'server.port', null, 8888, 'number', 'Server Port'),
Setting('setting_server', 'server.log', null, false, 'boolean', 'Disable Access Log'),
Setting('setting_oauth', 'oauth.githubappid', null, null, 'text', 'Github Oauth AppID'),
Setting('setting_oauth', 'oauth.githubsecret', null, null, 'text', 'Github Oauth Secret'),
Setting('setting_oauth', 'oauth.googleappid', null, null, 'text', 'Google Oauth ClientID'),
Setting('setting_oauth', 'oauth.googlesecret', null, null, 'text', 'Google Oauth Secret'),
Setting('setting_oauth', 'oauth.githubsecret', null, null, 'text', 'Github Oauth Secret', null, FLAG_SECRET),
Setting('setting_oauth', 'oauth.googleappid', null, null, 'text', 'Google Oauth ClientID', null),
Setting('setting_oauth', 'oauth.googlesecret', null, null, 'text', 'Google Oauth Secret', null, FLAG_SECRET),
Setting('setting_proxy', 'proxy', null, null, 'text', 'Proxy Server URL'),
Setting('setting_constant', 'PROBLEM_PER_PAGE', null, 100, 'number', 'Problems per Page'),
Setting('setting_constant', 'CONTEST_PER_PAGE', null, 20, 'number', 'Contests per Page'),
@@ -129,6 +137,9 @@ global.Hydro.model.setting = module.exports = {
DomainSetting,
DomainUserSetting,
SystemSetting,
FLAG_HIDDEN,
FLAG_DISABLED,
FLAG_SECRET,
PREFERENCE_SETTINGS,
ACCOUNT_SETTINGS,
SETTINGS,
+22 -19
View File
@@ -9,6 +9,14 @@ const db = require('../service/db');
const coll = db.collection('user');
function setPassword(uid, password) {
const salt = String.random();
return coll.findOneAndUpdate(
{ _id: uid },
{ $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } },
);
}
class USER {
constructor(udoc, dudoc) {
this.udoc = () => udoc;
@@ -18,7 +26,12 @@ class USER {
this.uname = udoc.uname;
this.salt = () => udoc.salt;
this.hash = () => udoc.hash;
this.hashType = udoc.hashType || 'hydro';
this.priv = udoc.priv;
this.regat = udoc.regat;
this.loginat = udoc.loginat;
this.perm = dudoc.perm;
this.role = dudoc.role || 'default';
for (const key in setting.SETTINGS_BY_KEY) {
if (udoc[key]) this[key] = udoc[key];
@@ -27,14 +40,12 @@ class USER {
}
}
this.regat = udoc.regat;
this.loginat = udoc.loginat;
this.ban = udoc.ban || false;
this.nAccept = dudoc.nAccept || 0;
this.nSubmit = dudoc.nSubmit || 0;
this.rating = dudoc.rating || 1500;
this.perm = dudoc.perm;
this.role = dudoc.role || 'default';
for (const key in setting.DOMAIN_USER_SETTINGS_BY_KEY) {
if (dudoc[key]) this[key] = dudoc[key];
else if (setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value) {
this[key] = setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value;
}
}
}
hasPerm(p) {
@@ -42,15 +53,16 @@ class USER {
}
hasPriv(p) {
// eslint-disable-next-line no-bitwise
return this.priv & p;
}
checkPassword(password) {
const h = global.Hydro.lib[`hash.${this.hashType || 'hydro'}`];
const h = global.Hydro.lib[`hash.${this.hashType}`];
if (!h) throw new Error('Unknown hash method');
if (!(h(password, this.salt(), this) === this.hash())) {
throw new LoginError(this.uname);
} else if (this.hashType !== 'hydro') {
setPassword(this._id, password);
}
}
}
@@ -59,7 +71,6 @@ async function getInDomain(domainId, udoc) {
let dudoc = await document.getStatus(domainId, document.TYPE_DOMAIN_USER, 0, udoc._id);
dudoc = dudoc || {};
if (udoc._id === 1) dudoc.role = 'guest';
// eslint-disable-next-line no-bitwise
if (udoc.priv & PRIV.PRIV_MANAGE_ALL_DOMAIN) dudoc.role = 'admin';
const p = await document.get(domainId, document.TYPE_DOMAIN_USER, dudoc.role || 'default');
dudoc.perm = p ? p.content : BUILTIN_ROLES[dudoc.role || 'default'].perm;
@@ -116,14 +127,6 @@ async function getByEmail(domainId, mail, ignoreMissing = false) {
return new USER(udoc, dudoc);
}
function setPassword(uid, password) {
const salt = String.random();
return coll.findOneAndUpdate(
{ _id: uid },
{ $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } },
);
}
function setById(uid, $set, $unset) {
const op = {};
if ($set && Object.keys($set).length) op.$set = $set;
+74 -86
View File
@@ -121,41 +121,7 @@ async function prepare() {
}));
}
class Handler {
/**
* @param {import('koa').Context} ctx
*/
constructor(ctx) {
this.ctx = ctx;
this.request = {
host: ctx.request.host,
hostname: ctx.request.hostname,
ip: ctx.request.ip,
headers: ctx.request.headers,
cookies: ctx.cookies,
body: ctx.request.body,
files: ctx.request.files,
query: ctx.query,
path: ctx.path,
params: ctx.params,
referer: ctx.request.headers.referer || '/',
};
this.response = {
body: '',
type: '',
status: null,
template: null,
redirect: null,
attachment: (name) => ctx.attachment(name),
};
this.UIContext = {
cdn_prefix: '/',
url_prefix: '/',
};
this._handler = {};
this.session = {};
}
const HandlerMixin = (Class) => class extends Class {
async renderHTML(name, context) {
if (enableLog) console.time(name);
this.hasPerm = (perm) => this.user.hasPerm(perm);
@@ -171,9 +137,12 @@ class Handler {
return res;
}
async render(name, context) {
this.response.body = await this.renderHTML(name, context);
this.response.type = 'text/html';
async limitRate(op, periodSecs, maxOperations) {
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
}
translate(str) {
return str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : '';
}
renderTitle(str) {
@@ -214,26 +183,6 @@ class Handler {
}
}
async limitRate(op, periodSecs, maxOperations) {
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
}
back(body) {
this.response.body = body || this.response.body || {};
this.response.redirect = this.request.headers.referer || '/';
}
translate(str) {
return str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : '';
}
binary(data, name) {
this.response.body = data;
this.response.template = null;
this.response.type = 'application/octet-stream';
this.response.disposition = `attachment; filename="${name}"`;
}
url(name, kwargs = {}) {
let res = '#';
const args = { ...kwargs };
@@ -252,8 +201,61 @@ class Handler {
}
return res;
}
};
async ___prepare({ domainId }) {
const Handler = HandlerMixin(class {
/**
* @param {import('koa').Context} ctx
*/
constructor(ctx) {
this.ctx = ctx;
this.request = {
host: ctx.request.host,
hostname: ctx.request.hostname,
ip: ctx.request.ip,
headers: ctx.request.headers,
cookies: ctx.cookies,
body: ctx.request.body,
files: ctx.request.files,
query: ctx.query,
path: ctx.path,
params: ctx.params,
referer: ctx.request.headers.referer || '/',
};
this.response = {
body: '',
type: '',
status: null,
template: null,
redirect: null,
attachment: (name) => ctx.attachment(name),
};
this.UIContext = {
cdn_prefix: '/',
url_prefix: '/',
};
this._handler = {};
this.session = {};
}
async render(name, context) {
this.response.body = await this.renderHTML(name, context);
this.response.type = 'text/html';
}
back(body) {
this.response.body = body || this.response.body || {};
this.response.redirect = this.request.headers.referer || '/';
}
binary(data, name) {
this.response.body = data;
this.response.template = null;
this.response.type = 'application/octet-stream';
this.response.disposition = `attachment; filename="${name}"`;
}
async init({ domainId }) {
this.response.body = {};
this.now = new Date();
this._handler.sid = this.request.cookies.get('sid');
@@ -377,7 +379,7 @@ class Handler {
};
await this.___cleanup().catch(() => { });
}
}
});
async function handle(ctx, HandlerClass, checker) {
global.Hydro.stat.reqCount++;
@@ -394,7 +396,7 @@ async function handle(ctx, HandlerClass, checker) {
.replace(/_([a-z])/gm, (s) => s[1].toUpperCase());
}
if (h.___prepare) await h.___prepare(args);
if (h.init) await h.init(args);
if (checker) checker.call(h);
if (method === 'post') {
await h.checkCsrfToken(args.csrfToken);
@@ -437,7 +439,7 @@ async function handle(ctx, HandlerClass, checker) {
if (h.__cleanup) await h.__cleanup(args);
if (h.___cleanup) await h.___cleanup(args);
} catch (e) {
if (h.onerror) await h.onerror(e);
await h.onerror(e);
}
}
@@ -471,7 +473,7 @@ function Route(name, route, RouteHandler, ...permPrivChecker) {
router.all(`${name}_with_domainId`, `/d/:domainId${route}`, (ctx) => handle(ctx, RouteHandler, checker));
}
class ConnectionHandler {
const ConnectionHandler = HandlerMixin(class {
/**
* @param {import('sockjs').Connection} conn
*/
@@ -486,25 +488,6 @@ class ConnectionHandler {
for (const i in p) this.request.params[p[i][0]] = decodeURIComponent(p[i][1]);
}
url(name, kwargs = {}) {
let res = '#';
const args = { ...kwargs };
try {
if (this.args.domainId !== 'system') {
name += '_with_domainId';
args.domainId = args.domainId || this.args.domainId;
}
const { anchor, query } = args;
if (query) res = router.url(name, args, { query });
else res = router.url(name, args);
if (anchor) return `${res}#${anchor}`;
} catch (e) {
console.error(e.message);
console.error(name, kwargs);
}
return res;
}
async renderHTML(name, context) {
this._user = { ...this.user, gravatar: misc.gravatar(this.user.gravatar, 128) };
const res = await template.render(name, Object.assign(context, {
@@ -524,7 +507,12 @@ class ConnectionHandler {
this.conn.close(code, reason);
}
async ___prepare({ domainId }) {
onerror(err) {
console.error(err);
this.close(1001, err.toString());
}
async init({ domainId }) {
try {
this.session = await token.get(this.request.params.token, token.TYPE_CSRF_TOKEN);
await token.del(this.request.params.token, token.TYPE_CSRF_TOKEN);
@@ -536,7 +524,7 @@ class ConnectionHandler {
this.user = await user.getById(domainId, this.session.uid);
if (!this.user) throw new UserNotFoundError(this.session.uid);
}
}
});
function Connection(name, prefix, RouteConnHandler, permission) {
const sock = sockjs.createServer({ prefix });
@@ -545,7 +533,7 @@ function Connection(name, prefix, RouteConnHandler, permission) {
try {
const args = { domainId: 'system', ...h.request.params };
h.args = args;
if (h.___prepare) await h.___prepare(args);
if (h.init) await h.init(args);
if (permission) h.checkPerm(permission);
let checking = '';
try {
@@ -575,7 +563,7 @@ function Connection(name, prefix, RouteConnHandler, permission) {
});
} catch (e) {
console.log(e);
if (h.onerror) await h.onerror(e);
await h.onerror(e);
}
});
sock.installHandlers(server);