I wanted to see how minimal a remote command execution system could be when restricted entirely to the Python standard library. No external dependencies. No frameworks. Just clean, standard networking primitives.
The result: Remote Exec Server & Client — a two-file system (server.py + client.py) with a BusyBox-style symlink architecture.
👉 github.com/foxhackerzdevs/remote-exec-server
The Problem
I run PARI/GP on a remote machine and wanted to pipe expressions to it from my local terminal — without SSH overhead, without installing PARI/GP locally, and without a heavyweight RPC framework.
The simplest possible solution: an HTTP server that accepts a command + stdin, runs it, and returns stdout.
The BusyBox Idea
BusyBox is a single binary that behaves differently depending on the name it's invoked under. ls, cp, mv — all the same binary, different symlinks.
I applied the same pattern to client.py. You create symlinks named after tools installed on the server:
ln -s client.py gp
ln -s client.py python
ln -s client.py node
When you invoke a symlink, client.py reads sys.argv[0] to determine which command to forward. That command, plus any arguments and stdin, gets packed into an HTTP POST request.
How It Works
server.py (26 lines)
#!/usr/bin/env python3
from http.server import BaseHTTPRequestHandler, HTTPServer
import subprocess, urllib.parse, shlex, os
class MyHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length).decode()
raw_path = urllib.parse.unquote(self.path.lstrip("/"))
cmd_parts = shlex.split(raw_path)
cmd_parts[0] = os.path.basename(cmd_parts[0])
try:
result = subprocess.run(
cmd_parts,
input=body.encode(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True
)
output = result.stdout.decode()
except subprocess.CalledProcessError as e:
output = f"Error:\n{e.stderr.decode()}"
except FileNotFoundError:
output = f"Error: command not found: {cmd_parts[0]}"
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(output.encode())
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", 8000), MyHandler)
print("Serving on 0.0.0.0:8000")
server.serve_forever()
client.py (13 lines)
#!/usr/bin/env python3
import http.client, sys, urllib.parse
host = "192.168.56.1:8000"
conn = http.client.HTTPConnection(host)
path = "/" + urllib.parse.quote(sys.argv[0] + " " + " ".join(sys.argv[1:]))
body = sys.stdin.read()
conn.request("POST", path, body=body)
response = conn.getresponse()
print(response.read().decode())
The protocol
The command and arguments travel in the URL path. Stdin travels in the request body. Stdout comes back in the response body. That's the entire protocol.
POST /gp%20-q HTTP/1.1
Host: 192.168.56.1:8000
Content-Type: text/plain
print(nextprime(100))
In Action
# Start the server on the remote machine
python server.py
# On the client machine
ln -s client.py gp
echo "print(nextprime(100))" | ./gp -q
# → 101
The Elephant in the Room: Security
This maps HTTP payloads directly to subprocess.run(). That's RCE by design. The baseline implementation has no authentication, no TLS, no rate limiting.
I built it this way intentionally — as a minimal blueprint that forces the operator to consciously design their own security layer rather than rely on defaults that provide a false sense of security.
Before any real deployment, the README covers:
Command whitelisting:
ALLOWED = {"python", "gp", "node"}
if cmd_parts[0] not in ALLOWED:
output = f"Command not allowed: {cmd_parts[0]}"
return
Network isolation: Bind to 127.0.0.1 and route through an SSH tunnel or VPN.
Sandboxing: Run the server inside a Docker container with restricted permissions.
What's Next
The core engine is solid. The roadmap includes:
- Native API-key and mTLS authentication
- Output streaming and async request handling
- Pre-configured Docker deployment
Contributions welcome — especially on the security and async fronts.
Top comments (0)