Use express-validator, refactor

This commit is contained in:
Vrganj 2021-01-16 00:53:51 +01:00
parent a0693fb3a2
commit e80bb0372b
6 changed files with 261 additions and 226 deletions

View File

@ -10,6 +10,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.17.1" "express": "^4.17.1",
"express-validator": "^6.9.2"
} }
} }

46
api/src/execute.js Normal file
View File

@ -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,
};

View File

@ -1,110 +1,63 @@
const { writeFile } = require('fs/promises');
const express = require('express'); const express = require('express');
const app = express(); const { execute } = require('./execute');
const languages = require('./languages'); const { languages } = require('./languages');
const { spawn } = require('child_process'); const { checkSchema, validationResult } = require('express-validator');
{
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 PORT = 2000; 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}`)); app.listen(PORT, () => console.log(`Listening on port ${PORT}`));

View File

@ -1,4 +1,6 @@
module.exports = [ const { spawn } = require('child_process');
const languages = [
{ {
name: 'nasm', name: 'nasm',
aliases: ['asm', 'nasm'], aliases: ['asm', 'nasm'],
@ -112,3 +114,40 @@ module.exports = [
aliases: ['ts', '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,
};

View File

@ -4,11 +4,7 @@ dir="$( cd "$( dirname "$0" )" && pwd )"
touch $dir/lockfile touch $dir/lockfile
if [ -z "$1" ]; then if [ -z "$1" ] || [ -z "$2" ]; then
echo "invalid args"
exit
fi
if [ -z "$2" ]; then
echo "invalid args" echo "invalid args"
exit exit
fi fi

View File

@ -167,7 +167,7 @@ Content-Type: application/json
- typescript - typescript
#### Principle of Operation #### 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 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. 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.). The source file is either ran or compiled and ran (in the case of languages like c, c++, c#, go, etc.).