- Rust 81.5%
- Shell 18.5%
| src | ||
| .gitignore | ||
| Cargo.toml | ||
| install | ||
| README.md | ||
| uninstall | ||
Command server
An HTTP server that executes pre-approved binaries on demand. Requests post a command and arguments; the server checks that the binary is allowed in ~/.config/cmdd/config.toml and runs it. Commands run in parallel unless a queue is specified in the request or config, in which case commands sharing that queue execute one-at-a-time.
Configuration
Create ~/.config/cmdd/config.toml (or pass --config with another path):
api_keys = ["super-secret-key"]
[[command]]
binary = "/usr/bin/echo"
queue = "echo-queue"
fork = true
[[command]]
binary = "/usr/bin/date"
allow_args = false
Fields:
api_keys(required): list of secrets clients must send via theX-API-Keyheader. Rotate these frequently and keep the file readable only by a trusted user.binary(required): must match exactly what clients send.queue(optional): commands with the same queue run serially; omit to allow parallel execution.fork(optional, default false): if true, the server starts the command and returns immediately withstatus: "started".allow_args(optional, default false): set totrueto allow arguments; omit orfalseto reject request arguments for this command.security.ip_allowlist(optional): comma-separated IP globs (default127.0.0.1,::1) that gate all requests. Use*to whitelist every address.security.rate_limit(optional): configuremax_requestsandwindow_secsto throttle traffic per API key/IP pair.security.throttle_delay_ms(optional): simple delay applied to every request to slow abusive clients.security.max_failed_attemptsandsecurity.failed_attempt_window_secs: block API keys when invalid auth is repeated.security.default_timeout_msandsecurity.max_output_bytes: control how long commands can run and how much stdout/stderr is returned (per-command overrides exist).security.allow_ssh_hostnames(optional): whentrue, anyHostName <ip>lines in~/.ssh/configare merged into the allowlist, so SSH-managed devices can connect.security.ip_allowlist(per-command, optional): overrides the global allowlist for that command.timeout_ms(per command): override the default timeout.max_output_bytes(per command): override the global output cap.
Running
cargo run -- --addr 0.0.0.0:8080
# optionally: cargo run -- --config /path/to/config.toml
Request format
POST http://<host>:<port>/execute with JSON body:
{
"command": "/usr/bin/echo",
"args": ["hello", "world"],
"queue": "echo-queue"
}
Headers:
-
X-API-Key(required): must match one of theapi_keyslisted in the config (case-sensitive). -
command(string): required, must appear in the config. -
args(string array): optional, defaults to empty; rejected whenallow_args = falsefor that command. -
queue(string): optional; overrides the config queue when provided.
Response
- Standard execution returns
status: "ok"or"failed"withexit_code,stdout, andstderrpopulated. - When
fork = truefor the command, the server returns immediately withstatus: "started",exit_code: 0, and emptystdout/stderrwhile the process continues in the background. - Errors return
4xxwith a plain text message.
Examples
Curl (queued and forked)
curl -X POST http://localhost:8080/execute \
-H "Content-Type: application/json" \
-d '{"command":"/usr/bin/echo","args":["hello"],"queue":"echo-queue"}'
Curl (no-args command)
curl -X POST http://localhost:8080/execute \
-H "Content-Type: application/json" \
-d '{"command":"/usr/bin/date"}'
# this will fail with 400 if args are supplied because allow_args = false
Rust (reqwest)
use reqwest::blocking::Client;
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
struct CommandResponse {
status: String,
exit_code: i32,
stdout: String,
stderr: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let resp: CommandResponse = client
.post("http://localhost:8080/execute")
.json(&json!({
"command": "/usr/bin/echo",
"args": ["from", "rust"],
"queue": "echo-queue"
}))
.send()?
.json()?;
println!("status: {}", resp.status);
println!("stdout: {}", resp.stdout);
println!("exit: {}", resp.exit_code);
Ok(())
}
Notes
- Commands are executed exactly as provided; ensure absolute paths in the config for safety.
- When no queue is set in the request or config, commands are dispatched in parallel.
- When a queue is set, commands sharing that queue run serially while other queues continue in parallel.
- Use
fork = truewhen you only need to start a command and return immediately. - Use
allow_args = falseto lock down commands to their exact binary invocation.