diff --git a/api/package.json b/api/package.json index b55f17c..cf6bf15 100644 --- a/api/package.json +++ b/api/package.json @@ -10,6 +10,7 @@ "author": "", "license": "ISC", "dependencies": { - "express": "^4.17.1" + "express": "^4.17.1", + "express-validator": "^6.9.2" } } diff --git a/api/src/execute.js b/api/src/execute.js new file mode 100644 index 0000000..3642671 --- /dev/null +++ b/api/src/execute.js @@ -0,0 +1,46 @@ +const { writeFile } = require('fs/promises'); +const { spawn } = require('child_process'); + +async function execute(res, language, body) { + const stamp = new Date().getTime(); + const sourceFile = `/tmp/${stamp}.code`; + + await writeFile(sourceFile, body.source); + + const process = spawn(__dirname + '/../../lxc/execute', [ + language.name, + sourceFile, + body.args?.join('\n') ?? '', + ]); + + const result = { + ran: true, + language: language.name, + stderr: '', + stdout: '', + output: '', + }; + + if (language.version) + result.version = language.version; + + process.stderr.on('data', chunk => { + result.stderr += chunk; + result.output += chunk; + }); + + process.stdout.on('data', chunk => { + result.stdout += chunk; + result.output += chunk; + }); + + result.stderr = result.stderr.trim().substring(0, 65535); + result.stdout = result.stdout.trim().substring(0, 65535); + result.output = result.output.trim().substring(0, 65535); + + process.on('exit', () => res.json(result)); +} + +module.exports = { + execute, +}; diff --git a/api/src/index.js b/api/src/index.js index a7c769f..d0efee4 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -1,110 +1,63 @@ -const { writeFile } = require('fs/promises'); const express = require('express'); -const app = express(); -const languages = require('./languages'); -const { spawn } = require('child_process'); - -{ - const process = spawn(__dirname + '/../../lxc/versions'); - - let output = ''; - process.stderr.on('data', chunk => output += chunk); - process.stdout.on('data', chunk => output += chunk); - - process.on('exit', () => { - const sections = output.toLowerCase().split('---'); - const versions = {}; - - for (const section of sections) { - const lines = section.trim().split('\n'); - - if (lines.length >= 2) { - const language = lines[0]; - - if (language === 'java') { - versions[language] = /\d+/.exec(lines[1])?.[0]; - } else if (language === 'emacs') { - versions[language] = /\d+\.\d+/.exec(lines[1])?.[0]; - } else { - versions[language] = /\d+\.\d+\.\d+/.exec(section)?.[0]; - } - } - } - - for (const language of languages) { - language.version = versions[language.name]; - } - }); -} - -app.use(express.json()); - -app.post('/execute', (req, res) => { - const body = req.body; - - const language = languages.find(language => { - return language.aliases.includes(body.language?.toString()?.toLowerCase()); - }); - - if (!language) { - return res.status(400).json({ - code: 'unsupported_language', - message: `${body.language} is not supported by Piston`, - }); - } else if (typeof body.source !== 'string') { - return res.status(400).json({ - code: 'missing_source', - message: 'source field is invalid', - }); - } else if (body.args && !Array.isArray(body.args)) { - return res.status(400).json({ - code: 'invalid_args', - message: 'args field is not an array', - }); - } - - launch(res, language, body); -}); - -async function launch(res, language, body) { - const stamp = new Date().getTime(); - const sourceFile = `/tmp/${stamp}.code`; - - await writeFile(sourceFile, body.source); - - const process = spawn(__dirname + '/../../lxc/execute', [language.name, sourceFile, (body.args ?? []).join('\n')]); - - const result = { - ran: true, - language: language.name, - stderr: '', - stdout: '', - output: '', - }; - - if (language.version) - result.version = language.version; - - process.stderr.addListener('data', chunk => { - result.stderr += chunk; - result.output += chunk; - }); - - process.stdout.addListener('data', chunk => { - result.stdout += chunk; - result.output += chunk; - }); - - result.stderr = result.stderr.trim().substring(0, 65535); - result.stdout = result.stdout.trim().substring(0, 65535); - result.output = result.output.trim().substring(0, 65535); - - process.on('exit', () => res.json(result)); -} - -app.get('/versions', (req, res) => { - res.json(languages); -}); +const { execute } = require('./execute'); +const { languages } = require('./languages'); +const { checkSchema, validationResult } = require('express-validator'); const PORT = 2000; + +const app = express(); +app.use(express.json()); + +app.post( + '/execute', + checkSchema({ + language: { + in: 'body', + notEmpty: { + errorMessage: 'Supply a language field', + }, + isString: { + errorMessage: 'Supplied language is not a string', + }, + custom: { + options: value => languages.find(language => language.name === value?.toLowerCase()), + errorMessage: 'Supplied language is not supported by Piston', + }, + }, + source: { + in: 'body', + notEmpty: { + errorMessage: 'Supply a source field', + }, + isString: { + errorMessage: 'Supplied source is not a string', + }, + }, + args: { + in: 'body', + optional: true, + isArray: { + errorMessage: 'Supplied args is not an array', + }, + } + }), + (req, res) => { + const errors = validationResult(req).array(); + + if (errors.length === 0) { + const language = languages.find(language => + language.aliases.includes(req.body.language.toLowerCase()) + ); + + execute(res, language, req.body); + } else { + res.status(400).json({ + message: errors[0].msg, + }); + } + }, +); + +app.get('/versions', (_, res) => res.json(languages)); + app.listen(PORT, () => console.log(`Listening on port ${PORT}`)); diff --git a/api/src/languages.js b/api/src/languages.js index 34409cb..9fda547 100644 --- a/api/src/languages.js +++ b/api/src/languages.js @@ -1,114 +1,153 @@ -module.exports = [ - { - name: 'nasm', - aliases: ['asm', 'nasm'], - }, - { - name: 'nasm64', - aliases: ['asm64', 'nasm64'], - }, - { - name: 'awk', - aliases: ['awk'], - }, - { - name: 'bash', - aliases: ['bash'], - }, - { - name: 'brainfuck', - aliases: ['bf', 'brainfuck'], - }, - { - name: 'c', - aliases: ['c'], - }, - { - name: 'csharp', - aliases: ['c#', 'cs', 'csharp'], - }, - { - name: 'cpp', - aliases: ['c++', 'cpp'], - }, - { - name: 'deno', - aliases: ['deno', 'denojs', 'denots'], - }, - { - name: 'ruby', - aliases: ['duby', 'rb', 'ruby'], - }, - { - name: 'emacs', - aliases: ['el', 'elisp', 'emacs'], - }, - { - name: 'elixir', - aliases: ['elixir'], - }, - { - name: 'haskell', - aliases: ['haskell', 'hs'], - }, - { - name: 'go', - aliases: ['go'], - }, - { - name: 'java', - aliases: ['java'], - }, - { - name: 'node', - aliases: ['javascript', 'js', 'node'], - }, - { - name: 'jelly', - aliases: ['jelly'], - }, - { - name: 'julia', - aliases: ['jl', 'julia'], - }, - { - name: 'kotlin', - aliases: ['kotlin'], - }, - { - name: 'lua', - aliases: ['lua'], - }, - { - name: 'paradoc', - aliases: ['paradoc'], - }, - { - name: 'perl', - aliases: ['perl'], - }, - { - name: 'php', - aliases: ['php', 'php3', 'php4', 'php5'], - }, - { - name: 'python3', - aliases: ['py', 'py3', 'python', 'python3'], - }, - { - name: 'python2', - aliases: ['python2'], - }, - { - name: 'rust', - aliases: ['rs', 'rust'], - }, - { - name: 'swift', - aliases: ['swift'], - }, - { - name: 'typescript', - aliases: ['ts', 'typescript'], - }, +const { spawn } = require('child_process'); + +const languages = [ + { + name: 'nasm', + aliases: ['asm', 'nasm'], + }, + { + name: 'nasm64', + aliases: ['asm64', 'nasm64'], + }, + { + name: 'awk', + aliases: ['awk'], + }, + { + name: 'bash', + aliases: ['bash'], + }, + { + name: 'brainfuck', + aliases: ['bf', 'brainfuck'], + }, + { + name: 'c', + aliases: ['c'], + }, + { + name: 'csharp', + aliases: ['c#', 'cs', 'csharp'], + }, + { + name: 'cpp', + aliases: ['c++', 'cpp'], + }, + { + name: 'deno', + aliases: ['deno', 'denojs', 'denots'], + }, + { + name: 'ruby', + aliases: ['duby', 'rb', 'ruby'], + }, + { + name: 'emacs', + aliases: ['el', 'elisp', 'emacs'], + }, + { + name: 'elixir', + aliases: ['elixir'], + }, + { + name: 'haskell', + aliases: ['haskell', 'hs'], + }, + { + name: 'go', + aliases: ['go'], + }, + { + name: 'java', + aliases: ['java'], + }, + { + name: 'node', + aliases: ['javascript', 'js', 'node'], + }, + { + name: 'jelly', + aliases: ['jelly'], + }, + { + name: 'julia', + aliases: ['jl', 'julia'], + }, + { + name: 'kotlin', + aliases: ['kotlin'], + }, + { + name: 'lua', + aliases: ['lua'], + }, + { + name: 'paradoc', + aliases: ['paradoc'], + }, + { + name: 'perl', + aliases: ['perl'], + }, + { + name: 'php', + aliases: ['php', 'php3', 'php4', 'php5'], + }, + { + name: 'python3', + aliases: ['py', 'py3', 'python', 'python3'], + }, + { + name: 'python2', + aliases: ['python2'], + }, + { + name: 'rust', + aliases: ['rs', 'rust'], + }, + { + name: 'swift', + aliases: ['swift'], + }, + { + name: 'typescript', + aliases: ['ts', 'typescript'], + }, ]; + +{ + const process = spawn(__dirname + '/../../lxc/versions'); + + let output = ''; + process.stderr.on('data', chunk => output += chunk); + process.stdout.on('data', chunk => output += chunk); + + process.on('exit', () => { + const sections = output.toLowerCase().split('---'); + const versions = {}; + + for (const section of sections) { + const lines = section.trim().split('\n'); + + if (lines.length >= 2) { + const language = lines[0]; + + if (language === 'java') { + versions[language] = /\d+/.exec(lines[1])?.[0]; + } else if (language === 'emacs') { + versions[language] = /\d+\.\d+/.exec(lines[1])?.[0]; + } else { + versions[language] = /\d+\.\d+\.\d+/.exec(section)?.[0]; + } + } + } + + for (const language of languages) { + language.version = versions[language.name]; + } + }); +} + +module.exports = { + languages, +}; diff --git a/lxc/execute b/lxc/execute index c33d7f2..42d5b52 100755 --- a/lxc/execute +++ b/lxc/execute @@ -4,11 +4,7 @@ dir="$( cd "$( dirname "$0" )" && pwd )" touch $dir/lockfile -if [ -z "$1" ]; then - echo "invalid args" - exit -fi -if [ -z "$2" ]; then +if [ -z "$1" ] || [ -z "$2" ]; then echo "invalid args" exit fi diff --git a/readme.md b/readme.md index 4baecf8..5b2cb85 100644 --- a/readme.md +++ b/readme.md @@ -167,7 +167,7 @@ Content-Type: application/json - typescript #### Principle of Operation -Piston utilizes LXC as the primary mechanism for sandboxing. There is a small API written in Go which takes +Piston utilizes LXC as the primary mechanism for sandboxing. There is a small API written in Node which takes in execution requests and executes them in the container. High level, the API writes a temporary source and args file to `/tmp` and that gets mounted read-only along with the execution scripts into the container. The source file is either ran or compiled and ran (in the case of languages like c, c++, c#, go, etc.).