From 9e96c471af0583c93c78367f0e99340018eaa58e Mon Sep 17 00:00:00 2001 From: undefined Date: Thu, 23 Jul 2020 13:34:20 +0800 Subject: [PATCH] problem_copy --- docs/deploy.md | 72 ---------------------------------------- docs/judge.md | 7 ---- docs/oauth.md | 4 --- hydro/entry/cli.ts | 0 hydro/entry/master.ts | 18 ++-------- hydro/entry/worker.ts | 5 +++ hydro/handler/misc.ts | 3 +- hydro/handler/problem.ts | 68 +++++++++++++++++++++++++------------ hydro/interface.ts | 12 +++++-- hydro/model/document.ts | 12 +++++++ hydro/model/domain.ts | 12 ++++--- hydro/model/problem.ts | 14 ++++---- hydro/service/server.ts | 48 ++++++++++++++------------- package.json | 2 +- 14 files changed, 118 insertions(+), 159 deletions(-) delete mode 100644 docs/deploy.md delete mode 100644 docs/judge.md delete mode 100644 docs/oauth.md create mode 100644 hydro/entry/cli.ts diff --git a/docs/deploy.md b/docs/deploy.md deleted file mode 100644 index ad2118eba..000000000 --- a/docs/deploy.md +++ /dev/null @@ -1,72 +0,0 @@ -# Introduction | introduction - -为什么使用 Hydro ? - -- 安全:使用 cgroup 进行隔离,杜绝卡评测; -- 高效:Hydro 使用了沙箱复用技术,拥有极高的评测效率; -- 扩展:Hydro 支持安装额外模块进行扩展; -- 强大:配合 Judger 模块(或 HydroJudger 独立评测机),可支持 spj,交互题,提交答案题,文件IO等多种特性; -- 简便:提供了多种迁移脚本,可从 Vijos 等快速升级至 Hydro; -- 自定:所有权限节点均可自由设置; -- 社区:Hydro 正在持续维护中; - -# Deployment | deployment - -Hydro 依赖于 MongoDB 与 NodeJS>=10.10,您应该先安装它们。 - -- 下载安装 MongoDB:[https://www.mongodb.com/try/download/community](https://www.mongodb.com/try/download/community) -- 使用 nvm 安装 NodeJS:[Linux](https://nvm.sh/) [Windows](https://github.com/coreybutler/nvm-windows) -- 安装 yarn: `npm install yarn -g` -- 使用 yarn 安装 Hydro:`yarn global add hydrooj @hydrooj/ui-default` -- 启动 Hydro:`hydrooj` - -可选项:pm2 守护程序 - -```sh -yarn global add pm2 # 安装pm2 -pm2 startup # 开机自启 -pm2 start hydrooj # 启动 Hydro -pm2 save # 保存 -``` - -注意:单个Hydro实例需要约100MB的运行内存,但在载入时可能需要较多内存(取决于安装的模块数量与大小) - -Hydro 会自行初始化并监听 8888 端口(可使用 `--port=1234` 指定其他端口)。请用浏览器访问并进行相应配置。 -数据库配置完成后,会自动创建 Root 用户。(账号 `Root` ,密码 `rootroot`),请及时修改。 - -之后的进阶配置可在 管理 面板进行。 - -## 附加组件 - -警告:附加组件对站点所有内容具有完全的访问权限。请不要安装来历不明的组件。 - -#### 安装附加组件: - -先全局安装所需模块,再向 hydrooj 注册即可。 例:安装 @hydrooj/geoip - -```sh -yarn global add @hydrooj/geoip -hydrooj addon add @hydrooj/geoip -``` - -#### 附加组件列表 - -Hydro官方目前提供了以下附加组件: - -| ID | 描述 | 大小 | -| ---------------------- | ------------------------------ | ----- | -| @hydrooj/ui-default | Hydro的默认用户界面 | ~10MB | -| @hydrooj/geoip | GeoIP 支持,用于显示用户登录地 | ~60MB | -| @hydrooj/migrate-vijos | 从vijos4的自动升级工具 | <1MB | -| @hydrooj/hydrojudge | 评测组件 | ~2MB | - -#### 卸载附加组件 - -```sh -yarn global remove @hydrooj/geoip -hydrooj addon remove @hydrooj/geoip -``` - -## 杂项 - -[OAuth 配置](./oauth.md) diff --git a/docs/judge.md b/docs/judge.md deleted file mode 100644 index 1323658a0..000000000 --- a/docs/judge.md +++ /dev/null @@ -1,7 +0,0 @@ -# Judge - -评测机配置: - -可直接安装 Judger 模块。相关配置可在系统设置中修改。 - -或者参照 [HydroJudger](https://github.com/hydro-dev/HydroJudger) 部署。 diff --git a/docs/oauth.md b/docs/oauth.md deleted file mode 100644 index 0865382a8..000000000 --- a/docs/oauth.md +++ /dev/null @@ -1,4 +0,0 @@ -# OAuth - -Hydro 支持使用 Github 和 Google 登录。申请相应 API Key 填入系统设置。 -p.s. 由于众所周知的原因,需要设置 proxy 设置项。 diff --git a/hydro/entry/cli.ts b/hydro/entry/cli.ts new file mode 100644 index 000000000..e69de29bb diff --git a/hydro/entry/master.ts b/hydro/entry/master.ts index 9588005d6..c3acfb222 100644 --- a/hydro/entry/master.ts +++ b/hydro/entry/master.ts @@ -1,13 +1,8 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable import/no-dynamic-require */ -import { - lib, service, model, setting, - builtinLib, builtinHandler, builtinModel, -} from './common'; +import { builtinModel } from './common'; -export async function load(call, args) { - let pending = args; - const fail = []; +export async function load(call) { require('../lib/i18n'); require('../utils'); require('../error'); @@ -28,20 +23,12 @@ export async function load(call, args) { }); require('../service/monitor'); for (const i of builtinModel) require(`../model/${i}`); - for (const i of builtinHandler) require(`../handler/${i}`); - for (const m in global.Hydro.model) { - if (global.Hydro.model[m].ensureIndexes) { - await global.Hydro.model[m].ensureIndexes(); - } - } const modelSystem = require('../model/system'); - const modelSetting = require('../model/setting'); const dbVer = await modelSystem.get('db.ver'); if (dbVer !== 1) { const ins = require('../script/install'); await ins.run({ username: 'Root', password: 'rootroot' }); } - await setting(pending, fail, modelSetting); for (const i in global.Hydro.service) { if (global.Hydro.service[i].postInit) { try { @@ -58,6 +45,5 @@ export async function load(call, args) { console.error(e); } } - pending = []; return await modelSystem.get('server.worker'); } diff --git a/hydro/entry/worker.ts b/hydro/entry/worker.ts index 994d77d38..1691d274a 100644 --- a/hydro/entry/worker.ts +++ b/hydro/entry/worker.ts @@ -37,6 +37,11 @@ export async function load(call, args) { for (const i of builtinModel) require(`../model/${i}`); for (const i of builtinHandler) require(`../handler/${i}`); await model(pending, fail); + for (const m in global.Hydro.model) { + if (global.Hydro.model[m].ensureIndexes) { + await global.Hydro.model[m].ensureIndexes(); + } + } const modelSetting = require('../model/setting'); await setting(pending, fail, modelSetting); await handler(pending, fail); diff --git a/hydro/handler/misc.ts b/hydro/handler/misc.ts index 88fef538e..b86e2c025 100644 --- a/hydro/handler/misc.ts +++ b/hydro/handler/misc.ts @@ -8,7 +8,7 @@ import * as user from '../model/user'; import * as markdown from '../lib/markdown'; import * as db from '../service/db'; import { - Route, Handler, Types, param, + Route, Handler, Types, param, multipart, } from '../service/server'; import { BadRequestError } from '../error'; @@ -39,6 +39,7 @@ class FileUploadHandler extends Handler { this.response.body = { fdoc: null, usage: this.user.usage, quota: await this.getQuota() }; } + @multipart(256 * 1024) @param('title', Types.String) async post(domainId: string, title: string) { if (!this.request.files.file) throw new BadRequestError(); diff --git a/hydro/handler/problem.ts b/hydro/handler/problem.ts index e3ce3d799..7b36113f6 100644 --- a/hydro/handler/problem.ts +++ b/hydro/handler/problem.ts @@ -15,12 +15,14 @@ import * as system from '../model/system'; import { PERM, PRIV } from '../model/builtin'; import * as bus from '../service/bus'; import { - Route, Connection, Handler, ConnectionHandler, Types, param, + Route, Connection, Handler, ConnectionHandler, Types, param, multipart, } from '../service/server'; import { NoProblemError, ProblemDataNotFoundError, BadRequestError, SolutionNotFoundError, ProblemNotFoundError, + ValidationError, + PermissionError, } from '../error'; import { Pdoc, User, Rdoc } from '../interface'; @@ -195,6 +197,22 @@ class ProblemDetailHandler extends ProblemHandler { // eslint-disable-next-line async get(...args: any[]) { } + + @param('pid', Types.ObjectID) + @param('dest', Types.String) + @param('hidden', Types.Boolean) + async postCopy(domainId: string, pid: number, destDomainId: string, hidden = false) { + const udoc = await user.getById(destDomainId, this.user._id); + if (!udoc.hasPerm(PERM.PERM_CREATE_PROBLEM)) { + throw new PermissionError(PERM.PERM_CREATE_PROBLEM); + } + pid = await problem.add( + destDomainId, this.pdoc.pid, this.pdoc.title, + this.pdoc.content, this.user._id, this.pdoc.tag, + this.pdoc.category, { domainId, pid }, hidden, + ); + this.response.redirect = this.url('problem_settings', { domainId: destDomainId, pid }); + } } class ProblemSubmitHandler extends ProblemDetailHandler { @@ -365,30 +383,27 @@ class ProblemEditHandler extends ProblemManageHandler { } class ProblemDataUploadHandler extends ProblemManageHandler { - async prepare() { + async get() { + if (this.pdoc.data instanceof ObjectID) { + const f = await file.getMeta(this.pdoc.data); + this.response.body.md5 = f.md5; + } else if (this.pdoc.data) { + this.response.body.from = this.pdoc.data; + } this.response.template = 'problem_upload.html'; } - async get() { - let md5; - if (this.pdoc.data && typeof this.pdoc.data === 'object') { - const f = await file.getMeta(this.pdoc.data); - md5 = f.md5; - } - this.response.body.md5 = md5; - } - + @multipart(256 * 1024) async post({ domainId }) { - let md5; - if (!this.request.files.file) throw new BadRequestError(); + if (!this.request.files.file) throw new ValidationError('file'); this.pdoc = await problem.setTestdata( domainId, this.pdoc.docId, this.request.files.file.path, ); - if (this.pdoc.data && typeof this.pdoc.data === 'object') { + if (this.pdoc.data instanceof ObjectID) { const f = await file.getMeta(this.pdoc.data); - md5 = f.md5; + this.response.body.md5 = f.md5; } - this.response.body.md5 = md5; + this.response.template = 'problem_upload.html'; } } @@ -399,9 +414,15 @@ class ProblemDataDownloadHandler extends ProblemDetailHandler { this.checkPerm(PERM.PERM_READ_PROBLEM_DATA); } else this.checkPerm(PERM.PERM_READ_PROBLEM_DATA_SELF); } - if (!this.pdoc.data) throw new ProblemDataNotFoundError(pid); - else if (typeof this.pdoc.data === 'string') [, this.response.redirect] = this.pdoc.data.split('from:'); - else this.response.redirect = await file.url(this.pdoc.data, `${this.pdoc.title}.zip`); + if (this.pdoc.data instanceof ObjectID) { + this.response.redirect = await file.url(this.pdoc.data, `${this.pdoc.title}.zip`); + } else if (this.pdoc.data) { + // Redirect? + this.response.redirect = this.url('problem_data', { + domainId: this.pdoc.domainId, + pid: this.pdoc.pid, + }); + } else throw new ProblemDataNotFoundError(pid); } } @@ -552,9 +573,12 @@ class ProblemCreateHandler extends Handler { @param('content', Types.String, isContent) @param('hidden', Types.Boolean) async post(domainId: string, title: string, pid: string, content: string, hidden = false) { - pid = await problem.add(domainId, pid, title, content, this.user._id, [], [], null, hidden); - this.response.body = { pid }; - this.response.redirect = this.url('problem_settings', { pid }); + const docId = await problem.add( + domainId, pid, title, content, + this.user._id, [], [], null, hidden, + ); + this.response.body = { pid: docId }; + this.response.redirect = this.url('problem_settings', { pid: docId }); } } diff --git a/hydro/interface.ts b/hydro/interface.ts index 932c0aaae..7436618b8 100644 --- a/hydro/interface.ts +++ b/hydro/interface.ts @@ -53,6 +53,15 @@ export interface User extends Dictionary { export type Udict = NumericDictionary; +export interface ProblemDataSource { + host?: string, // Empty for local + domainId: string, + pid: number, +} + +// ObjectID for built-in files, ProblemDataSource for other source (RemoteJudge, etc.) +export type ProblemData = ObjectID | ProblemDataSource; + export interface Pdoc { _id: ObjectID, domainId: string, @@ -66,8 +75,7 @@ export interface Pdoc { nAccept: number, tag: string[], category: string[], - // ObjectID for built-in files, string for other source (RemoteJudge, etc.) - data?: ObjectID | string, + data?: ProblemData, hidden: boolean, config: string, acMsg?: string, diff --git a/hydro/model/document.ts b/hydro/model/document.ts index 171439213..4d94b0a62 100644 --- a/hydro/model/document.ts +++ b/hydro/model/document.ts @@ -18,6 +18,18 @@ export const TYPE_TRAINING = 40; export const TYPE_FILE = 50; export const TYPE_HOMEWORK = 60; +export async function add( + domainId: string, content: string, owner: number, + docType: number, docId: DocID, + parentType?: number | null, parentId?: DocID, + args?: any, +): Promise +export async function add( + domainId: string, content: string, owner: number, + docType: number, docId: null, + parentType?: number, parentId?: DocID, + args?: any, +): Promise export async function add( domainId: string, content: string, owner: number, docType: number, docId: DocID = null, diff --git a/hydro/model/domain.ts b/hydro/model/domain.ts index 927c492d5..f18eb785e 100644 --- a/hydro/model/domain.ts +++ b/hydro/model/domain.ts @@ -30,13 +30,13 @@ export function edit(domainId: string, $set: any) { return coll.updateOne({ _id: domainId }, { $set }); } -export async function inc(domainId: string, field: string, n: number): Promise { +export async function inc(domainId: string, field: string, n: number): Promise { const res = await coll.findOneAndUpdate( { _id: domainId }, { $inc: { [field]: n } }, { returnOriginal: false }, ); - return res.value; + return res.value.field; } export async function getList(domainIds: string[]): Promise> { @@ -91,10 +91,14 @@ export async function deleteRoles(domainId: string, roles: string[]) { ]); } -export async function getDomainUser(domainId: string, udoc: Udoc) { +interface DomainUserArg { + _id: number, + priv: number, +} +export async function getDomainUser(domainId: string, udoc: DomainUserArg) { let dudoc = await collUser.findOne({ domainId, uid: udoc._id }); dudoc = dudoc || {}; - if (udoc._id === 1) dudoc.role = 'guest'; + if (!(udoc.priv & PRIV.PRIV_USER_PROFILE)) dudoc.role = 'guest'; if (udoc.priv & PRIV.PRIV_MANAGE_ALL_DOMAIN) dudoc.role = 'admin'; dudoc.role = dudoc.role || 'default'; const ddoc = await get(domainId); diff --git a/hydro/model/problem.ts b/hydro/model/problem.ts index 1c7870451..ec2c65e7b 100644 --- a/hydro/model/problem.ts +++ b/hydro/model/problem.ts @@ -3,7 +3,7 @@ import { STATUS } from './builtin'; import * as file from './file'; import * as document from './document'; import * as domain from './domain'; -import { Pdoc, Pdict } from '../interface'; +import { Pdoc, Pdict, ProblemDataSource } from '../interface'; import { ProblemNotFoundError } from '../error'; import readConfig from '../lib/readConfig'; @@ -28,16 +28,16 @@ export const pdocHidden: Pdoc = { export async function add( domainId: string, pid: string = null, title: string, content: string, owner: number, - tag: string[] = [], category: string[] = [], data: ObjectID = null, hidden = false, + tag: string[] = [], category: string[] = [], data: ProblemDataSource = null, hidden = false, ) { - const d = await domain.inc(domainId, 'pidCounter', 1); - if (!pid) pid = d.pidCounter.toString(); + const pidCounter = await domain.inc(domainId, 'pidCounter', 1); + if (!pid) pid = pidCounter.toString(); return await document.add( - domainId, content, owner, document.TYPE_PROBLEM, d.pidCounter, null, null, + domainId, content, owner, document.TYPE_PROBLEM, pidCounter, null, null, { pid, title, data, category, tag, hidden, nSubmit: 0, nAccept: 0, }, - ); + ) as number; } export async function get( @@ -153,7 +153,7 @@ export async function setTestdata(domainId: string, _id: number, filePath: strin const pdoc = await get(domainId, _id); const config = await readConfig(filePath); const id = await file.add(filePath, 'data.zip', 1); - if (pdoc.data && typeof pdoc.data === 'object') file.dec(pdoc.data); + if (pdoc.data instanceof ObjectID) file.del(pdoc.data); return await edit(domainId, _id, { data: id, config }); } diff --git a/hydro/service/server.ts b/hydro/service/server.ts index e80028ae8..2d19e45e6 100644 --- a/hydro/service/server.ts +++ b/hydro/service/server.ts @@ -2,9 +2,9 @@ import assert from 'assert'; import path from 'path'; import os from 'os'; -import http from 'http'; +import http, { Server } from 'http'; import moment from 'moment-timezone'; -import { isSafeInteger } from 'lodash'; +import { isSafeInteger, Dictionary } from 'lodash'; import { ObjectID } from 'mongodb'; import Koa from 'koa'; import morgan from 'koa-morgan'; @@ -13,6 +13,7 @@ import Router from 'koa-router'; import cache from 'koa-static-cache'; import sockjs from 'sockjs'; import serialize, { SerializeJSOptions } from 'serialize-javascript'; +import { argv } from 'yargs'; import { lrucache } from '../utils'; import { User, DomainDoc } from '../interface'; import { @@ -32,14 +33,9 @@ import * as token from '../model/token'; import * as opcount from '../model/opcount'; const app = new Koa(); -let server; +const server = http.createServer(app.callback()); const router = new Router(); -// TODO fix this ugly hack -interface H { - [key: string]: any, -} - type MethodDecorator = (target: any, name: string, obj: any) => any; type Converter = (value: any) => any; type Validator = (value: any) => boolean; @@ -155,6 +151,22 @@ export function param(name: string, ...args: any): MethodDecorator { }; } +export function multipart(maxSizeKiB: number): MethodDecorator { + return function multipartDecorator(target: any, funcName: string, obj: any) { + const originalMethod = obj.value; + obj.value = async function checkCsrfToken(...args: any[]) { + const body = Body({ + multipart: true, + formidable: { + maxFileSize: maxSizeKiB * 1024, + }, + }); + return await body.call(this.ctx, this.ctx, () => originalMethod.call(this, ...args)); + }; + return obj; + }; +} + export function requireCsrfToken(target: any, funcName: string, obj: any) { const originalMethod = obj.value; obj.value = async function checkCsrfToken(...args: any[]) { @@ -167,10 +179,9 @@ export function requireCsrfToken(target: any, funcName: string, obj: any) { } export async function prepare() { - server = http.createServer(app.callback()); app.keys = await system.get('session.keys'); - if (process.env.debug) { - app.use(cache(path.join(process.cwd(), 'public'), { + if (argv.public) { + app.use(cache(argv.public, { maxAge: 0, })); } else { @@ -178,12 +189,7 @@ export async function prepare() { maxAge: 365 * 24 * 60 * 60, })); } - app.use(Body({ - multipart: true, - formidable: { - maxFileSize: 256 * 1024 * 1024, - }, - })); + app.use(Body()); } export class Handler { @@ -735,7 +741,7 @@ export function Connection( const sock = sockjs.createServer({ prefix }); const checker = Checker(permPrivChecker); sock.on('connection', async (conn) => { - const h: H = new RouteConnHandler(conn); + const h: Dictionary = new RouteConnHandler(conn); try { const args = { domainId: 'system', ...h.request.params }; h.args = args; @@ -761,10 +767,6 @@ export function Connection( sock.installHandlers(server); } -export function Middleware(middleware: Koa.Middleware) { - app.use(middleware); -} - export async function start() { const [disableLog, port] = await system.getMany(['server.log', 'server.port']); if (!disableLog) { @@ -781,12 +783,12 @@ export async function start() { global.Hydro.service.server = { Types, param, + multipart, requireCsrfToken, Handler, ConnectionHandler, Route, Connection, - Middleware, prepare, start, }; diff --git a/package.json b/package.json index 63f5ee4b6..54545720b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.9.9", + "version": "2.9.10", "main": "dist/loader.js", "bin": "bin/hydrooj.js", "repository": "https://github.com/hydro-dev/Hydro.git",