Lightweight Python-based remote command execution system
One client script, multiple symlinks, BusyBox-style.
⚠️ WARNING
This software executes commands received over HTTP.
Never expose it directly to the public Internet without authentication, encryption, access controls, and proper isolation.
Overview • Installation • Usage • Security Considerations • License
- Overview
- Features
- Architecture
- Requirements
- Quick Start
- Installation
- Configuration
- Usage
- Protocol
- Examples
- Security Considerations
- Troubleshooting
- Project Structure
- Limitations
- Future Improvements
- Contributing
- License
Remote Exec Server & Client is a minimal remote command execution framework written entirely with the Python standard library.
The project consists of:
- server.py — receives HTTP requests and executes commands locally.
- client.py — forwards command invocations and standard input to the server.
- BusyBox-style symlink support — one client script can act as many commands depending on the name it is invoked under.
The design is intentionally lightweight, dependency-free, and easy to deploy.
The idea for Remote Exec Server & Client grew out of work on pari-gp-scripts, a collection of scripts for the PARI/GP number theory system.
While developing those scripts, I experimented with:
- Symlink wrappers — one script acting as multiple commands, BusyBox‑style.
- Lightweight tooling — keeping dependencies minimal and relying on the standard library.
- Command forwarding — piping input into
gpand other interpreters for quick execution.
These patterns sparked the realization that the same approach could be generalized: instead of just wrapping gp locally, why not design a framework where one client script can forward commands to a server over HTTP?
Thus, Remote Exec Server & Client was born — a minimal, dependency‑free system for remote command execution, inspired by the simplicity and flexibility of the PARI/GP scripting workflow.
- HTTP-based command forwarding
- Standard input (stdin) forwarding
- Command-line argument support
- Real-time output streaming (chunked transfer encoding) — live progress bars and interactive output display correctly
- Optional command whitelist via
REMOTE_EXEC_ALLOWEDenvironment variable - BusyBox-style symlink invocation
- Works with any executable installed on the server
- No third-party dependencies
- Cross-platform Python implementation
- Minimal setup and deployment
┌─────────────┐
│ Client │
│ (symlink) │
└──────┬──────┘
│ HTTP POST
▼
┌─────────────┐
│ Server │
│ server.py │
└──────┬──────┘
│
▼
┌─────────────┐
│ subprocess │
│ execution │
└─────────────┘
│
▼
┌─────────────┐
│ Output │
└─────────────┘
- Python 3.8+
- Network connectivity between client and server
No external dependencies are required.
Start the server:
python server.pyCreate a command symlink:
ln -s client.py gpExecute a remote command:
echo "print(nextprime(100))" | ./gp -qCopy server.py to the machine that will execute commands.
Run:
python server.pyDefault listening address:
0.0.0.0:8000
Edit the server address:
host = "SERVER_IP:8000"Make executable:
chmod +x client.pyPlace in your PATH:
cp client.py ~/bin/Create command aliases:
ln -s ~/bin/client.py ~/bin/gp
ln -s ~/bin/client.py ~/bin/python
ln -s ~/bin/client.py ~/bin/nodeEach symlink name becomes the command executed remotely.
Set the server host:
host = "192.168.56.1:8000"To enable HTTPS:
http.client.HTTPSConnectioninstead of:
http.client.HTTPConnectionThe server must also be configured for TLS.
Bind only to localhost:
HTTPServer(("127.0.0.1", 8000), MyHandler)Change port:
HTTPServer(("0.0.0.0", 9000), MyHandler)command_name [arguments]echo "input data" | command_name [arguments]ln -s client.py python
ln -s client.py node
ln -s client.py gpThe invoked name determines the command sent to the server.
The communication protocol is intentionally simple.
POST /python script.py arg1 arg2 HTTP/1.1
Host: server:8000
Content-Type: text/plainRequest body:
stdin data goes here
cmd_parts = shlex.split(raw_path)
process = subprocess.Popen(cmd_parts, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process.stdin.write(stdin_data)
process.stdin.close() # signal EOF to the subprocess
# stdout and stderr are read by two concurrent threads feeding a shared
# queue, so neither stream can block or lose data while waiting on the otherOutput is streamed to the client line by line as the subprocess produces it, using HTTP chunked transfer encoding — rather than waiting for the process to exit and sending the full output at once. stdout and stderr are captured concurrently by separate reader threads, so stderr output is never lost and can't deadlock the subprocess by filling its pipe buffer.
The response uses Transfer-Encoding: chunked. Each line is sent as a separate chunk as soon as it's produced, so long-running or interactive commands (e.g. progress bars) display in real time on the client rather than appearing all at once at the end.
stdout lines are streamed as-is. stderr lines are streamed live too, prefixed with [stderr] so they're distinguishable from stdout in the client's terminal:
stdout line 1
[stderr] warning: something noteworthy
stdout line 2
Both streams are forwarded regardless of the process's exit code — not just on failure.
echo "print(nextprime(100))" | gp -qecho "print('Hello World')" | pythonecho "console.log('hello from node')" | nodeecho "hello" | python script.py arg1 arg2This project provides remote command execution capability and should be treated accordingly.
- Arbitrary command execution
- Remote code execution (RCE)
- Unauthorized access
- Resource exhaustion
- Data exposure
Built in as of v1.3.0. Set the REMOTE_EXEC_ALLOWED environment variable to a comma-separated list of allowed command names before starting the server:
REMOTE_EXEC_ALLOWED="gp,python,node" python server.pyIf unset or empty, all commands are allowed (the original default behavior) — setting this is strongly recommended for any network-exposed deployment. Disallowed commands receive a 403 Forbidden response.
- VPN access only
- SSH tunnels
- Firewall allowlists
- Private networks
Implement one or more of:
- API keys
- Bearer tokens
- Mutual TLS
- Basic authentication
Run commands:
- Inside Docker containers
- Inside restricted user accounts
- Inside sandboxes
Use HTTPS/TLS whenever traffic crosses untrusted networks.
Verify:
python server.pyCheck:
- Server is running
- Correct IP address
- Correct port
- Firewall configuration
Verify the executable exists:
which python
which node
which gpCheck:
- Server logs
- Whether the command writes to stdout or stderr — both are streamed live and prefixed
[stderr]when applicable - Input handling
Verify:
host = "SERVER_IP:8000".
├── client.py
├── server.py
├── README.md
├── LICENSE
├── CONTRIBUTING.md
└── docs/
└── diagram.png
Current implementation intentionally remains minimal.
- No authentication
- No TLS support by default
- No concurrency (single request handled at a time)
- No request validation
- No rate limiting
- No audit logging
- No file transfer support
- HTTPS/TLS support
- API-key authentication
- Mutual TLS
- Async request handling
- Docker deployment
- Audit logging
- Request signing
- Rate limiting
- File upload/download support
Contributions are welcome.
Potential contribution areas:
- Security enhancements
- Protocol improvements
- Testing
- Documentation
- Containerization
- Performance optimization
Feel free to open issues, submit pull requests, or fork the project.
See CONTRIBUTING.md for guidelines on documentation, security, development, and the contribution flow.
SPDX-License-Identifier: MIT
This project is licensed under the MIT License — see the repository LICENSE file for the full text:
- PARI/GP Scripts —
A collection of Bash wrappers for PARI/GP number theory experiments that inspired the design of this project.
