// Requires const fs = require('fs'); const process = require('process'); const pathtools = require('path'); const express = require('express'); const fileUpload = require('express-fileupload'); const { spawn } = require('child_process'); const pug = require('pug'); const unzip = require('unzip'); const touch = require('touch'); const bcrypt = require('bcryptjs'); const useragent = require('express-useragent'); const { port, pythonPath, aisPath, aisPathOld, hashPath } = require('./config'); // Create the directories to store the files try { fs.mkdirSync(aisPath); } catch { } try { fs.mkdirSync(aisPathOld); } catch { } let pythonRunning = false; let pythonShouldRun = false; function runPythonTotal() { let p = spawn(pythonPath, ['run.py'], {cwd: 'pytron_run'}); p.stdout.on('data', (data) => { let content = data.toString().split('\n'); for (let line of content) { console.log('run.py: ' + line); } }); p.stderr.on('data', (data) => { let content = data.toString().split('\n'); for (let line of content) { console.log('run.py: ' + line); } }); p.on('close', (code) => { process.stdout.write('Python finished executing'); }); } function runPython() { process.stdout.write('Requested to run python'); if (pythonRunning) { process.stdout.write(' buy python is already running'); pythonShouldRun = true; } else { pythonRunning = true; } process.stdout.write('\n'); let p = spawn(pythonPath, ['refresh.py'], {cwd: 'pytron_run'}); p.stdout.on('data', (data) => { let content = data.toString().split('\n'); for (let line of content) { console.log('run.py: ' + line); } }); p.stderr.on('data', (data) => { let content = data.toString().split('\n'); for (let line of content) { console.log('run.py: ' + line); } }); p.on('close', (code) => { process.stdout.write('Python finished executing'); if (pythonShouldRun) { process.stdout.write(' but another request arrive, so we will relaunch it\n'); pythonShouldRun = false; pythonRunning = false; runPython(); } else { process.stdout.write('\n'); pythonRunning = false; } }); } function parse(data) { let content = JSON.parse(data); let parsed = {ais: [], battles: {}}; for (let key in content) { let battlers = key.split('/'); let ai1 = battlers[0]; let ai2 = battlers[1]; parsed.battles[ai1 + '/' + ai2] = content[key][0]; parsed.battles[ai2 + '/' + ai1] = content[key][1]; let realAi1 = parsed.ais.find((x) => x.name == ai1); if (realAi1 === undefined) { realAi1 = { name: ai1, victories: 0, defeats: 0, nulls: 0, score: 0, }; parsed.ais.push(realAi1); } let realAi2 = parsed.ais.find((x) => x.name == ai2); if (realAi2 === undefined) { realAi2 = { name: ai2, victories: 0, defeats: 0, nulls: 0, score: 0, }; parsed.ais.push(realAi2); } realAi1.victories += content[key][0]; realAi1.defeats += content[key][1]; realAi2.victories += content[key][1]; realAi2.defeats += content[key][0]; realAi1.nulls += content[key][2]; realAi2.nulls += content[key][2]; } parsed.sortedAis = parsed.ais.slice(0); for (let ai of parsed.sortedAis) { ai.score = ai.victories * 3 + ai.nulls; } parsed.sortedAis.sort((a, b) => b.score - a.score); if (parsed.sortedAis.length > 0) parsed.sortedAis[0].rank = 1; if (parsed.sortedAis.length > 1) parsed.sortedAis[1].rank = 2; if (parsed.sortedAis.length > 2) parsed.sortedAis[2].rank = 3; return parsed; } function readData(req, res, callback) { fs.readFile('pytron_run/assets/data.json', 'utf-8', (err, data) => { if (err != null) { console.log(err); res.status(400); render(req, res, 'error', {message: 'It seems like the site is broken :\'('}); return; } let parsed = parse(data); callback(parsed); }); } function saveArchiveAndRun(req, res) { console.log("Saving archive"); let path = pathtools.join(aisPath, req.body.name); try { fs.mkdirSync(path); } catch { } let zipfile = pathtools.join(path, 'archive.zip'); req.files.archive.mv(zipfile, (err) => { if (err != null) { console.log("Failed to save archive"); console.log(err); res.status(400); render(req, res, 'error', {message: 'Unable to save the ZIP archive to the server'}); return; } fs.createReadStream(zipfile) .pipe(unzip.Extract({path})) .on('close', () => { // Touch __init__.py touch(pathtools.join(path, '__init__.py'), () => { // Trigger python_run runPython(); if (req.useragent.isCurl) { res.send('Success'); } else { res.redirect('/'); } }); }) .on('error', () => { render(req, res, 'error', {message: 'Failed to unzip the archive'}); }); }); } function render(req, res, template, context) { if (req.useragent.isCurl) { res.render("txt/" + template + "_txt", context); } else { res.render("html/" + template + "_html", context); } } function startServer() { const app = express(); app.set('view engine', 'pug'); app.use(fileUpload()); app.use(useragent.express()); app.get('/', (req, res) => { readData(req, res, (parsed) => { render(req, res, 'index', { data: parsed, running: pythonRunning, }); }); }); app.get('/is-running', (req, res) => { if (pythonRunning) { res.send('yes'); } else { res.send('no'); } }); app.get('/leaderboard', (req, res) => { readData(req, res, (parsed) => { render(req, res, 'leaderboard', { data: parsed, running: pythonRunning, }); }); }); app.get('/battles', (req, res) => { readData(req, res, (parsed) => { render(req, res, 'battles', { data: parsed, running: pythonRunning, }); }); }); app.get('/upload', (req, res) => { render(req, res, 'upload', {}); }); app.post('/upload-target', (req, res) => { console.log("/upload-target"); if (!req.body) { res.status(400); render(req, res, 'error', {message: "You have to send the form"}); } if (!req.body.name) { res.status(400); render(req, res, 'error', {message: "You have to enter a name in the form"}); return; } if (!req.body.password) { res.status(400); render(req, res, 'error', {message: "You have to enter a password in the form"}); } if (req.body.name.indexOf('.') !== -1) { res.status(400); render(req, res, 'error', {message: "The name of your AI can't contain dots"}); } if (!req.files.archive) { res.status(400); render(req, res, 'error', {message: "You have to send a ZIP archive"}); return; } let path = pathtools.join(aisPath, req.body.name); let aiExisted; try { fs.statSync(path).isDirectory(); aiExisted = true; } catch (e) { aiExisted = false; fs.mkdirSync(path); } if (aiExisted) { console.log("Ai existed, try to verify password"); fs.readFile(pathtools.join(path, hashPath), 'utf-8', function(err, data) { if (err != null) { res.status(400); render(req, res, 'error', {message: "Couldn't read hashed password"}); console.log(err); return; } // If the AI already existed, verify the password bcrypt.compare(req.body.password, data, function(err, success) { if (err != null) { res.status(400); render(req, res, 'error', {message: "Couldn't compare password"}); return; } if (!success) { console.log("Authentication failed"); res.status(400); render(req, res, 'error', {message: "Authentication failed"}); return; } console.log("Authentication complete"); try { if (fs.statSync(path).isDirectory()) { // Move it to old let version = 0; for(;;) { let movePath = pathtools.join(aisPathOld, req.body.name + '.' + version); try { fs.statSync(movePath); // If the sync succeded, it means that the directory already exists version++; } catch { // If we're here, it means that we found the right path. We // will move the old one to the old directories, and save // the new one fs.rename(path, movePath, () => { console.log("copying from " + pathtools.join(movePath, "__bcrypt__hash.txt") + " to " + pathtools.join(path, "__bcrypt__hash.txt")); // Don't forget to copy the hashed password. fs.copyFileSync( pathtools.join(movePath, "__bcrypt__hash.txt"), pathtools.join(path, "__bcrypt__hash.txt")); }); break; } } } } catch { // Nothing to do here } // Save archive and run stuff saveArchiveAndRun(req, res); }); }); } else { console.log("New user, hashing password"); bcrypt.hash(req.body.password, 10, function(err, hash) { if (err != null) { console.log(err); res.status(400); render(req, res, 'error', {message: "Couldn't hash password"}); return; } console.log("Storing password"); // Store hash in your password DB. fs.writeFile(pathtools.join(path, hashPath), hash, (err) => { if (err != null) { console.log(err); res.status(400); render(req, res, 'error', {message: "Couldn't save hashed password"}); return; } // Save archive and run stuff saveArchiveAndRun(req, res); }); }); } }); app.use('/static', express.static('static')); app.listen(port, () => { console.log(`Server listening on port ${port}!`) }) } function main() { switch (process.argv[2]) { case 'start': startServer(); break; case 'refresh': runPython(); break; case 'recompute': runPythonTotal(); break; default: console.log('Unknown option: ' + process.argv[2]); console.log('Usage: node server.js start|refresh|recompute') process.exit(1); break; } } main();