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)
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 other
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. 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 -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: