Skip to content

Latest commit

 

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Terminal

A self-hosted, browser-based Linux terminal that gives every visitor a real, sandboxed Bash shell running on the server. It pairs a Python backend (Flask + Flask-SocketIO) with an xterm.js frontend and, when the host kernel allows, drops each session into an isolated Bubblewrap sandbox.

The repository also ships a Dockerfile that wraps Gitpod's openvscode-server image, rebrands it under the izzidevelopers name, pre-installs a curated set of Rust-focused VS Code extensions, and exposes the whole stack on port 3000.

⚠️ Use with care. A web-facing terminal is, by definition, remote code execution. The Bubblewrap sandboxing is intended to limit the blast radius, but you are responsible for authentication, rate-limiting, and where you deploy this.


✨ Features

  • True PTY-backed shell – every connected browser tab gets its own real bash --login session via pty.fork(), with correct TTY sizing handled through TIOCSWINSZ ioctls.
  • xterm.js UI – full xterm.js terminal with FitAddon so the canvas always matches the browser window size.
  • Bidirectional I/O over WebSockets – keystrokes go to the server via Socket.IO events (pty_input, resize); output is streamed back via pty_output.
  • Per-session isolation (when supported) – sessions are launched inside Bubblewrap with read-only bind mounts for /usr, /bin, /lib, /lib64, /sbin, /etc, in-memory tmpfs for /tmp and /root, and unshared PID/IPC/UTS namespaces. Outbound network access is preserved so curl, wget, git and friends still work.
  • Graceful fallback – if bwrap is missing or kernel namespaces are restricted, the server transparently falls back to a regular bash and logs a warning on startup.
  • Automatic cleanup – on disconnect or PTY EOF, the child process is SIGKILL'd and reaped, and the file descriptor is closed.
  • Container-ready – ships a Dockerfile that builds a fully rebranded, extension-loaded server image on top of gitpod/openvscode-server.

📁 Project layout

.
├── app.py              # Flask + Flask-SocketIO backend, PTY management, Bubblewrap integration
├── requirements.txt    # Python dependencies
├── Dockerfile          # Sandboxed, rebranded openvscode-server image
└── templates/
    └── index.html      # xterm.js + Socket.IO client UI

🚀 Quick start (local Python)

1. Install dependencies

python3 -m pip install -r requirements.txt

Dependencies:

  • Flask >= 3.0.0
  • Flask-SocketIO >= 5.3.6
  • gevent >= 23.9.1
  • gevent-websocket >= 0.10.1
  • gunicorn >= 21.2.0

2. (Optional) Install Bubblewrap

For session sandboxing on Linux:

# Debian / Ubuntu
sudo apt-get install -y bubblewrap

If bwrap is unavailable, the server will still start — sessions will simply run without an extra sandbox layer.

3. Run the development server

python3 app.py

By default Flask-SocketIO's dev server listens on http://localhost:5000. Open it in your browser and you should see the green Connected to terminal server. banner followed by a working shell prompt.

4. Run with Gunicorn (production-ish)

Because the project uses gevent.monkey.patch_all(), it's safe to run behind gunicorn:

gunicorn --worker-class gevent --workers 1 \
         --bind 0.0.0.0:5000 app:app

🐳 Docker

The included Dockerfile builds a more ambitious image: it takes Gitpod's openvscode-server, renames internal binaries and directories to izzidevelopers, swaps branding strings inside the compiled assets, sets a custom PS1 prompt, installs the stable Rust toolchain, and pre-installs three VS Code extensions:

  • rust-lang.rust-analyzer
  • tamasfe.even-better-toml
  • Continue.continue

Build and run:

docker build -t izzidevelopers/terminal .
docker run --rm -p 3000:3000 izzidevelopers/terminal

Then visit http://localhost:3000.

You can also enforce a connection token:

docker run --rm -p 3000:3000 \
  -e CONNECTION_TOKEN=please-change-me \
  izzidevelopers/terminal

If CONNECTION_TOKEN is unset, the container starts the server with --without-connection-token.


🧠 How it works

┌──────────────────┐         WebSocket         ┌──────────────────────┐
│  Browser         │  ◄─────────────────────►  │  Flask + Socket.IO   │
│  (xterm.js)      │   pty_input / pty_output  │  (app.py)            │
└──────────────────┘                           └─────────┬────────────┘
                                                         │ pty.fork()
                                                         ▼
                                               ┌──────────────────────┐
                                               │  bash --login        │
                                               │  (inside bwrap if    │
                                               │   kernel allows)     │
                                               └──────────────────────┘
  1. On connect, the backend forks a child process attached to a PTY, optionally executes it inside Bubblewrap, then begins a background gevent task that reads from the PTY and emits pty_output to the originating client only.
  2. Every keystroke from xterm.js is forwarded via pty_input and write()'d into the PTY.
  3. Window resize events update the PTY winsize so vim, htop, etc. redraw correctly.
  4. On disconnect or EOF, the child is killed, the FD is closed, and the session map entry is removed.

🔧 Configuration

Variable Default Description
SECRET_KEY fallback-secret-key Flask SECRET_KEY. Set this in production.
PORT 3000 (Docker) Listening port when running through the provided Dockerfile.
CONNECTION_TOKEN (unset) If set, the container's openvscode-server requires it.

The server has cors_allowed_origins="*". Tighten this in app.py before deploying publicly.


🛡️ Security notes

  • Run it behind authentication. There is currently no login layer; anyone reaching the page gets a shell.
  • The Bubblewrap sandbox is best-effort. It hides the host filesystem and isolates PID/IPC/UTS, but it deliberately keeps network access for normal tooling. Do not rely on it as a hard isolation boundary against a determined attacker.
  • Don't run as root in production. Drop capabilities, run the Python process as an unprivileged user, and consider adding --unshare-net if you want to disable outbound network per-session (this will break apt, pip, git clone, etc.).
  • Set a strong SECRET_KEY to protect Flask-SocketIO sessions.

🧪 Development tips

  • gevent.monkey.patch_all() must stay at the very top of app.py — Socket.IO needs the monkey-patched standard library before anything else imports.
  • Each browser tab = one PTY = one background task. Keep an eye on the terminals dict growth if you ever implement a "share session" feature.
  • To debug the sandbox, run the binary by hand:
    bwrap --ro-bind /usr /usr --ro-bind /bin /bin --ro-bind /lib /lib \
          --proc /proc --dev /dev --tmpfs /tmp --tmpfs /root \
          --unshare-pid --unshare-ipc --unshare-uts \
          --setenv TERM xterm-256color --setenv HOME /root \
          bash --login

📜 License

No license file is currently included in this repository. If you intend to redistribute or build on top of it, please add an explicit LICENSE file or contact the maintainers (izzidevelopers) for terms.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages