const fs = require('fs'), path = require('path'), util = require('util'), http = require('http'), serializejs = require('serialize-javascript'), UUID = require('uuid'), commandLineArgs = require('command-line-args'); const optionDefinitions = [ { name: 'config', alias: 'c', type: String, defaultValue: __dirname + '/config.json' }, ]; const options = commandLineArgs(optionDefinitions); require('reflect-metadata'); global.Promise = require('bluebird'); // Disable 'Warning: a promise was created in a handler at ...' Promise.config({ warnings: { wForgottenReturn: false } }); global.syzoj = { rootDir: __dirname, config: require('object-assign-deep')({}, require('./config-example.json'), require(options.config)), languages: require('./language-config.json'), configDir: options.config, models: [], modules: [], db: null, serviceID: UUID(), log(obj) { if (obj instanceof ErrorMessage) return; console.log(obj); }, checkMigratedToTypeORM() { const userConfig = require(options.config); if (!userConfig.db.migrated_to_typeorm) { app.use((req, res) => res.send('Please refer to TypeORM Migration Guide.')); app.listen(parseInt(syzoj.config.port), syzoj.config.hostname); return false; } return true; }, async run() { // Check config if (syzoj.config.session_secret === '@SESSION_SECRET@' || syzoj.config.judge_token === '@JUDGE_TOKEN@' || (syzoj.config.email_jwt_secret === '@EMAIL_JWT_SECRET@' && syzoj.config.register_mail) || syzoj.config.db.password === '@DATABASE_PASSWORD@') { console.log('Please generate and fill the secrets in config!'); process.exit(); } let Express = require('express'); global.app = Express(); if (!this.checkMigratedToTypeORM()) return; syzoj.production = app.get('env') === 'production'; let winstonLib = require('./libs/winston'); winstonLib.configureWinston(!syzoj.production); this.utils = require('./utility'); // Set assets dir app.use(Express.static(__dirname + '/static', { maxAge: syzoj.production ? '1y' : 0 })); // Set template engine ejs app.set('view engine', 'ejs'); // Use body parser let bodyParser = require('body-parser'); app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' })); app.use(bodyParser.json({ limit: '50mb' })); // Use cookie parser app.use(require('cookie-parser')()); app.locals.serializejs = serializejs; let multer = require('multer'); app.multer = multer({ dest: syzoj.utils.resolvePath(syzoj.config.upload_dir, 'tmp') }); // This should before load api_v2, to init the `res.locals.user` this.loadHooks(); // Trick to bypass CSRF for APIv2 app.use((() => { let router = new Express.Router(); app.apiRouter = router; require('./modules/api_v2'); return router; })()); app.server = http.createServer(app); await this.connectDatabase(); this.loadModules(); // redis and redisCache is for syzoj-renderer const redis = require('redis'); this.redis = redis.createClient(this.config.redis); this.redisCache = { get: util.promisify(this.redis.get).bind(this.redis), set: util.promisify(this.redis.set).bind(this.redis) }; if (!module.parent) { // Loaded by node CLI, not by `require()`. if (process.send) { // if it's started by child_process.fork(), it must be requested to restart // wait until parent process quited. await new Promise((resolve, reject) => { process.on('message', message => { if (message === 'quited') resolve(); }); process.send('quit'); }); } await this.lib('judger').connect(); app.server.listen(parseInt(syzoj.config.port), syzoj.config.hostname, () => { this.log(`SYZOJ is listening on ${syzoj.config.hostname}:${parseInt(syzoj.config.port)}...`); }); } }, restart() { console.log('Will now fork a new process.'); const child = require('child_process').fork(__filename, ['-c', options.config]); child.on('message', (message) => { if (message !== 'quit') return; console.log('Child process requested "quit".') child.send('quited', err => { if (err) console.error('Error sending "quited" to child process:', err); process.exit(); }); }); }, async connectDatabase() { // Patch TypeORM to workaround https://github.com/typeorm/typeorm/issues/3636 const TypeORMMysqlDriver = require('typeorm/driver/mysql/MysqlDriver'); const OriginalNormalizeType = TypeORMMysqlDriver.MysqlDriver.prototype.normalizeType; TypeORMMysqlDriver.MysqlDriver.prototype.normalizeType = function (column) { if (column.type === 'json') { return 'longtext'; } return OriginalNormalizeType(column); }; const TypeORM = require('typeorm'); global.TypeORM = TypeORM; const modelsPath = __dirname + '/models/'; const modelsBuiltPath = __dirname + '/models-built/'; const models = fs.readdirSync(modelsPath) .filter(filename => filename.endsWith('.ts') && filename !== 'common.ts') .map(filename => require(modelsBuiltPath + filename.replace('.ts', '.js')).default); await TypeORM.createConnection({ type: 'mariadb', host: this.config.db.host.split(':')[0], port: this.config.db.host.split(':')[1] || 3306, username: this.config.db.username, password: this.config.db.password, database: this.config.db.database, entities: models, synchronize: true, logging: !syzoj.production, extra: { connectionLimit: 50 } }); }, loadModules() { fs.readdir(__dirname + '/modules/', (err, files) => { if (err) { this.log(err); return; } files.filter((file) => file.endsWith('.js')) .forEach((file) => this.modules.push(require(`./modules/${file}`))); }); }, lib(name) { return require(`./libs/${name}`); }, model(name) { return require(`./models-built/${name}`).default; }, loadHooks() { let Session = require('express-session'); let FileStore = require('session-file-store')(Session); let sessionConfig = { secret: this.config.session_secret, cookie: { httpOnly: false }, rolling: true, saveUninitialized: true, resave: true, store: new FileStore }; if (syzoj.production) { app.set('trust proxy', 1); sessionConfig.cookie.secure = false; } app.use(Session(sessionConfig)); app.use((req, res, next) => { res.locals.useLocalLibs = !!parseInt(req.headers['syzoj-no-cdn']) || syzoj.config.no_cdn; let User = syzoj.model('user'); if (req.session.user_id) { User.findById(req.session.user_id).then((user) => { res.locals.user = user; next(); }).catch((err) => { this.log(err); res.locals.user = null; req.session.user_id = null; next(); }); } else { if (req.cookies.login) { let obj; try { obj = JSON.parse(req.cookies.login); User.findOne({ where: { username: obj[0], password: obj[1] } }).then(user => { if (!user) throw null; res.locals.user = user; req.session.user_id = user.id; next(); }).catch(err => { console.log(err); res.locals.user = null; req.session.user_id = null; next(); }); } catch (e) { res.locals.user = null; req.session.user_id = null; next(); } } else { res.locals.user = null; req.session.user_id = null; next(); } } }); // Active item on navigator bar app.use((req, res, next) => { res.locals.active = req.path.split('/')[1]; next(); }); app.use((req, res, next) => { res.locals.req = req; res.locals.res = res; next(); }); } }; syzoj.untilStarted = syzoj.run();