Last active
February 9, 2017 16:14
-
-
Save dcollien/4c8f80505bf6209c983548270baa2dfb to your computer and use it in GitHub Desktop.
HTTP-ify anything: A server which executes commands.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "http-exec", | |
| "version": "1.0.0", | |
| "description": "Command Execution Server", | |
| "main": "server.js", | |
| "scripts": { | |
| "test": "echo \"Error: no test specified\" && exit 1", | |
| "start": "node server.js" | |
| }, | |
| "author": "David Collien", | |
| "license": "ISC", | |
| "dependencies": { | |
| "command-line-usage": "^4.0.0", | |
| "form-data": "^2.1.2", | |
| "minimist": "^1.2.0", | |
| "stream-replace": "^1.0.0", | |
| "temp": "^0.8.3" | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use strict"; | |
| const http = require('http'); | |
| const exec = require('child_process').exec; | |
| const argv = require('minimist')(process.argv.slice(2)); | |
| const temp = require('temp').track(); | |
| const fs = require('fs'); | |
| const usage = require('command-line-usage'); | |
| const FormData = require('form-data'); | |
| const replace = require('stream-replace'); | |
| const PORT = argv['p'] || argv['port'] || 7113; | |
| const MIME_TYPE = argv['m'] || argv['mimetype'] || 'text/plain'; | |
| const FILE_TYPE = argv['f'] || argv['filetype']; | |
| const NO_MULTI = argv['n'] || argv['nomulti'] || false; | |
| const CMD = argv['c'] || argv['command']; | |
| const INPUT_FILE_EXT = argv['ext'] || argv['i'] || '.txt'; | |
| const OUTPUT_FILE_EXT = argv['oext'] || argv['o'] || '.txt'; | |
| const NO_LOG = argv['silent'] || argv['s'] || false; | |
| const INPUT_FILE_PATTERN = '{input}'; | |
| const OUTPUT_FILE_PATTERN = '{output}'; | |
| const CORS_ORIGIN = argv['cors'] || argv['x']; | |
| const CORS = CORS_ORIGIN === true ? '*' : CORS_ORIGIN; | |
| if (!CMD || argv['help']) { | |
| console.log(usage([ | |
| { | |
| header: 'Command Server', | |
| content: 'Run a server which responds with the output of a command.' | |
| }, | |
| { | |
| header: 'Options', | |
| optionList: [ | |
| { | |
| name: 'command', | |
| alias: 'c', | |
| typeLabel: '[underline]{command}', | |
| description: `Command to execute. The {${INPUT_FILE_PATTERN}} keyword is replaced with a filename containing the request body. If not present, request body is sent via stdin. The {${OUTPUT_FILE_PATTERN}} keyword is replaced with a filename of a temporary file which is to contain the response body. The {${OUTPUT_FILE_PATTERN}} keyword can only be used in conjunction with stdout if [bold]{--nomulti} is not set.` | |
| }, | |
| { | |
| name: 'port', | |
| alias: 'p', | |
| typeLabel: '[underline]{port}', | |
| description: 'Port to listen on.' | |
| }, | |
| { | |
| name: 'silent', | |
| alias: 's', | |
| description: 'Disable logging.' | |
| }, | |
| { | |
| name: 'mimetype', | |
| alias: 'm', | |
| typeLabel: '[underline]{mimetype}', | |
| description: 'Mime-Type which the command outputs to stdout (default: text/plain).' | |
| }, | |
| { | |
| name: 'filetype', | |
| alias: 'f', | |
| typeLabel: '[underline]{mimetype}', | |
| description: `Mime-Type which the command outputs to {${OUTPUT_FILE_PATTERN}} (default: [bold]{--mimetype}).` | |
| }, | |
| { | |
| name: 'ext', | |
| alias: 'i', | |
| typeLabel: '[underline]{extension}', | |
| description: `File extension (default .txt) which is used for the file given to {${INPUT_FILE_PATTERN}}.` | |
| }, | |
| { | |
| name: 'oext', | |
| alias: 'o', | |
| typeLabel: '[underline]{extension}', | |
| description: `File extension (default .txt) which is used for the file given to {${OUTPUT_FILE_PATTERN}}.` | |
| }, | |
| { | |
| name: 'cors', | |
| alias: 'x', | |
| typeLabel: '[underline]{origin (optional)}', | |
| description: `Enable CORS access headers, origin is '*' if unspecified.` | |
| }, | |
| { | |
| name: 'nomulti', | |
| alias: 'n', | |
| description: 'Response Mime-Type will become the same as [bold]{--mimetype} (rather than multipart/form-data). Either stdout will be returned (if no stderr), or stderr will be returned with a status 502' | |
| }, | |
| { | |
| name: 'help', | |
| description: 'Print this usage guide.' | |
| } | |
| ] | |
| } | |
| ])); | |
| process.exit(); | |
| } | |
| console.log(`Running server on port ${PORT}`); | |
| console.log(`Command: ${CMD}`); | |
| console.log(`Stdout Mime-Type: ${MIME_TYPE}`); | |
| console.log(`File Mime-Type: ${FILE_TYPE}`); | |
| console.log(`Response Mime-Type: ${NO_MULTI ? 'As above.' : 'multipart/form-data'}`); | |
| const CORS_HEADERS = CORS ? { | |
| 'Access-Control-Allow-Origin': CORS, | |
| 'Access-Control-Allow-Methods': 'POST, OPTIONS', | |
| 'Access-Control-Expose-Headers': 'Content-Type, X-Return-Code' | |
| } : {}; | |
| const operations = { | |
| processRequest(request, response) { | |
| if (request.method === 'OPTIONS') { | |
| const cors_headers = CORS ? { | |
| 'Access-Control-Allow-Origin': CORS, | |
| 'Access-Control-Allow-Methods': 'POST, OPTIONS', | |
| 'Access-Control-Allow-Headers': 'Content-Type, X-Return-Code' | |
| } : {}; | |
| response.writeHead(200, cors_headers); | |
| response.end(); | |
| } else if (request.method === 'GET') { | |
| response.writeHead(405, Object.assign({ | |
| 'Allow': 'POST, OPTIONS' | |
| }, CORS_HEADERS)); | |
| response.end('POST requests only.'); | |
| } else if (CMD.includes(INPUT_FILE_PATTERN)) { | |
| // Stream the request into a temporary file | |
| const file = temp.createWriteStream({suffix: INPUT_FILE_EXT}); | |
| const command = CMD.replace(INPUT_FILE_PATTERN, file.path); | |
| file.on('error', (error) => { | |
| operations.error(response, error); | |
| }); | |
| file.on('finish', () => { | |
| // when file has been written | |
| operations.exec(response, command, null, file.path); | |
| }); | |
| request.pipe(file); | |
| } else { | |
| operations.exec(response, CMD, request, null); | |
| } | |
| }, | |
| exec(response, commandToRun, stdin, filePath) { | |
| let outputPath = null; | |
| if (commandToRun.includes(OUTPUT_FILE_PATTERN)) { | |
| outputPath = temp.path({suffix: OUTPUT_FILE_EXT}); | |
| commandToRun = commandToRun.replace(OUTPUT_FILE_PATTERN, outputPath); | |
| } | |
| const child = exec(commandToRun); | |
| const stdinChunks = []; | |
| const stderrChunks = []; | |
| const stdoutChunks = []; | |
| const stdout = child.stdout; | |
| const stderr = child.stderr.pipe(replace(filePath, 'code' + INPUT_FILE_EXT)); | |
| if (stdin) { | |
| stdin.on('data', (data) => { | |
| stdinChunks.push(data); | |
| }); | |
| } | |
| stdout.on('data', (data) => { | |
| stdoutChunks.push(data); | |
| }); | |
| stderr.on('data', (data) => { | |
| stderrChunks.push(data); | |
| }); | |
| child.on('error', (error) => { | |
| operations.error(response, error); | |
| }); | |
| child.on('close', (code) => { | |
| const stdoutStr = stdoutChunks.join(''); | |
| const stderrStr = stderrChunks.join(''); | |
| operations.writeResponse(response, code, stdoutStr, stderrStr, outputPath); | |
| // Logging | |
| if (!NO_LOG) { | |
| const stdinStr = stdinChunks.join(''); | |
| if (stdinStr) { | |
| console.log('* In', '\n' + stdinStr); | |
| } else { | |
| console.log('* File (in)', filePath); | |
| } | |
| if (stdoutStr) { | |
| console.log('* Stdout', '\n' + stdoutStr); | |
| } | |
| if (stderrStr) { | |
| console.log('* Stderr', '\n' + stderrStr); | |
| } | |
| if (outputPath) { | |
| console.log('* File (out)', outputPath); | |
| } | |
| console.log('*******'); | |
| } | |
| }); | |
| if (stdin) { | |
| stdin.pipe(child.stdin); | |
| } | |
| }, | |
| writeResponse(response, code, stdout, stderr, outputPath) { | |
| if (NO_MULTI) { | |
| if (code !== 0) { | |
| // If the return code is non-zero, return stderr only with a 502 | |
| response.writeHead(502, Object.assign({ | |
| 'Content-Type': 'text/plain', | |
| 'X-Return-Code': '' + code | |
| }, CORS_HEADERS)); | |
| response.end(stderr); | |
| } else if (outputPath) { | |
| // Use the output file only | |
| response.writeHead(200, Object.assign({ | |
| 'Content-Type': FILE_TYPE || MIME_TYPE | |
| }, CORS_HEADERS)); | |
| fs.createReadStream(outputPath).pipe(response); | |
| } else if (stdout || !stderr) { | |
| // Use stdout only | |
| response.writeHead(200, Object.assign({ | |
| 'Content-Type': MIME_TYPE | |
| }, CORS_HEADERS)); | |
| response.end(stdout); | |
| } else { | |
| // No output file and no stdout, return stderr only with a 502 | |
| response.writeHead(502, Object.assign({ | |
| 'Content-Type': 'text/plain', | |
| 'X-Return-Code': '' + code | |
| }, CORS_HEADERS)); | |
| response.end(stderr); | |
| } | |
| } else { | |
| const form = new FormData(); | |
| form.append('stdout', stdout, { | |
| contentType: MIME_TYPE | |
| }); | |
| form.append('stderr', stderr, { | |
| contentType: 'text/plain' | |
| }); | |
| if (outputPath) { | |
| form.append('output', fs.createReadStream(outputPath), { | |
| contentType: FILE_TYPE || MIME_TYPE, | |
| filename: 'output' + OUTPUT_FILE_EXT | |
| }); | |
| } | |
| response.writeHead(200, Object.assign({ | |
| 'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`, | |
| 'X-Return-Code': '' + code | |
| }, CORS_HEADERS)); | |
| form.pipe(response); | |
| } | |
| }, | |
| error(response, error) { | |
| console.log(error); | |
| response.writeHead(500, Object.assign({ | |
| 'Content-Type': 'text/plain' | |
| }, CORS_HEADERS)); | |
| response.end('Error: Unable to execute due to a server issue.'); | |
| } | |
| }; | |
| http.createServer(operations.processRequest).listen(PORT); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage (e.g. python execution server):
node server.js --command "python {input}" --ext ".py" --nomultior just:
node server.js --command "python" --nomultior if the program outputs an image (filename taken as first argument):
node server.js --command "python {input} {output}" --ext ".py" --oext ".png" --filetype "image/png"will return multipart/form-data response with sections for stderr (text/plain), stdout (text/plain), and output (image/png)
Run some python:
Run some broken python: