More about me / other projects: abhrankan.netlify.app
Remote Exec Server & Client is a minimal remote command execution framework written entirely with the Python standard library.
The project consists of:
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:
gp and 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.
REMOTE_EXEC_ALLOWED environment variableREMOTE_EXEC_TLS_CERT / REMOTE_EXEC_TLS_KEY (server) and REMOTE_EXEC_TLS (client)βββββββββββββββ
β Client β
β (symlink) β
ββββββββ¬βββββββ
β HTTP POST
βΌ
βββββββββββββββ
β Server β
β server.py β
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β subprocess β
β execution β
βββββββββββββββ
β
βΌ
βββββββββββββββ
β Output β
βββββββββββββββ
No external dependencies are required.
Start the server:
python server.py
Create a command symlink:
ln -s client.py gp
Execute a remote command:
echo "print(nextprime(100))" | ./gp -q
Copy server.py to the machine that will execute commands.
Run:
python server.py
Default listening address:
0.0.0.0:8000
Edit the server address:
host = "SERVER_IP:8000"
Make executable:
chmod +x client.py
Place 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/node
Each symlink name becomes the command executed remotely.
Set the server host:
host = "192.168.56.1:8000"
TLS, built in as of v1.4.0. Set environment variables before running the client:
REMOTE_EXEC_TLS=1 python client.py
For self-signed certificates (typical for local/dev deployments), also skip verification:
REMOTE_EXEC_TLS=1 REMOTE_EXEC_TLS_INSECURE=1 python client.py
REMOTE_EXEC_TLS_INSECURE disables certificate verification entirely β never set it when connecting over an untrusted network, only for local testing with a self-signed cert you generated yourself.
Bind only to localhost:
HTTPServer(("127.0.0.1", 8000), MyHandler)
Change port:
HTTPServer(("0.0.0.0", 9000), MyHandler)
TLS, built in as of v1.4.0. Set REMOTE_EXEC_TLS_CERT and REMOTE_EXEC_TLS_KEY to the paths of a PEM certificate and private key:
REMOTE_EXEC_TLS_CERT=cert.pem REMOTE_EXEC_TLS_KEY=key.pem python server.py
If either is unset, the server runs in plain HTTP (the original default behavior).
Generating a self-signed certificate for local testing:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"
For real deployments, use a certificate from a trusted CA (e.g. Letβs Encrypt) instead of a self-signed one, and omit REMOTE_EXEC_TLS_INSECURE on the client.
command_name [arguments]
echo "input data" | command_name [arguments]
ln -s client.py python
ln -s client.py node
ln -s client.py gp
The 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/plain
Request 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)
# stdin is written, and stdout/stderr are read, by three concurrent
# threads -- not sequentially. Writing all of stdin first (before
# anything drains stdout/stderr) deadlocks for any command whose
# input exceeds the OS pipe buffer (~64KB on Linux): the child fills
# its stdout pipe and blocks since nobody's reading yet, stops
# consuming stdin, and the stdin write then blocks too.
threading.Thread(target=write_stdin, args=(process, stdin_data)).start()
threading.Thread(target=read_stream, args=(process.stdout, "")).start()
threading.Thread(target=read_stream, args=(process.stderr, "[stderr] ")).start()
Output 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. stdin is written and stdout/stderr are read by three concurrent threads (fixed in v1.5.0 β previously stdin was written in a single blocking call before the stdout/stderr threads started, which deadlocked the entire single-threaded server for any command whose stdin exceeded the OS pipe buffer, roughly 64KB on Linux).
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 -q
echo "print('Hello World')" | python
echo "console.log('hello from node')" | node
echo "hello" | python script.py arg1 arg2
This project provides remote command execution capability and should be treated accordingly.
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.py
If 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.
Implement one or more of:
Run commands:
Built in as of v1.4.0. Set REMOTE_EXEC_TLS_CERT / REMOTE_EXEC_TLS_KEY on the server and REMOTE_EXEC_TLS=1 on the client β see Configuration. Use HTTPS/TLS whenever traffic crosses an untrusted network; a self-signed certificate is fine for local testing, but use a CA-issued certificate for real deployments.
Verify:
python server.py
Check:
Verify the executable exists:
which python
which node
which gp
Check:
[stderr] when applicableVerify:
host = "SERVER_IP:8000"
.
βββ client.py
βββ server.py
βββ README.md
βββ LICENSE
βββ CONTRIBUTING.md
βββ docs/
βββ diagram.png
Current implementation intentionally remains minimal.
Contributions are welcome.
Potential contribution areas:
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: