diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3abe3d4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4d98a89 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM ubuntu +WORKDIR /usr/src/app +COPY package*.json ./ +COPY . . +RUN apt-get update +RUN apt-get install curl -y +RUN curl -sL https://deb.nodesource.com/setup_10.x | bash +RUN apt-get install nodejs -y +RUN npm install -y +RUN apt-get install \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg-agent \ + software-properties-common -y +RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - +RUN add-apt-repository \ + "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) \ + stable" +RUN apt-get update +RUN apt-get install docker-ce docker-ce-cli containerd.io -y +CMD ["npm","run","dev"] diff --git a/Dockerfiles/DockerCPP b/Dockerfiles/DockerCPP new file mode 100644 index 0000000..8d8c820 --- /dev/null +++ b/Dockerfiles/DockerCPP @@ -0,0 +1,4 @@ +FROM ubuntu +WORKDIR /usr/src/app +RUN apt-get update +RUN apt-get install build-essential -y diff --git a/Dockerfiles/DockerJava b/Dockerfiles/DockerJava new file mode 100644 index 0000000..e71e2e5 --- /dev/null +++ b/Dockerfiles/DockerJava @@ -0,0 +1,4 @@ +FROM ubuntu +WORKDIR /usr/src/app +RUN apt-get update +RUN apt-get install openjdk-8-jdk -y diff --git a/Dockerfiles/DockerPython b/Dockerfiles/DockerPython new file mode 100644 index 0000000..825f062 --- /dev/null +++ b/Dockerfiles/DockerPython @@ -0,0 +1,7 @@ +FROM ubuntu +WORKDIR /usr/src/app +RUN apt-get update +RUN apt-get install software-properties-common -y +RUN add-apt-repository ppa:deadsnakes/ppa -y +RUN apt-get update +RUN apt-get install python3.8 -y diff --git a/LICENSE b/LICENSE index 72d2fc6..408991c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2020 Abhigyan Abhikaushalam Students' Forum - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2020 Rajat Maheshwari + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c2de498..d01700a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,182 @@ -# Remote Code Executor -- Server Side Code for a Remote Code Executor +

+ + + +

Remote Code Executor

+ +

+ Server-side code of a Remote Code Executor +
+ Report Bug + ยท + Request Feature +

+

+ + +
+

Table of Contents

+
    +
  1. + About The Project + +
  2. +
  3. + Getting Started + +
  4. +
  5. Usage
  6. +
  7. Roadmap
  8. +
  9. Contributing
  10. +
  11. License
  12. +
  13. Contact
  14. +
+
+ + + +## About The Project + +This is the server-side code of a Remote Code Executor. This is a project assigned by the Coding forum of my college and is similar to the Online IDEs of websites like CodeChef and Leetcode. + +Salient Features: + +- Code Sanitisation +- An individual Docker Container is created for every code posted on the API, so no code interferes with any other code +- All Async code so that the server can handle multiple requests without error + +### Built With + + nodejs + docker + + + +## Getting Started + +To get a local copy up and running follow these simple steps. + +### Prerequisites + +- npm + ```sh + npm install npm@latest -g + ``` +- docker + ```sh + curl -fsSL https://get.docker.com -o get-docker.sh + ``` + ```sh + sudo sh get-docker.sh + ``` + +### Installation +#### If you are on a Linux(preferably Ubuntu) Machine +1. Clone the repo + ```sh + git clone https://github.com/rajatmaheshwari2512/remote-code-exec + ``` +2. Install NPM packages + ```sh + npm install + ``` +3. To build the docker images + ```sh + cd Dockerfiles + ``` + ```sh + docker build -t cpp:v1 -f DockerCPP . + ``` + ```sh + docker build -t python:v1 -f DockerPython . + ``` + ```sh + docker build -t java:v1 -f DockerJava . + ``` +#### If you are on any other Machine +1. Clone the repo + ```sh + git clone https://github.com/rajatmaheshwari2512/remote-code-exec + ``` +2. Install NPM packages + ```sh + npm install + ``` +3. Build the Server's Dockerfile + ```sh + docker build -t rceserver:v1 . + ``` +4. Run the Docker Image + ```sh + docker run --privileged=true -v /var/run/docker.sock:/var/run/docker.sock -d -p 3000:3000 rceserver:v1 + ``` +5. Create a shell to the created Docker Container + ```sh + docker ps + ``` + ```sh + docker exec -it /bin/bash + ``` +6. Build the Images inside the Container + ```sh + cd Dockerfiles + ``` + ```sh + docker build -t cpp:v1 -f DockerCPP . + ``` + ```sh + docker build -t python:v1 -f DockerPython . + ``` + ```sh + docker build -t java:v1 -f DockerJava . + ``` + + +## Usage + +1. To run the server in dev mode use + ```sh + npm run dev + ``` +2. To run the server in production mode + ```sh + npm start + ``` +3. Note that dev mode uses nodemon so that the server can be changed and restarted easily + + + +## Roadmap + +See the [open issues](https://github.com/rajatmaheshwari2512/remote-code-exec/issues) for a list of proposed features (and known issues). + + + +## Contributing + +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + + + +## License + +Distributed under the MIT License. See `LICENSE` for more information. + + + +## Contact + +Rajat Maheshwari - mrajat67@yahoo.com + +Project Link: [https://github.com/rajatmaheshwari2512/remote-code-exec](https://github.com/rajatmaheshwari2512/remote-code-exec) \ No newline at end of file diff --git a/app.js b/app.js index fa7e6d6..07dc080 100644 --- a/app.js +++ b/app.js @@ -4,6 +4,8 @@ var cookieParser = require("cookie-parser"); var logger = require("morgan"); var codeRouter = require("./routes/codeUpload"); var cors = require("cors"); +var swaggerUi = require("swagger-ui-express"), + swaggerDocument = require("./swagger.json"); var app = express(); @@ -14,6 +16,7 @@ app.use(cookieParser()); app.use(cors()); app.use("/codeupload", codeRouter); +app.use("/", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); app.use(function (req, res, next) { next(createError(404)); diff --git a/bin/www b/bin/www index 91605aa..e1fac40 100644 --- a/bin/www +++ b/bin/www @@ -51,4 +51,4 @@ function onError(error) { function onListening() { console.log("Server running on localhost:" + port); -} +} \ No newline at end of file diff --git a/languages/cpp.js b/languages/cpp.js index a66720b..7ee3f47 100644 --- a/languages/cpp.js +++ b/languages/cpp.js @@ -2,25 +2,34 @@ const util = require("util"); const exec = util.promisify(require("child_process").exec); var fs = require("fs"); -const cpp = (input, res) => { - fs.writeFile("input.txt", input, (err) => { +const cpp = (input, res, name) => { + fs.writeFile(`${name}.txt`, input, (err) => { if (err) res.json({ error: err }); - exec("g++ input.cpp && ./a.out { - res.json(result); - exec("rm input.cpp && rm a.out").then((resp) => - console.log("CPP File Deleted") - ); - exec("rm input.txt").then((resp) => console.log("Input CPP Deleted")); - }) - .catch((err) => { - res.json(err); - exec("rm input.cpp").then((resp) => console.log("CPP File Deleted")); - exec("rm input.txt").then((resp) => console.log("Input CPP Deleted")); - }); + exec("docker run -d -it cpp:v1 /bin/bash").then((resp) => { + var id = resp.stdout.substring(0, 12); + exec( + `docker cp ${name}.cpp ${id}:/usr/src/app/test.cpp && docker cp ${name}.txt ${id}:/usr/src/app/input.txt && docker exec ${id} bash -c "g++ test.cpp && ./a.out { + res.json(resp); + exec(`rm ${name}.cpp && rm ${name}.txt`).then((resp) => + console.log("Files removed") + ); + exec(`docker kill ${id}`).then((resp) => + console.log("Container Stopped") + ); + }) + .catch((err) => { + res.json(err); + exec(`rm ${name}.cpp && rm ${name}.txt`).then((resp) => + console.log("Files removed") + ); + exec(`docker kill ${id}`).then((resp) => + console.log("Container Stopped") + ); + }); + }); }); }; -module.exports = cpp; +module.exports = cpp; \ No newline at end of file diff --git a/languages/java.js b/languages/java.js new file mode 100644 index 0000000..15ba44c --- /dev/null +++ b/languages/java.js @@ -0,0 +1,35 @@ +const util = require("util"); +const exec = util.promisify(require("child_process").exec); +var fs = require("fs"); + +const java = (input, res, name) => { + fs.writeFile(`${name}.txt`, input, (err) => { + if (err) res.json({ error: err }); + exec("docker run -d -it java:v1 /bin/bash").then((resp) => { + var id = resp.stdout.substring(0, 12); + exec( + `docker cp ${name}.java ${id}:/usr/src/app/test.java && docker cp ${name}.txt ${id}:/usr/src/app/input.txt && docker exec ${id} bash -c "javac test.java && java test { + res.json(resp); + exec(`rm ${name}.java && rm ${name}.txt`).then((resp) => + console.log("Files removed") + ); + exec(`docker kill ${id}`).then((resp) => + console.log("Container Stopped") + ); + }) + .catch((err) => { + res.json(err); + exec(`rm ${name}.java && rm ${name}.txt`).then((resp) => + console.log("Files removed") + ); + exec(`docker kill ${id}`).then((resp) => + console.log("Container Stopped") + ); + }); + }); + }); +}; +module.exports = java; diff --git a/languages/python.js b/languages/python.js index ceb06bc..fef4e67 100644 --- a/languages/python.js +++ b/languages/python.js @@ -2,23 +2,34 @@ const util = require("util"); const exec = util.promisify(require("child_process").exec); var fs = require("fs"); -const python = (input, res) => { - fs.writeFile("input.txt", input, (err) => { +const python = (input, res, name) => { + fs.writeFile(`${name}.txt`, input, (err) => { if (err) res.json({ error: err }); - exec("python input.py { - res.json(result); - exec("rm input.py").then((resp) => console.log("PY File Deleted")); - exec("rm input.txt").then((resp) => console.log("Input PY Deleted")); - }) - .catch((err) => { - res.json(err); - exec("rm input.py").then((resp) => console.log("PY File Deleted")); - exec("rm input.txt").then((resp) => console.log("Input PY Deleted")); - }); + exec("docker run -d -it python:v1 /bin/bash").then((resp) => { + var id = resp.stdout.substring(0, 12); + exec( + `docker cp ${name}.py ${id}:/usr/src/app/test.py && docker cp ${name}.txt ${id}:/usr/src/app/input.txt && docker exec ${id} bash -c "python3 test.py { + res.json(resp); + exec(`rm ${name}.py && rm ${name}.txt`).then((resp) => + console.log("Files removed") + ); + exec(`docker kill ${id}`).then((resp) => + console.log("Container Stopped") + ); + }) + .catch((err) => { + res.json(err); + exec(`rm ${name}.py && rm ${name}.txt`).then((resp) => + console.log("Files removed") + ); + exec(`docker kill ${id}`).then((resp) => + console.log("Container Stopped") + ); + }); + }); }); }; module.exports = python; diff --git a/package-lock.json b/package-lock.json index f5114fb..33e5c97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-uniq": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.2.tgz", + "integrity": "sha1-X8w3OSB3VyPP1k1lxkvvU7+eum0=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -1037,6 +1042,14 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, + "randomstring": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/randomstring/-/randomstring-1.1.5.tgz", + "integrity": "sha1-bfBij3XL1ZMpMNn+OrTpVqGFGMM=", + "requires": { + "array-uniq": "1.0.2" + } + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1234,6 +1247,19 @@ "has-flag": "^3.0.0" } }, + "swagger-ui-dist": { + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.38.0.tgz", + "integrity": "sha512-sselV8VY6f1BBauY9Sdmwz0jVaWTnGuHQWei7BaTpiUrLcoEUdmmK5bKefLXiwq+dx//es2S8mOvUS+tcXDsKg==" + }, + "swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-Xs2BGGudvDBtL7RXcYtNvHsFtP1DBFPMJFRxHe5ez/VG/rzVOEjazJOOSc/kSCyxreCTKfJrII6MJlL9a6t8vw==", + "requires": { + "swagger-ui-dist": "^3.18.1" + } + }, "term-size": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", @@ -1385,4 +1411,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 3265b08..4d5b972 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ "debug": "~2.6.9", "express": "~4.16.1", "http-errors": "~1.6.3", - "morgan": "~1.9.1" + "morgan": "~1.9.1", + "randomstring": "^1.1.5", + "swagger-ui-express": "^4.1.6" }, "devDependencies": { "nodemon": "^2.0.6" } -} +} \ No newline at end of file diff --git a/routes/codeUpload.js b/routes/codeUpload.js index 5cf9561..4815d1d 100644 --- a/routes/codeUpload.js +++ b/routes/codeUpload.js @@ -1,10 +1,17 @@ var express = require("express"); var fs = require("fs"); +const util = require("util"); +const exec = util.promisify(require("child_process").exec); +var randomstring = require("randomstring"); var cppRun = require("../languages/cpp"); var pythonRun = require("../languages/python"); +var javaRun = require("../languages/java"); var { languageCode } = require("../shared/languageCode"); var { cppList } = require("../shared/blacklist"); +var { pythonList } = require("../shared/blacklist"); +var { javaList } = require("../shared/blacklist"); +var validate = require("../shared/validate"); var router = express.Router(); @@ -16,13 +23,38 @@ router const code = req.body.code; const langid = req.body.langid; const input = req.body.input; - fs.writeFile(`input.${languageCode[langid]}`, code, (err) => { + var name = randomstring.generate({ + length: 7, + charset: "alphabetic", + }); + fs.writeFile(`${name}.${languageCode[langid]}`, code, (err) => { if (err) res.json({ error: err }); if (langid == 1) { - cppRun(input, res); + if (validate(cppList, code)) { + cppRun(input, res, name); + } else { + res.json({ error: "invalid code" }); + exec(`rm ${name}.cpp`).then((resp) => + console.log("Input CPP Deleted") + ); + } } else if (langid == 2) { - pythonRun(input, res); + if (validate(pythonList, code)) { + pythonRun(input, res, name); + } else { + res.json({ error: "invalid code" }); + exec(`rm ${name}.py`).then((resp) => console.log("Input PY Deleted")); + } + } else if (langid == 3) { + if (validate(javaList, code)) { + javaRun(input, res, name); + } else { + res.json({ error: "invalid code" }); + exec(`rm ${name}.java`).then((resp) => + console.log("Input JAVA Deleted") + ); + } } }); }); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/shared/blacklist.js b/shared/blacklist.js index 2a6a466..b1da914 100644 --- a/shared/blacklist.js +++ b/shared/blacklist.js @@ -1,4 +1,12 @@ -var pythonList = ["os", "subprocess"]; -var cppList = ["bits/stdc++.h", "stdlib.h"]; +var pythonList = ["import os", "import subprocess"]; +var cppList = ["popen", "fork", "system(", "unistd.h", "exec"]; +var javaList = [ + "Process", + "getRuntime()", + "exec(", + "ProcessBuilder", + "start()", +]; exports.pythonList = pythonList; exports.cppList = cppList; +exports.javaList = javaList; \ No newline at end of file diff --git a/shared/languageCode.js b/shared/languageCode.js index e475aec..16e0914 100644 --- a/shared/languageCode.js +++ b/shared/languageCode.js @@ -1,4 +1,5 @@ var languageCode = new Map(); languageCode[1] = "cpp"; languageCode[2] = "py"; -exports.languageCode = languageCode; +languageCode[3] = "java"; +exports.languageCode = languageCode; \ No newline at end of file diff --git a/shared/validate.js b/shared/validate.js new file mode 100644 index 0000000..073ffca --- /dev/null +++ b/shared/validate.js @@ -0,0 +1,11 @@ +const fs = require("fs"); + +const validate = (list, code) => { + for (let i = 0; i < list.length; i++) { + if (code.includes(list[i])) { + return false; + } + } + return true; +}; +module.exports = validate; diff --git a/swagger.json b/swagger.json new file mode 100644 index 0000000..24447a3 --- /dev/null +++ b/swagger.json @@ -0,0 +1,99 @@ +{ + "swagger":"2.0", + "info":{ + "version":"1.0.0", + "title":"API Explorer for the Remote Code Executor", + "description":"API Sandbox for you to familiarize yourself with the API of this server", + "license":{ + "name":"MIT", + "url":"https://opensource.org/licenses/MIT" + } + }, + "host":"localhost:3000", + "basePath":"/", + "schemes":[ + "http" + ], + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "paths":{ + "/codeUpload":{ + "get":{ + "tags":[ + "codeUpload" + ], + "summary":"Check Health of the API and working status of the server", + "responses":{ + "200":{ + "description":"OK", + "schema":{ + "$ref":"#/definitions/Health" + } + } + } + }, + "post":{ + "tags":[ + "codeUpload" + ], + "parameters":[ + { + "name":"Input Body", + "in":"body", + "description":"The Body of the Request", + "schema":{ + "$ref":"#/definitions/Code" + } + } + ], + "summary":"Post the Language, the Code and the Input at this endpoint", + "responses":{ + "200":{ + "description":"OK", + "schema":{ + "$ref":"#/definitions/Response" + } + } + } + } + } + }, + "definitions":{ + "Response":{ + "properties":{ + "stdout":{ + "type":"string" + }, + "stderr":{ + "type":"string" + } + } + }, + "Health":{ + "properties":{ + "APIWorking":{ + "type":"boolean", + "default":true + } + } + }, + "Code":{ + "properties":{ + "langid":{ + "type":"integer", + "default":2 + }, + "code":{ + "type":"string" + }, + "input":{ + "type":"string" + } + } + } + } +} \ No newline at end of file