├── .gitignore ├── package.json ├── .github └── workflows │ └── deploy.yml ├── fly.toml ├── Dockerfile ├── server.js ├── LICENSE.md ├── README.md └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "python-in-a-box", 3 | "version": "0.1.0", 4 | "description": "Python in the cloud, 30 lines of code", 5 | "author": "Radian LLC ", 6 | "license": "MIT", 7 | "private": true, 8 | "dependencies": { 9 | "express": "^4.17.1", 10 | "express-ws": "^4.0.0", 11 | "node-pty": "^0.10.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | env: 7 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 8 | jobs: 9 | deploy: 10 | name: Deploy 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: superfly/flyctl-actions/setup-flyctl@master 15 | - run: flyctl deploy --remote-only 16 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | app = "python-in-a-box" 2 | primary_region = "sjc" 3 | kill_signal = "SIGINT" 4 | kill_timeout = "5s" 5 | 6 | [experimental] 7 | auto_rollback = true 8 | 9 | [[services]] 10 | protocol = "tcp" 11 | internal_port = 8081 12 | processes = ["app"] 13 | 14 | [[services.ports]] 15 | port = 80 16 | handlers = ["http"] 17 | force_https = true 18 | 19 | [[services.ports]] 20 | port = 443 21 | handlers = ["tls", "http"] 22 | [services.concurrency] 23 | type = "connections" 24 | hard_limit = 25 25 | soft_limit = 20 26 | 27 | [[services.tcp_checks]] 28 | interval = "15s" 29 | timeout = "2s" 30 | grace_period = "1s" 31 | restart_limit = 0 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM radiansoftware/sleeping-beauty:v4.0.0 AS sleepingd 2 | 3 | # EOL April 2027 4 | FROM ubuntu:22.04 5 | 6 | RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* 7 | RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && rm -rf /var/lib/apt/lists/* 8 | RUN apt-get update && apt-get install -y g++ make nodejs python3 tini && rm -rf /var/lib/apt/lists/* 9 | 10 | WORKDIR /src 11 | COPY package.json package-lock.json /src/ 12 | RUN npm ci 13 | 14 | COPY index.html server.js /src/ 15 | 16 | COPY --from=sleepingd /sleepingd /usr/local/bin/sleepingd 17 | ENTRYPOINT ["/usr/bin/tini", "--"] 18 | 19 | ENV SLEEPING_BEAUTY_COMMAND="PORT=8080 node server.js" 20 | ENV SLEEPING_BEAUTY_TIMEOUT_SECONDS=60 21 | ENV SLEEPING_BEAUTY_COMMAND_PORT=8080 22 | ENV SLEEPING_BEAUTY_LISTEN_PORT=8081 23 | 24 | RUN useradd -p '!' -m -l pythoninabox 25 | 26 | CMD ["sleepingd"] 27 | USER pythoninabox 28 | EXPOSE 8081 29 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const process = require("process"); 3 | 4 | const express = require("express"); 5 | const ws = require("express-ws"); 6 | const pty = require("node-pty"); 7 | 8 | const html = fs.readFileSync("index.html"); 9 | 10 | const app = express(); 11 | 12 | const expressWs = ws(app); 13 | 14 | app.get("/", (req, res) => { 15 | res.setHeader("Content-Type", "text/html"); 16 | res.send(html); 17 | }); 18 | app.ws("/ws", (ws) => { 19 | const term = pty.spawn("python3", [], { name: "xterm-color" }); 20 | setTimeout(() => term.kill(), 3600 * 1000); // session timeout 21 | term.on("data", (data) => { 22 | try { 23 | ws.send(data); 24 | } catch (err) {} 25 | }); 26 | ws.on("message", (data) => term.write(data)); 27 | }); 28 | 29 | // Prevent malformed packets from crashing server. 30 | expressWs.getWss().on("connection", (ws) => ws.on("error", console.error)); 31 | 32 | app.listen(parseInt(process.env.PORT), "0.0.0.0"); 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2021–2022 [Radian LLC](https://radian.codes) and 4 | contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python in a Box 2 | 3 | Try it online: 4 | 5 | This is an interactive online Python REPL, implemented in JavaScript 6 | using 7 | 8 | * **thirteen** lines of [code on the frontend](https://github.com/radian-software/python-in-a-box/blob/b28a39383c6b66098b414edf4a1b4165a5d11ca2/index.html#L32-L48) 9 | * **seventeen** lines of [code on the backend](https://github.com/radian-software/python-in-a-box/blob/b28a39383c6b66098b414edf4a1b4165a5d11ca2/server.js#L1-L23) 10 | 11 | and based on the open-source libraries 12 | 13 | * [Express](https://expressjs.com/) 14 | * [node-pty](https://github.com/microsoft/node-pty) 15 | * [Xterm.js](https://xtermjs.org/) 16 | 17 | Read the blog post, [How Replit used legal threats to kill my open-source project](https://intuitiveexplanations.com/tech/replit/). 18 | 19 | Also, this should go without saying, but **letting people run 20 | unsandboxed code on your server is incredibly stupid**. Do not ever, 21 | ever do this in production. This repository demonstrates a proof of 22 | concept only, and does *not* reflect appropriate ethical practices for 23 | handling of user data. 24 | 25 | If you'd like to see a service that actually *does* attempt to run 26 | user code in a secure manner, please check out 27 | [Riju](https://github.com/raxod502/riju). 28 | 29 | *Note:* Please do not attempt to do malicious things with the hosted 30 | version of this application, including using it for free compute. It 31 | is running on an isolated Railway account that will automatically 32 | terminate service if the free-tier limits are exceeded or if abuse is 33 | registered. So all you will accomplish is taking the service offline 34 | for everyone else. 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Python in a Box 6 | 12 | 17 | 22 | 23 | 24 |

25 | Python in a Box 28 |

29 |
30 |

31 | Note: if the terminal has frozen, reload the page. This happens because 32 | the frontend does not have any logic to automatically reconnect when the 33 | websocket connection is broken, which happens frequently in modern 34 | browsers when you switch to a different tab. 35 |

36 | 37 | 38 | 39 | 58 | 59 | 60 | --------------------------------------------------------------------------------