diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2025-12-14 22:43:24 -0800 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2025-12-14 22:43:24 -0800 |
| commit | cdb1a57614068fcfefa145bc6df45c9def7ccc6a (patch) | |
| tree | 92cadbecda8658c143b7625d5925e3411976a892 /README.md | |
| parent | 6d318665a08c0d4564d8de23cc39425d2c0bac49 (diff) | |
| download | posthook-cdb1a57614068fcfefa145bc6df45c9def7ccc6a.tar.gz posthook-cdb1a57614068fcfefa145bc6df45c9def7ccc6a.zip | |
Updates
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 114 |
1 files changed, 78 insertions, 36 deletions
@@ -28,6 +28,7 @@ A simple API service for receiving and storing webhook requests with dynamic rou **⚠️ Recommendation**: Put `/admin/*` behind an OAuth proxy (e.g., OAuth2 Proxy, Pomerium) for authentication. #### Register a Route + ```bash POST /admin/routes Content-Type: application/json @@ -40,6 +41,7 @@ Content-Type: application/json ``` With hCaptcha protection: + ```bash POST /admin/routes Content-Type: application/json @@ -53,6 +55,7 @@ Content-Type: application/json ``` With ntfy notifications: + ```bash POST /admin/routes Content-Type: application/json @@ -70,6 +73,7 @@ Content-Type: application/json ``` With CSRF token protection: + ```bash POST /admin/routes Content-Type: application/json @@ -83,6 +87,7 @@ Content-Type: application/json ``` #### List Routes + ```bash GET /admin/routes ``` @@ -90,17 +95,19 @@ GET /admin/routes ### Public Webhook Endpoints #### Get CSRF Token (for routes with requireToken: true) + ```bash GET /hook/{routeName}/token ``` Response: + ```json { - "ok": { - "token": "eyJ0aW1lc3RhbXAiOjE3MDI4MzQ1Njc4OTB5...", - "expiresAt": 1702835467890 - } + "ok": { + "token": "eyJ0aW1lc3RhbXAiOjE3MDI4MzQ1Njc4OTB5...", + "expiresAt": 1702835467890 + } } ``` @@ -109,6 +116,7 @@ Response: #### Send Webhook Send webhooks to registered routes: + ```bash POST /hook/{routeName} Content-Type: application/json @@ -119,6 +127,7 @@ Content-Type: application/json ``` For hCaptcha-protected routes, include the token: + ```bash POST /hook/{routeName} H-Captcha-Response: <token> @@ -130,6 +139,7 @@ Content-Type: application/json ``` For form submissions with redirect (returns 303 redirect instead of JSON): + ```bash POST /hook/{routeName} Content-Type: application/x-www-form-urlencoded @@ -138,16 +148,18 @@ name=John&email=john@example.com&_redirect=https://example.com/thank-you ``` Or in HTML: + ```html <form action="/hook/my-form" method="POST"> - <input type="text" name="name" required> - <input type="email" name="email" required> - <input type="hidden" name="_redirect" value="https://example.com/thank-you"> - <button type="submit">Submit</button> + <input type="text" name="name" required /> + <input type="email" name="email" required /> + <input type="hidden" name="_redirect" value="https://example.com/thank-you" /> + <button type="submit">Submit</button> </form> ``` For token-protected routes, include the token: + ```bash POST /hook/{routeName} X-CSRF-Token: <token> @@ -159,6 +171,7 @@ Content-Type: application/json ``` Or in form data: + ```bash POST /hook/{routeName} Content-Type: application/x-www-form-urlencoded @@ -167,6 +180,7 @@ name=John&email=john@example.com&_token=<token> ``` #### Health Check + ```bash GET /health ``` @@ -177,7 +191,7 @@ GET /health - `form` - Parse as URL-encoded form data (supports `_redirect` field) - `text` - Store as plain text - `raw` - Store raw body -- `multipart` - Multipart form data (not yet implemented, will support `_redirect` field) +- `multipart` - Multipart form data (supports file uploads and `_redirect`) ## CSRF Token Protection @@ -186,18 +200,18 @@ Posthook supports stateless CSRF token protection using HMAC-signed tokens. When ### How it works: 1. **Token Generation**: Tokens are HMAC-signed with a secret and contain: - - Route name (prevents token reuse across routes) - - Timestamp (30-second TTL) + - Route name (prevents token reuse across routes) + - Timestamp (30-second TTL) 2. **Token Delivery**: Tokens can be provided via: - - `_token` field in form data (extracted and not stored) - - `X-CSRF-Token` request header + - `_token` field in form data (extracted and not stored) + - `X-CSRF-Token` request header 3. **Validation**: Server validates: - - HMAC signature - - Route name matches - - Token not expired (30 seconds) - - Timestamp not from future + - HMAC signature + - Route name matches + - Token not expired (30 seconds) + - Timestamp not from future ### Token Secret Configuration: @@ -218,23 +232,27 @@ npm start # Warns and generates random secret With a 30-second TTL, you should generate the token server-side when rendering the form, not via client-side JavaScript. **Example with server-side rendering**: + ```html <!-- Your server fetches the token before rendering --> <form action="/hook/contact-form" method="POST"> - <input type="hidden" name="_token" value="<%= token %>"> - <input type="text" name="name" required> - <input type="email" name="email" required> - <input type="hidden" name="_redirect" value="/thanks"> - <button type="submit">Submit</button> + <input type="hidden" name="_token" value="<%= token %>" /> + <input type="text" name="name" required /> + <input type="email" name="email" required /> + <input type="hidden" name="_redirect" value="/thanks" /> + <button type="submit">Submit</button> </form> ``` **Your server** (Node.js example): + ```javascript app.get('/contact', async (req, res) => { - const response = await fetch('http://localhost:9000/hook/contact-form/token'); - const { ok: { token } } = await response.json(); - res.render('contact', { token }); + const response = await fetch('http://localhost:9000/hook/contact-form/token'); + const { + ok: { token }, + } = await response.json(); + res.render('contact', { token }); }); ``` @@ -243,11 +261,13 @@ app.get('/contact', async (req, res) => { When using `contentType: "form"` or `contentType: "multipart"`, you can include a `_redirect` field in your form data. If the request is successfully stored, the server will respond with a `303 See Other` redirect to the specified URL instead of a JSON response. The `_redirect` field is: + - Extracted from the form data and not stored in the request body - Only honored on successful requests (validation failures, errors, etc. will still return JSON error responses) - Useful for traditional HTML form submissions where you want to redirect users after submission Example form flow: + 1. User submits form with `_redirect` field (and `_token` if required) 2. Posthook validates token (if required) and stores the request 3. Posthook sends ntfy notification (if configured) @@ -264,6 +284,7 @@ When you register a route with ntfy enabled, posthook will send a notification t - Route name Example notification: + ``` Title: Webhook received: my-webhook Message: Method: POST @@ -273,26 +294,43 @@ UUID: 123e4567-e89b-12d3-a456-426614174000 ## Storage Format -Requests are stored per-topic in `data/{routeName}/{timestamp}_{uuid}.json`: +Requests are stored per-topic in a per-request directory: `data/{routeName}/{timestamp}_{uuid}/`: + +- `request.json` - full stored request (headers + parsed body + file metadata) +- `body.json` - parsed body only (convenience) +- `files/` - uploaded files (multipart only) + +Example `request.json`: ```json { - "timestamp": 1702834567890, - "uuid": "123e4567-e89b-12d3-a456-426614174000", - "routeName": "my-webhook", - "method": "POST", - "headers": { - "content-type": "application/json" - }, - "body": { - "your": "data" - } + "timestamp": 1702834567890, + "uuid": "123e4567-e89b-12d3-a456-426614174000", + "routeName": "my-webhook", + "method": "POST", + "headers": { + "content-type": "application/json" + }, + "body": { + "your": "data" + }, + "files": [ + { + "fieldName": "attachment", + "originalFilename": "invoice.pdf", + "filename": "0_invoice.pdf", + "contentType": "application/pdf", + "size": 12345, + "path": "files/0_invoice.pdf" + } + ] } ``` ## Usage ### Development + ```bash npm install npm run build @@ -301,6 +339,7 @@ npm start -- --port 9000 --host 0.0.0.0 --data-dir ./data ``` ### Docker + ```bash docker build -t posthook . docker run -p 9000:9000 \ @@ -330,16 +369,19 @@ docker run -p 4180:4180 \ ``` Then configure your reverse proxy: + - `/admin/*` → OAuth2 Proxy on port 4180 - `/` → Posthook on port 9000 ## Configuration ### Command Line Arguments: + - `--port` - Server port (default: 9000) - `--host` - Server host (default: 0.0.0.0) - `--data-dir` - Data storage directory (default: ./data) - `--token-secret` - HMAC secret for token signing (optional, generates random if not provided) ### Environment Variables: + - `POSTHOOK_TOKEN_SECRET` - HMAC secret for token signing (recommended for production) |
