{"id":15896189,"url":"https://github.com/numberoverzero/rook","last_synced_at":"2025-10-08T11:20:03.241Z","repository":{"id":142804017,"uuid":"423033388","full_name":"numberoverzero/rook","owner":"numberoverzero","description":"minimal rust-based webhook server","archived":false,"fork":false,"pushed_at":"2024-06-22T01:45:01.000Z","size":103,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-02-08T09:12:11.848Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/numberoverzero.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2021-10-31T02:18:12.000Z","updated_at":"2024-06-22T01:44:58.000Z","dependencies_parsed_at":null,"dependency_job_id":"7d000871-2009-4384-bc40-f9e1b4d509da","html_url":"https://github.com/numberoverzero/rook","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numberoverzero%2Frook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numberoverzero%2Frook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numberoverzero%2Frook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numberoverzero%2Frook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/numberoverzero","download_url":"https://codeload.github.com/numberoverzero/rook/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246867711,"owners_count":20846796,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":[],"created_at":"2024-10-06T09:06:45.187Z","updated_at":"2025-10-08T11:19:58.205Z","avatar_url":"https://github.com/numberoverzero.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# A small, simple, secure webhook handler\n\n* ~200KB binary\n* ~500µs response times (8kb payload)\n* verifies `x-hub-signature-256` header from github\n* toml configuration to run multiple hooks per route and per repository\n* multi-threaded server ([tokio](https://docs.rs/tokio)) with daemonized script execution ([fork](https://docs.rs/fork))\n\nSupports the github [`push` event](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#push) or an arbitrary payload `\"rook\"` event.  Other github event types (like [issues](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#issues) or [deployments](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#deployment)) are not supported.\n\n# Quick start\n\n1. Create a file that contains only the shared secret\n2. Create a script to run when the hook is called\n3. Create a config file (see below) that maps a url to these two file paths\n4. Get a copy of the server (see [releases](https://github.com/numberoverzero/rook/releases) or clone and `cargo build --release`)\n5. Start listening for webhooks with `./rook your-config.toml`\n\n## Configuration\n\nThere are two types of hooks: `\"github\"` and `\"rook\"`.  The only event that the `\"github\"` hook type supports is [push](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#push).\n\nMultiple hooks can listen on the same path but they must be the same type.  When using multiple `\"github\"` hooks on the same path, the event's `repository` value is used to filter for matching hooks.  When using multiple `\"rook\"` hooks on the same path, any whose signature is verified will be invoked.\n\n### Sample config\n\n```toml\naddr = 0.0.0.0  # or 127.0.0.1 if you're proxying the server\nport = 9000\n\n[[hooks]]\ntype = \"github\"\nurl = \"/hooks/gh\"\nrepo = \"numberoverzero/webhook-test\"\nsecret_file = \"/home/crossj/my_secret\"\ncommand_path = \"/home/crossj/my_script.sh\"\n\n[[hooks]]\ntype = \"github\"\nurl = \"/hooks/gh\"\nrepo = \"numberoverzero/bloop\"\nsecret_file = \"/tmp/my_shared_secret\"\ncommand_path = \"/tmp/pull_latest.sh\"\n\n[[hooks]]\ntype = \"rook\"\nurl = \"/build-hooks/blog\"\nsecret_file = \"/home/crossj/blog/secret\"\ncommand_path = \"/home/crossj/blog/rebuild.sh\"\n```\n\n## Hook data\n\nWhen using a `\"rook\"` hook the post body is passed in a single environment variable `$ROOK_INPUT`.  A `\"github\"` hook has three variables: `$GITHUB_REPO`, `$GITHUB_COMMIT`, `$GITHUB_REF`.  Why not args?  See [security details](#security) below.\n\n### Sample `\"github\"` script\n\n```sh\n#!/usr/bin/env bash\necho \"  time: $(date +%s)\"    \u003e\u003e output.log\necho \"  repo: $GITHUB_REPO\"   \u003e\u003e output.log\necho \"commit: $GITHUB_COMMIT\" \u003e\u003e output.log\necho \"   ref: $GITHUB_REF\"    \u003e\u003e output.log\n```\n\n### Sample `\"rook\"` script\n\n```sh\n#!/usr/bin/env bash\necho \"time: $(date +%s)\" \u003e\u003e output.log\necho \"body: $ROOK_INPUT\" \u003e\u003e output.log\n```\n\n## Running the server\n\n```sh\n$ ./rook my-config.toml\nlistening on port 9000\n140.82.115.81:50925 - - [06/Nov/2021:02:25:57 +0000] \"POST /hooks/gh HTTP/1.1\" 200 OK - 291µs\n140.82.115.117:28685 - - [06/Nov/2021:03:45:42 +0000] \"POST /hooks/gh HTTP/1.1\" 400 Bad Request - 5µs\n140.82.115.117:24349 - - [06/Nov/2021:03:57:15 +0000] \"POST /hooks/gh HTTP/1.1\" 200 OK - 236µs\n```\n\n# Sending a `\"rook\"` hook\n\nRook uses the same signing mechanism as github's hooks, with a slightly different header name: `x-rook-signature-256`.\n\n1. Construct a request `body`\n2. Load a shared `secret`\n3. Calculate `hmacSha256(secret, body)`\n4. Put the hex-encoded digest value prefixed with `sha256=` in a request header.  In pseudocode:\n\n```\n# some command to run\nbody = b\"build --release --target x86_64-pc-windows-gnu\"\nsecret = hex_to_bytes(\"d33e7cdf2126defc0e88cd3aab9fffd91681b89291f1dfc74e4c3d3a19405fd6\")\ndigest = bytes_to_hex(hmacSha256(secret, body).digest())\n\nurl = \"http://localhost:9000\"\nverb = \"POST\"\nheaders = { \"x-rook-signature-256\": \"sha256=\" + digest }\nrequest = new_request(verb, url, headers, body)\n```\n\n# Implementation Details\n\nUnless you're auditing the code you can safely skip this section.\n\n## Readability\n\nThe server is ~0.7kLOC[0] after `cargo fmt` and can be read completely in an hour or two.  ~1/4 is generic logging and config and there is no shared mutable state to track.  You may want to start reading at `main.rs::main`.\n\n[0] `find src -type f -name \"*.rs\" -print0 | wc -l --files0-from=-`\n\n\n## Performance\n\nrook is designed to do one thing: map incoming POST requests with valid signatures to a local script and pass some environment variables or arguments.  If you're looking for more complex setups or verbose logging there are hundreds of other feature-rich implementations to explore.\n\nrook provides minimal output (for debugging builds, see [debugging](#debugging)) and doesn't return detailed errors to callers.  It doesn't capture process output from scripts or failures to run scripts.  For example, if you forget to set the executable bit (`chmod +x my_hook.sh`) then rook will return a `500 Internal Error` with no body.\n\n## Security\n\nrook spawns processes from wherever it is running.  Both `\"github\"` and `\"rook\"` hooks pass the hook data through environment variables which is [reasonably secure](https://security.stackexchange.com/a/14009) on modern linuxes.  Note that command args are usually insecure because the default `hidepid=0` option when mounting [`proc(5)`](https://man7.org/linux/man-pages/man5/proc.5.html) allows [other users to view them](https://unix.stackexchange.com/questions/163145/how-to-get-whole-command-line-from-a-process).  If you want to forward sensitve data through a `\"rook\"` hook, you need to protect `/proc/[pid]/cmdline`:\n\u003e Users may not access files and subdirectories inside any /proc/[pid] directories but their own (the /proc/[pid] directories themselves remain visible).  Sensitive files such as /proc/[pid]/cmdline and /proc/[pid]/status are now protected against other users.\n\n## Process spawning\n\n* **Pipes**: `stdin`, `stdout`, `stderr` are all set to [null](https://doc.rust-lang.org/std/process/struct.Stdio.html#method.null)\n* **Ordering**: rook simultaneously starts all matching hooks for the given path.\n* **Non-blocking**: rook returns an http response without waiting for the processes to exit.\n* **Non-graceful shutdown**: Because child processes are detached from the main rook process, killing the server will not terminate any running hook scripts.  This is done by calling [`setsid(2)`](https://man7.org/linux/man-pages/man2/setsid.2.html) in the child process after [`fork(2)`](https://man7.org/linux/man-pages/man2/fork.2.html).  This process is described in the [notes](https://man7.org/linux/man-pages/man2/setsid.2.html#NOTES) of `setsid(2)`, specifically:\n  \u003e In order to be sure that setsid() will succeed, call fork(2) and have the parent _exit(2), while the child (which by definition can't be a process group leader) calls setsid().\n* **Threading**: The main rook process is multi-threaded with [tokio](https://docs.rs/tokio), so care must be taken when forking, as noted in `fork(2)`:\n  \u003e The child process is created with a single thread—the one that called fork().  The entire virtual address space of the parent is replicated in the child [..]; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.\n\n  However, [pthread_atfork(3)](https://man7.org/linux/man-pages/man3/pthread_atfork.3.html) has this to say on the feasibility of correct implementation:\n    \u003e The intent of pthread_atfork() was to provide a mechanism whereby the application (or a library) could ensure that mutexes and other process and thread state would be restored to a consistent state. In practice, this task is generally too difficult to be practicable.\n\n  Rather than try to use `pthread_atfork(3)` correctly, rook avoids the issue by not sharing mutable state across threads.  One atomic ref-counted ([Arc](https://doc.rust-lang.org/std/sync/struct.Arc.html)) struct holds the read-only route config which will not deadlock if a child process panics.\n\n## Debugging\n\nAdditional debugging output is available in non-release builds.  Clone this repository and compile a debug build:\n\n```sh\n$ git clone git@github.com:numberoverzero/rook.git\n$ cargo build\n$ scp target/debug/rook your-server:~/rook-DEBUG\n```\n\nSample output:\n```sh\n$ ./rook-DEBUG your-config.toml\nDEBUG:loaded config:\nDEBUG:port 8080 with 2 routes\nDEBUG:  1 github /hooks/gh/push\nDEBUG:  1 rook   /hooks/rook/status\nINFO:listening on port 8080\nDEBUG:incoming request\nDEBUG:\u003c\u003c\u003cPOST /hooks/gh/push\nDEBUG:\u003c\u003c\u003chost: \"159.89.149.210:8080\"\nDEBUG:\u003c\u003c\u003cuser-agent: \"GitHub-Hookshot/f7bdd04\"\nDEBUG:\u003c\u003c\u003ccontent-length: \"7714\"\nDEBUG:\u003c\u003c\u003caccept: \"*/*\"\nDEBUG:\u003c\u003c\u003cx-github-delivery: \"d219d74c-40ee-11ec-88b9-916812175124\"\nDEBUG:\u003c\u003c\u003cx-github-event: \"push\"\nDEBUG:\u003c\u003c\u003cx-github-hook-id: \"327497270\"\nDEBUG:\u003c\u003c\u003cx-github-hook-installation-target-id: \"423089939\"\nDEBUG:\u003c\u003c\u003cx-github-hook-installation-target-type: \"repository\"\nDEBUG:\u003c\u003c\u003cx-hub-signature: \"sha1=a0fb93ad0aa28a9afc0a7f3f2f00726ae18927e7\"\nDEBUG:\u003c\u003c\u003cx-hub-signature-256: \"sha256=8fd95ba5c47675f73c046ee24ae06de0593468823d32f55985e55ab619be259c\"\nDEBUG:\u003c\u003c\u003ccontent-type: \"application/json\"\nDEBUG:\u003c\u003c\u003cconnection: \"close\"\nDEBUG:dispatch '/hooks/gh/push' as github\nDEBUG:github payload: (numberoverzero/webhook-test, 2a536c03b2ee2e28d946cc3ee5a507751a267c6f, refs/heads/main)\nDEBUG:path dispatch failed: HttpResponse\u003cbad route\u003e\nINFO:140.82.115.145:59913 - - [08/Nov/2021:23:51:41 +0000] \"POST /hooks/gh/push HTTP/1.1\" 400 Bad Request - 570µs\nDEBUG:incoming request\nDEBUG:\u003c\u003c\u003cPOST /hooks/rook/status\nDEBUG:\u003c\u003c\u003cx-rook-signature-256: \"sha256=39c08e2550981e8100a768f4626beee89f9ed1b2dc17797810630be97ee24b01\"\nDEBUG:\u003c\u003c\u003ccontent-length: \"42\"\nDEBUG:\u003c\u003c\u003caccept: \"*/*\"\nDEBUG:\u003c\u003c\u003chost: \"159.89.149.210:8080\"\nDEBUG:dispatch '/hooks/rook/status' as rook\nDEBUG:rook payload (42b): \"{\\\"some\\\": \\\"literal\\\", \\\"json\\\": [{}, 0, null]}\"\nDEBUG:hmac check success\nDEBUG:hook forked\nDEBUG:path dispatched successfully\nINFO:52.173.143.145:1984 - - [08/Nov/2021:23:51:53 +0000] \"POST /hooks/rook/status HTTP/1.1\" 200 OK - 603µs\n```\n\n## Optimized Release Builds\n\nYou can build a space-optimized binary with:\n\n```\nmake release-musl\n```\n\nThis uses Docker for build isolation and outputs the binary to your `target/` directory at:\n\n```\ntarget/optimized/rook.x86_64-unknown-linux-musl\n```\n\nRelevant optimizations are defined in the following places:\n\n```\nConfig.toml (build, profile.release sections)\ndocker-build/Dockerfile.release (RUN cargo build, RUN upx)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnumberoverzero%2Frook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnumberoverzero%2Frook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnumberoverzero%2Frook/lists"}