{"id":46579899,"url":"https://github.com/dkarczmarski/webcmd","last_synced_at":"2026-03-07T11:00:44.559Z","repository":{"id":339816871,"uuid":"1128823419","full_name":"dkarczmarski/webcmd","owner":"dkarczmarski","description":"Run predefined host commands via HTTP/HTTPS endpoints","archived":false,"fork":false,"pushed_at":"2026-03-06T12:36:20.000Z","size":264,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-06T16:46:16.942Z","etag":null,"topics":["automation","cicd","command-runner","devops-tools","golang","http","self-hosted"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dkarczmarski.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-06T07:47:31.000Z","updated_at":"2026-03-06T12:36:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dkarczmarski/webcmd","commit_stats":null,"previous_names":["dkarczmarski/webcmd"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/dkarczmarski/webcmd","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkarczmarski%2Fwebcmd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkarczmarski%2Fwebcmd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkarczmarski%2Fwebcmd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkarczmarski%2Fwebcmd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dkarczmarski","download_url":"https://codeload.github.com/dkarczmarski/webcmd/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkarczmarski%2Fwebcmd/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30212103,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T09:02:10.694Z","status":"ssl_error","status_checked_at":"2026-03-07T09:02:08.429Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","cicd","command-runner","devops-tools","golang","http","self-hosted"],"created_at":"2026-03-07T11:00:21.558Z","updated_at":"2026-03-07T11:00:44.548Z","avatar_url":"https://github.com/dkarczmarski.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# webcmd\n\n[![Build \u0026 Test (Go)](https://github.com/dkarczmarski/webcmd/actions/workflows/build.yml/badge.svg)](https://github.com/dkarczmarski/webcmd/actions/workflows/build.yml)\n\n\u003e [!WARNING]\n\u003e **Status: Pre-1.0 (v0.x.x)**\n\u003e The public API is not stable yet and may change between releases.\n\n**webcmd** is a lightweight tool that allows you to execute predefined commands on a host machine via HTTP/HTTPS endpoints.\n\nIt is designed for small projects, CI/CD tasks, and maintenance jobs where you need to trigger a process or command on a remote machine - without giving full SSH access.\n\nInstead of exposing broad system permissions, **webcmd** lets you define a safe, explicit list of commands that can be executed remotely. Commands can be parameterized and protected with API keys.\n\n## Why webcmd?\n\nIn many scenarios (CI/CD pipelines, automation, maintenance):\n\n- You need to run **one specific command** on a remote host.\n- Giving SSH access is **too powerful and risky**.\n- You want a **simple HTTP/HTTPS interface** instead.\n\n**webcmd** solves this by:\n- Exposing commands via HTTP/HTTPS endpoints.\n- Allowing optional API key authorization per endpoint.\n- Supporting command parameters from the request.\n\n## Build and run\n\n#### Fetch and build\n\n```shell\ngit clone https://github.com/dkarczmarski/webcmd.git\ncd webcmd\ngo build -o webcmd cmd/main.go\n```\n\n#### Run and test sample commands\n\nRun sample configuration:\n\n```shell\ncp config.sample.yaml config.yaml\n./webcmd -config config.yaml\n```\n\nFor more examples, check [config.sample.yaml](config.sample.yaml) and [config.sample-ssl.yaml](config.sample-ssl.yaml) (HTTPS).\n\nTest the `POST /cmd/echo` endpoint, which executes the `/bin/echo` command with a message passed as a query parameter. This endpoint requires authorization using an API key:\n\n```shell\ncurl -H \"X-Api-Key: MYSECRETKEY\" -X POST http://localhost:8080/cmd/echo?message=hello\n```\n\n```output\nhello\n```\n\nTest the `GET /stream/time` endpoint, which streams the output of a bash script that prints the current time every second for 10 seconds. This endpoint also requires an API key for authorization:\n\n```shell\ncurl -H \"X-Api-Key: MYSECRETKEY\" http://localhost:8080/stream/time\n```\n\n```output\n1 11:47:28\n2 11:47:29\n3 11:47:30\n4 11:47:31\n5 11:47:32\n6 11:47:33\n7 11:47:34\n8 11:47:36\n9 11:47:37\n10 11:47:38\n```\n\n## Quick Start\n\n#### Basic steps\n\n1. Define the command you want to run. You can create a command template that uses parameters taken from URL query parameters or from the request body or from the http header. In the command definition, each argument must be placed on a separate line.\n2. Assign it to an HTTP/HTTPS endpoint.\n3. (Optional) Protect the endpoint with an API key.\n\n#### Example 1 - Public endpoint (no authorization)\n\nWe want to create an endpoint that returns the current disk usage using the `df -h` command.\n\n- Endpoint: `GET /maintenance/diskspace`.\n- Authorization: **none**.\n- Command: `df -h`.\n\nDefine `config.yaml`:\n\n```yaml\nurlCommands:\n  - url: GET /maintenance/diskspace\n    commandTemplate: |\n      df\n      -h\n````\n\nCall the endpoint:\n\n```shell\ncurl http://localhost:8080/maintenance/diskspace\n```\n\n#### Example 2 - Protected endpoint with API key and parameters\n\nWe want to run:\n\n```shell\n/usr/local/bin/myapp restart --reason MY_REASON\n```\n\n* Endpoint: `POST /myapp/restart`.\n* Required API key: `MYSECRETKEY`.\n* Parameter: `reason` (taken from query parameters).\n* Command: `/usr/local/bin/myapp restart --reason {{.url.reason}}`.\n\nDefine `config.yaml`:\n\n```yaml\nauthorization:\n  - name: my-auth-1\n    key: MYSECRETKEY\nurlCommands:\n  - url: POST /myapp/restart\n    authorizationName: my-auth-1\n    commandTemplate: |\n      /usr/local/bin/myapp\n      restart\n      --reason\n      {{.url.reason}}\n```\n\nCall the endpoint:\n\n```shell\ncurl -H \"X-Api-Key: MYSECRETKEY\" \\\n     -X POST \\\n     \"http://localhost:8080/myapp/restart?reason=MY_REASON\"\n```\n\n#### Example 3 - Streaming output\n\nWe want to watch the output of the following command online:\n\n```shell\ndocker logs -f my-container\n```\n\n* Endpoint: `GET /docker/logs/my-container`.\n* Required API key: `MYSECRETKEY`.\n* Command: `docker logs -f my-container`.\n* Output type: `stream`.\n* no Timeout.\n\nDefine `config.yaml`:\n\n```yaml\nauthorization:\n  - name: my-auth-1\n    key: MYSECRETKEY\nurlCommands:\n  - url: GET /docker/logs/my-container\n    authorizationName: my-auth-1\n    commandTemplate: |\n      docker\n      logs\n      -f\n      my-container\n    executionMode: stream\n```\n\nCall the endpoint:\n\n```shell\ncurl -H \"X-Api-Key: MYSECRETKEY\" \\\n     \"http://localhost:8080/docker/logs/my-container\"\n```\n\nWhen you close the connection, the command is stopped.\n\n#### Example 4 - Asynchronous execution\n\nWe want to trigger a long-running background task (e.g., a backup script) without waiting for it to finish.\n\n* Endpoint: `POST /maintenance/backup`.\n* Command: `/usr/local/bin/backup.sh`.\n* Output type: `none` (asynchronous).\n* Timeout: `1h`.\n\nDefine `config.yaml`:\n\n```yaml\nurlCommands:\n  - url: POST /maintenance/backup\n    commandTemplate: |\n      /usr/local/bin/backup.sh\n    executionMode: async\n    timeout: 1h\n```\n\nCall the endpoint:\n\n```shell\ncurl -X POST http://localhost:8080/maintenance/backup\n```\n\nThe server will return an immediate response as soon as the command starts. The command will continue running in the background. Even in asynchronous mode, the optional `timeout` is respected - if the process exceeds the specified time, it will be terminated.\n\n## Command template\n\nThe `commandTemplate` uses Go's `text/template` syntax to inject data from the HTTP request into your command. The following data sources are available:\n\n* `url` - provides access to URL query parameters.\n  Example: `{{.url.param_name}}` .\n\n* `body` - provides access to the request body.\n  - `{{.body.text}}` - the entire request body as a plain string (enabled by default).\n  - `{{.body.json}}` - the request body parsed as JSON (requires `params.bodyAsJson: true` in configuration).\n  - `{{.body.json.field_name}}` - specific field from the JSON body.\n\n* `headers` - provides access to HTTP request headers.\n  Header names are normalized by replacing hyphens (`-`) with underscores (`_`).\n  Example: `{{.headers.X_Api_Key}}` or `{{.headers.User_Agent}}` .\n\n## HTTP Response and Error Handling\n\nThe server returns different HTTP status codes depending on the outcome of the request and the command execution:\n\n- **200 OK**\n  Returned when the command starts successfully, regardless of whether the command later exits with code 0 or non-zero, or fails while executing.\n  In this case, the handler sets the following response headers:\n  - `X-Success`: `\"true\"` if the process exit code is 0, otherwise `\"false\"`.\n  - `X-Exit-Code`: The process exit code (if available).\n  - `X-Error-Message`: Empty on success, or contains the execution error message if the command fails (only if `server.withErrorHeader` is enabled in the configuration).\n\n- **429 Too Many Requests**\n  Returned when command execution cannot start because the call gate rejects the request as busy (e.g., when `mode: single` is used).\n\n- **404 Not Found**\n  Returned when the URL command is missing or the endpoint is not configured.\n\n- **400 Bad Request**\n  Returned when `bodyAsJson` is enabled but the request body is not a valid JSON object.\n\n- **500 Internal Server Error**\n  Returned when the command cannot be prepared or started at all, for example:\n  - Streaming was requested but the `ResponseWriter` does not support flushing.\n  - Command template rendering/building failed.\n  - Gate or pre-action setup failed before the process was started.\n  - Handler configuration is invalid.\n\n**Important distinction:** A command that starts successfully but later fails (e.g., returns a non-zero exit code) is still treated as an HTTP-level success and returns **200 OK**. Detailed information about the process outcome is available in the `X-Success` and `X-Exit-Code` headers. The `X-Error-Message` header is also provided if `server.withErrorHeader` is set to `true` in the configuration.\n\n## Configuration (`config.yaml`)\n\n### `server`\n\n* `address` *(optional)* - address the server listens on, in `host:port` format. Default: `\"127.0.0.1:8080\"`.\n  Examples:\n\n    * `\":8080\"`\n    * `\"localhost:8080\"`\n\n* `shutdownGracePeriod` *(optional)* - the time to wait for active requests to finish before the server shuts down (e.g., `5s`, `30s`). Format: [Go Duration](https://pkg.go.dev/time#ParseDuration). Default: `5s`.\n\n* `withErrorHeader` *(optional)* - if set to `true`, the `X-Error-Message` header will be included in the HTTP response when a command execution fails. Default: `false`.\n\n* `https` *(optional)* - HTTPS configuration:\n    * `enabled` - enable or disable HTTPS. Default: `false`.\n    * `certFile` - path to the SSL certificate file.\n    * `keyFile` - path to the SSL key file.\n\nExample:\n\n```yaml\nserver:\n  address: \":8443\"\n  https:\n    enabled: true\n    certFile: \"./cert.pem\"\n    keyFile: \"./key.pem\"\n```\n\n### `authorization`\n\nList of API key authorizations.\n\nEach authorization entry contains:\n\n* `name` - identifier referenced by `urlCommands.authorizationName`.\n* `key` - API key value, must be provided in the `X-Api-Key` HTTP header.\n\nExample:\n\n```yaml\nauthorization:\n  - name: admin-auth\n    key: MYSECRETKEY\n```\n\n### `urlCommands`\n\nDefines HTTP/HTTPS endpoints and the commands they execute.\n\nEach entry contains:\n\n* `url`\n  HTTP method and path, e.g. `GET /health` or `POST /deploy`.\n\n* `authorizationName` *(optional)*\n  Name of authorization defined in `authorization`. Multiple names can be separated by commas (e.g., `auth1,auth2`).\n\n* `commandTemplate`\n  Command template:\n\n    * First line: executable.\n    * Each following line: one argument.\n    * Empty lines are ignored.\n    * Supports Go `text/template` syntax [https://golang.org/pkg/text/template/](https://golang.org/pkg/text/template/).\n\n  Request data (e.g. query parameters, HTTP headers, body and JSON body) can be used as placeholders.\n\n* `timeout` *(optional)*\n  Timeout for the command execution (e.g., `30s`, `1m`, `1h`). Format: [Go Duration](https://pkg.go.dev/time#ParseDuration).\n\n* `graceTerminationTimeout` *(optional)*\n  The time to wait for the process to exit gracefully after sending `SIGTERM` when the context is cancelled (e.g., client disconnects or timeout occurs) before sending `SIGKILL`. Format: [Go Duration](https://pkg.go.dev/time#ParseDuration). Default: no grace period (sends `SIGKILL` immediately).\n\n* `executionMode` *(optional)*\n  Determines how the command is executed and how its output is returned:\n  - `buffered`: (default) run command synchronously and return the full output once it finishes.\n  - `stream`: run command synchronously and stream output in real-time as it is produced.\n  - `async`: start command and return immediately without waiting for it to finish. Any output is discarded. Note that the optional `timeout` is still respected for background processes.\n\n* `callGate` *(optional)*\n  Controls the concurrency of command execution. It allows you to limit how many instances of a command (or a group of commands) can run simultaneously.\n  - `mode`: specifies the concurrency control strategy:\n    - `single`: only one execution at a time is allowed for the group. If another execution is already running, the request is rejected immediately with a `429 Too Many Requests` error.\n    - `sequence`: only one execution at a time is allowed for the group. If another execution is already running, the request waits (blocks) until the previous one finishes.\n  - `groupName`: *(optional)* an identifier used to group multiple endpoints under the same concurrency control.\n    - If not provided, the `url` of the endpoint (e.g., `GET /my/path`) is used as the default group name, meaning the limit applies only to that specific endpoint.\n    - If provided as an empty string (`groupName: \"\"`), the endpoint belongs to a shared default group.\n    - If provided as a non-empty string, all endpoints with the same `groupName` share the same concurrency limit.\n\n* `params` *(optional)*\n  Optional configuration for request body processing:\n\n    * `bodyAsJson` *(optional)*\n      If set to `true`, the HTTP request body will be parsed as JSON and made available in the command template under `{{.body.json}}`.\n      - Allows access to individual fields, e.g., `{{.body.json.field_name}}`.\n      - Using `{{.body.json}}` without a field will insert the full, valid JSON string.\n      - Default: `false`.\n\nThe HTTP request body is always available as plain text in the command template as `{{.body.text}}`.\n\nExample 1 - Accessing request body as text:\n\n```yaml\nurlCommands:\n  - url: POST /echo-text\n    commandTemplate: |\n      /bin/echo\n      -n\n      {{.body.text}}\n```\n\nCall the endpoint:\n\n```shell\ncurl -X POST http://localhost:8080/echo-text \\\n     -d \"Hello from request body\"\n```\n\nExample 2 - Using `bodyAsJson`:\n\n```yaml\nurlCommands:\n  - url: POST /deploy\n    params:\n      bodyAsJson: true\n    commandTemplate: |\n      /usr/local/bin/deploy.sh\n      --project\n      {{.body.json.project_name}}\n      --payload\n      {{.body.json}}\n```\n\nCall the endpoint:\n\n```shell\ncurl -X POST http://localhost:8080/deploy \\\n     -d '{\"project_name\": \"my-app\", \"version\": \"1.0.1\"}'\n```\n\nIn the above example:\n- `{{.body.json.project_name}}` will be replaced by `my-app`.\n- `{{.body.json}}` will be replaced by the full JSON string: `{\"project_name\":\"my-app\",\"version\":\"1.0.1\"}`.\n\nExample 3 - using URL query parameters:\n\nYou can use any URL query parameters in the command template by prefixing the key name with `url.`.\n\n```yaml\nurlCommands:\n  - url: POST /cmd/echo\n    authorizationName: auth-name1    \n    commandTemplate: |\n      /bin/echo\n      {{.url.message}}\n    timeout: 30s\n```\n\nCall the endpoint:\n\n```shell\ncurl -H \"X-Api-Key: MYSECRETKEY\" -X POST http://localhost:8080/cmd/echo?message=hello\n```\n\nExample 4 - Using `callGate` to limit concurrent execution:\n\nWe want to prevent multiple concurrent database backups from running at the same time.\n\n```yaml\nurlCommands:\n  - url: POST /db/backup\n    callGate:\n      mode: single\n      groupName: db-maintenance\n    commandTemplate: |\n      /usr/local/bin/backup-db.sh\n```\n\nIf you call `POST /db/backup` while another backup is already in progress, the server will immediately return `429 Too Many Requests`.\n\nExample 5 - Using `callGate` in `sequence` mode:\n\nIf you want tasks to wait for their turn instead of being rejected, use `mode: sequence`.\n\n```yaml\nurlCommands:\n  - url: POST /process/task\n    callGate:\n      mode: sequence\n      groupName: task-queue\n    commandTemplate: |\n      /usr/local/bin/slow-process.sh\n```\n\nIn this case, if multiple requests are made, they will be executed one by one in the order they arrived.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdkarczmarski%2Fwebcmd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdkarczmarski%2Fwebcmd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdkarczmarski%2Fwebcmd/lists"}