Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a69ec23979 |
@@ -1,8 +1,8 @@
|
|||||||
# webhooker
|
# webhooker
|
||||||
|
|
||||||
webhooker is an MIT-licensed web service, written in [Go](https://golang.org) by
|
webhooker is an MIT-licensed [Go](https://golang.org) web service by
|
||||||
[@sneak](https://sneak.berlin), that receives the HTTP requests other services
|
[@sneak](https://sneak.berlin) that receives the HTTP requests other services
|
||||||
send when something happens and forwards them to your own systems.
|
send and forwards them to your own systems.
|
||||||
|
|
||||||
Many services send an HTTP request to a URL you give them when something
|
Many services send an HTTP request to a URL you give them when something
|
||||||
happens: a code push, a payment, a new ticket. webhooker receives those
|
happens: a code push, a payment, a new ticket. webhooker receives those
|
||||||
@@ -30,8 +30,8 @@ Five words name the moving parts.
|
|||||||
it. The UUID is a random 36-character identifier, and it is the only thing
|
it. The UUID is a random 36-character identifier, and it is the only thing
|
||||||
that authorises a sender.
|
that authorises a sender.
|
||||||
- A **target** is a place events are delivered to. There are four kinds: an HTTP
|
- A **target** is a place events are delivered to. There are four kinds: an HTTP
|
||||||
URL, a Slack or Mattermost message-posting URL, an archive file, and the
|
URL, the message-posting URL of a chat service (Slack or Mattermost), an
|
||||||
application log.
|
archive file, and the application log.
|
||||||
- An **event** is one stored request: its method, query string, headers and
|
- An **event** is one stored request: its method, query string, headers and
|
||||||
body.
|
body.
|
||||||
- A **delivery** is one event paired with one target. It records every attempt.
|
- A **delivery** is one event paired with one target. It records every attempt.
|
||||||
@@ -102,7 +102,7 @@ sooner after 24 hours without use, unless `SESSION_IDLE_TIMEOUT` says otherwise.
|
|||||||
|
|
||||||
### Webhooks
|
### Webhooks
|
||||||
|
|
||||||
**New Webhook** on the webhook list at `/hooks` asks for **Name**, an optional
|
On the webhook list at `/hooks`, **New Webhook** asks for **Name**, an optional
|
||||||
**Description** and **Retention (days)**. Events older than the retention period
|
**Description** and **Retention (days)**. Events older than the retention period
|
||||||
are deleted, along with their deliveries. The default is 30 days. `0` keeps
|
are deleted, along with their deliveries. The default is 30 days. `0` keeps
|
||||||
events forever. The form also has an **HTTP target URL** field and an
|
events forever. The form also has an **HTTP target URL** field and an
|
||||||
@@ -123,7 +123,7 @@ deactivated entrypoint answers `410 Gone`. A deleted or unknown one answers
|
|||||||
|
|
||||||
### Targets
|
### Targets
|
||||||
|
|
||||||
**Add** under **Targets** asks for the type first, then that type's settings.
|
Under **Targets**, **Add** asks for the type first, then that type's settings.
|
||||||
|
|
||||||
- An `http` target posts each event to a URL, with optional **Headers** and a
|
- An `http` target posts each event to a URL, with optional **Headers** and a
|
||||||
**Timeout**.
|
**Timeout**.
|
||||||
@@ -174,19 +174,19 @@ Do not set `BIND_ADDRESS` to an empty value in a container. An empty value means
|
|||||||
unset, and unset falls back to `127.0.0.1`, so the container answers nothing on
|
unset, and unset falls back to `127.0.0.1`, so the container answers nothing on
|
||||||
its published port.
|
its published port.
|
||||||
|
|
||||||
The app runs as user `webhooker`. The image's start-up script,
|
The app runs as user `webhooker`. The image's start-up script is
|
||||||
`deploy/docker-entrypoint.sh`, gives `DATA_DIR` and its contents to that user
|
`deploy/docker-entrypoint.sh`. It gives `DATA_DIR` and its contents to that user
|
||||||
before the app starts. So a host directory can be mounted as it is. webhooker
|
before the app starts. So a host directory can be mounted as it is. webhooker
|
||||||
creates every SQLite file with mode `0600`.
|
creates every SQLite file with mode `0600`.
|
||||||
|
|
||||||
### Running under upaas
|
### Running under upaas
|
||||||
|
|
||||||
[upaas](https://git.eeqj.de/sneak/upaas), a deployment tool, builds the image
|
[upaas](https://git.eeqj.de/sneak/upaas) is a deployment tool. It builds the
|
||||||
from this repository's `Dockerfile` and runs it. Add no port mapping, because
|
image from this repository's `Dockerfile` and runs it. Add no port mapping,
|
||||||
upaas publishes every mapped port on all interfaces of the host. Put the app on
|
because upaas publishes every mapped port on all interfaces of the host. Put the
|
||||||
your reverse proxy's Docker network instead, and mount one host directory at
|
app on your reverse proxy's Docker network instead, and mount one host directory
|
||||||
`/var/lib/webhooker`. The first-boot password goes to the container's log. Read
|
at `/var/lib/webhooker`. The first-boot password goes to the container's log.
|
||||||
it with `docker logs` and the name upaas gave the container.
|
Read it with `docker logs` and the name upaas gave the container.
|
||||||
|
|
||||||
### Behind a reverse proxy
|
### Behind a reverse proxy
|
||||||
|
|
||||||
@@ -201,19 +201,22 @@ Four things must be right.
|
|||||||
1. **Keep the app port private.** Only the proxy may reach it: `127.0.0.1` on a
|
1. **Keep the app port private.** Only the proxy may reach it: `127.0.0.1` on a
|
||||||
bare host, or a port published to `127.0.0.1` for the image.
|
bare host, or a port published to `127.0.0.1` for the image.
|
||||||
2. **Send `X-Forwarded-Proto: https`.** This header tells webhooker the request
|
2. **Send `X-Forwarded-Proto: https`.** This header tells webhooker the request
|
||||||
arrived over TLS. It decides the `Secure` flag on cookies, and whether a form
|
arrived over TLS. It decides the `Secure` flag on cookies, the flag that
|
||||||
submission must carry a matching `Origin` or `Referer`. webhooker reads it
|
makes a browser send them only over HTTPS. It also decides whether a form
|
||||||
from any connecting machine, so the proxy must overwrite whatever the client
|
submission must carry a matching `Origin` or `Referer` header. A browser
|
||||||
sent.
|
sends those two headers to say which site or page a request came from.
|
||||||
|
webhooker reads `X-Forwarded-Proto` from any connecting machine, so the proxy
|
||||||
|
must overwrite whatever the client sent.
|
||||||
3. **Cover the proxy in `TRUSTED_PROXIES`.** Otherwise every client shares one
|
3. **Cover the proxy in `TRUSTED_PROXIES`.** Otherwise every client shares one
|
||||||
rate-limit bucket, the count a limit keeps per client. A proxy on the host
|
rate-limit bucket, the count a limit keeps per client. A proxy on the host
|
||||||
reaches the container from the Docker bridge address, such as `172.17.0.1`.
|
reaches the container from the Docker bridge address, such as `172.17.0.1`.
|
||||||
The default list covers it. The list does not cover loopback, so on a bare
|
The default list covers it. The list does not cover loopback, so on a bare
|
||||||
host set `TRUSTED_PROXIES=127.0.0.1`.
|
host set `TRUSTED_PROXIES=127.0.0.1`.
|
||||||
4. **Pass on the client's `Host` header with its port.** webhooker checks a
|
4. **Pass on the client's `Host` header with its port.** `Host` is the header
|
||||||
form's `Origin` against the `Host` header it receives. A `Host` without the
|
that names the site the client asked for. webhooker checks a form's `Origin`
|
||||||
port makes every form submission fail with `403` on any port other than 443.
|
against the `Host` header it receives. A `Host` without the port makes every
|
||||||
In nginx that means `$http_host`, not `$host`.
|
form submission fail with `403` on any port other than `443`. For the nginx
|
||||||
|
reverse proxy that means `$http_host`, not `$host`.
|
||||||
|
|
||||||
Protect the proxy's access log: its request paths hold the entrypoint UUIDs.
|
Protect the proxy's access log: its request paths hold the entrypoint UUIDs.
|
||||||
|
|
||||||
@@ -246,7 +249,10 @@ client first.
|
|||||||
HSTS is a response header that tells browsers to use HTTPS only for this
|
HSTS is a response header that tells browsers to use HTTPS only for this
|
||||||
hostname and every name under it, for `max-age` seconds. Every response carries
|
hostname and every name under it, for `max-age` seconds. Every response carries
|
||||||
it unless `HSTS_MAX_AGE` is `0`. A browser keeps the rule until `max-age` runs
|
it unless `HSTS_MAX_AGE` is `0`. A browser keeps the rule until `max-age` runs
|
||||||
out, so serve webhooker on a hostname you will keep on HTTPS.
|
out, so serve webhooker on a hostname you will keep on HTTPS. The header always
|
||||||
|
carries `preload`. On a whole domain such as `example.com`, that lets the domain
|
||||||
|
be added to the HTTPS-only list that browsers ship with. That listing is hard to
|
||||||
|
undo.
|
||||||
|
|
||||||
## Recovering a lost admin password
|
## Recovering a lost admin password
|
||||||
|
|
||||||
@@ -282,11 +288,11 @@ All state lives in `DATA_DIR`. Back up the whole directory. It holds:
|
|||||||
- the files of each archive target;
|
- the files of each archive target;
|
||||||
- the lock file, `webhooker.lock`, which is always empty.
|
- the lock file, `webhooker.lock`, which is always empty.
|
||||||
|
|
||||||
Every database runs in WAL mode. WAL is SQLite's write-ahead log: finished
|
Every database runs in WAL mode. WAL is SQLite's write-ahead log. Finished
|
||||||
writes land in a `-wal` file beside the database before they reach the `.db`
|
writes land in a `-wal` file beside the database before they reach the `.db`
|
||||||
itself, and a `-shm` file beside it indexes that log. A copy of the `.db`
|
itself. A `-shm` file beside it indexes that log. A copy of the `.db` without
|
||||||
without its `-wal` is missing data. A clean shutdown removes the `-wal` and
|
its `-wal` is missing data. A clean shutdown removes the `-wal` and `-shm`
|
||||||
`-shm` files; a crash leaves them, and they must be carried with the `.db`. Keep
|
files; a crash leaves them, and they must be carried with the `.db`. Keep
|
||||||
`DATA_DIR` on a local disk: WAL mode is not reliable on network filesystems, and
|
`DATA_DIR` on a local disk: WAL mode is not reliable on network filesystems, and
|
||||||
webhooker does not check for one.
|
webhooker does not check for one.
|
||||||
|
|
||||||
@@ -302,7 +308,7 @@ done
|
|||||||
```
|
```
|
||||||
|
|
||||||
Each file is captured at its own instant. A webhook created during the backup
|
Each file is captured at its own instant. A webhook created during the backup
|
||||||
can be in the copy of `webhooker.db` with no event database in the backup; a
|
can be in the copy of `webhooker.db` with no event database in the backup. A
|
||||||
restore then creates that event database empty.
|
restore then creates that event database empty.
|
||||||
|
|
||||||
To restore, stop the service and put back every file together, with any `-wal`
|
To restore, stop the service and put back every file together, with any `-wal`
|
||||||
@@ -320,11 +326,11 @@ plain text. Protect it accordingly.
|
|||||||
|
|
||||||
### The entrypoint URL is the credential
|
### The entrypoint URL is the credential
|
||||||
|
|
||||||
Each entrypoint is a random version 4 UUID served at `/h/{uuid}`. Holding that
|
Each entrypoint is a random UUID served at `/h/{uuid}`. Holding that URL is what
|
||||||
URL is what authorises a sender. The receiver checks nothing else: no shared
|
authorises a sender. The receiver checks nothing else: no shared secret, no HMAC
|
||||||
secret, no HMAC signature (a hash of the body made with a shared key), no token
|
signature (a hash of the body made with a shared key), no token header. If a
|
||||||
header. Signature headers a sender adds anyway are stored and forwarded as
|
sender adds signature headers anyway, they are stored and forwarded as ordinary
|
||||||
ordinary headers.
|
headers.
|
||||||
|
|
||||||
Keep the URL out of tickets, chat and screenshots. webhooker's own log never
|
Keep the URL out of tickets, chat and screenshots. webhooker's own log never
|
||||||
writes it. The reverse proxy's access log does hold it, in the request path.
|
writes it. The reverse proxy's access log does hold it, in the request path.
|
||||||
@@ -333,21 +339,26 @@ writes it. The reverse proxy's access log does hold it, in the request path.
|
|||||||
|
|
||||||
- Passwords are hashed with Argon2id, a slow password hashing function.
|
- Passwords are hashed with Argon2id, a slow password hashing function.
|
||||||
- Session cookies are signed, `HttpOnly` (hidden from page scripts) and
|
- Session cookies are signed, `HttpOnly` (hidden from page scripts) and
|
||||||
`SameSite=Lax` (not sent with cross-site form posts). They carry `Secure` when
|
`SameSite=Lax` (not sent with cross-site form posts). They carry `Secure`
|
||||||
the request carries `X-Forwarded-Proto: https`.
|
(sent only over HTTPS) when the request carries `X-Forwarded-Proto: https`.
|
||||||
- Every form is protected against cross-site request forgery (CSRF). CSRF is
|
- Every form is protected against cross-site request forgery (CSRF). CSRF is
|
||||||
when another site makes your browser submit a form here. Each form carries a
|
when another site makes your browser submit a form here. Each form carries a
|
||||||
token tied to a cookie, and a submission's `Origin` must name this host.
|
token tied to a cookie, and a submission's `Origin` must name this host.
|
||||||
- Form bodies are capped at 1 MB.
|
- Form bodies are capped at 1 MB.
|
||||||
- Every response carries these headers:
|
- Every response carries these headers:
|
||||||
- `Strict-Transport-Security`, unless `HSTS_MAX_AGE` is `0`;
|
- `Strict-Transport-Security`, the HSTS header, unless `HSTS_MAX_AGE` is
|
||||||
- `X-Content-Type-Options: nosniff`;
|
`0`;
|
||||||
- `X-Frame-Options: DENY`;
|
- `X-Content-Type-Options: nosniff`, which stops the browser guessing a
|
||||||
|
file's type;
|
||||||
|
- `X-Frame-Options: DENY`, which stops other sites showing these pages in a
|
||||||
|
frame;
|
||||||
- a Content-Security-Policy (CSP), the header that tells the browser where
|
- a Content-Security-Policy (CSP), the header that tells the browser where
|
||||||
scripts and styles may come from, set to
|
scripts and styles may come from, set to
|
||||||
`default-src 'self'; script-src 'self'; style-src 'self'`;
|
`default-src 'self'; script-src 'self'; style-src 'self'`;
|
||||||
- `Referrer-Policy: strict-origin-when-cross-origin`;
|
- `Referrer-Policy: strict-origin-when-cross-origin`, which tells another
|
||||||
- `Permissions-Policy: camera=(), microphone=(), geolocation=()`.
|
site only which site a visitor came from, not which page;
|
||||||
|
- `Permissions-Policy: camera=(), microphone=(), geolocation=()`, which
|
||||||
|
denies the pages the camera, microphone and location.
|
||||||
- `/metrics` sits behind HTTP basic authentication, a username and password sent
|
- `/metrics` sits behind HTTP basic authentication, a username and password sent
|
||||||
with each request.
|
with each request.
|
||||||
|
|
||||||
@@ -357,9 +368,9 @@ The password is checked first, and only a failed sign-in is counted. A correct
|
|||||||
password is never answered `429`. Failures are counted per client and submitted
|
password is never answered `429`. Failures are counted per client and submitted
|
||||||
username. From the fifth failure within a minute, each failure is answered
|
username. From the fifth failure within a minute, each failure is answered
|
||||||
`429`. At most two password checks run at once; a request that gets no turn
|
`429`. At most two password checks run at once; a request that gets no turn
|
||||||
within 5 seconds is answered `503`. So those two checks, not the failure count,
|
within 5 seconds is answered `503`. So guessing speed is bounded by the limit of
|
||||||
bound how fast passwords can be guessed. Choose a long random admin password, or
|
two checks at a time, not by the failure count. Choose a long random admin
|
||||||
rate-limit `POST /pages/login` at the reverse proxy.
|
password, or rate-limit `POST /pages/login` at the reverse proxy.
|
||||||
|
|
||||||
### Delivery destinations
|
### Delivery destinations
|
||||||
|
|
||||||
@@ -367,8 +378,9 @@ A delivery target could be used to make webhooker send requests into the network
|
|||||||
it sits in and read the answers back. This is called server-side request forgery
|
it sits in and read the answers back. This is called server-side request forgery
|
||||||
(SSRF). webhooker refuses it with the SSRF guard.
|
(SSRF). webhooker refuses it with the SSRF guard.
|
||||||
|
|
||||||
Every target URL must use `http` or `https` and resolve to a public address. The
|
Every target URL must use `http` or `https`. By default its hostname must also
|
||||||
SSRF guard refuses these ranges:
|
point at a public address, one reachable from the internet. The SSRF guard
|
||||||
|
refuses these ranges:
|
||||||
|
|
||||||
- loopback, `127.0.0.0/8` and `::1`;
|
- loopback, `127.0.0.0/8` and `::1`;
|
||||||
- the RFC 1918 private ranges, `10.0.0.0/8`, `172.16.0.0/12` and
|
- the RFC 1918 private ranges, `10.0.0.0/8`, `172.16.0.0/12` and
|
||||||
@@ -376,14 +388,16 @@ SSRF guard refuses these ranges:
|
|||||||
- link-local space, which is valid only on one network segment;
|
- link-local space, which is valid only on one network segment;
|
||||||
- carrier-grade NAT space, which internet providers use inside their own
|
- carrier-grade NAT space, which internet providers use inside their own
|
||||||
networks;
|
networks;
|
||||||
- multicast, the IPv6 unique local range, and the other reserved ranges.
|
- multicast, the addresses that reach a group of hosts at once;
|
||||||
|
- the IPv6 unique local range, `fc00::/7`, the private space of IPv6;
|
||||||
|
- the other reserved ranges.
|
||||||
|
|
||||||
The SSRF guard runs when a target is saved and again when each delivery
|
The SSRF guard runs when a target is saved and again when each delivery
|
||||||
connects, so a hostname that changes its address later is still checked.
|
connects. So a hostname that changes its address later is still checked.
|
||||||
Redirects are followed up to ten hops, and each hop is checked. A hop to another
|
Redirects are followed up to ten hops, and each hop is checked. A hop to another
|
||||||
host or port, or from `https` to `http`, drops every configured or forwarded
|
host or port, or from `https` to `http`, drops every configured or forwarded
|
||||||
header except `Content-Type`. So a redirect cannot pass a credential to another
|
header except `Content-Type`, which names the body's format. So a redirect
|
||||||
host.
|
cannot pass a credential to another host.
|
||||||
|
|
||||||
Egress means the connections webhooker opens outward to deliver.
|
Egress means the connections webhooker opens outward to deliver.
|
||||||
[`ALLOWED_EGRESS_CIDRS`](#allowing-egress-to-your-own-network) lets deliveries
|
[`ALLOWED_EGRESS_CIDRS`](#allowing-egress-to-your-own-network) lets deliveries
|
||||||
@@ -401,8 +415,9 @@ The `admin` password is printed in the clear on the first boot and by
|
|||||||
query string, headers and body.
|
query string, headers and body.
|
||||||
|
|
||||||
When `SENTRY_DSN` is set, error reports go to Sentry, an error reporting
|
When `SENTRY_DSN` is set, error reports go to Sentry, an error reporting
|
||||||
service. A report carries no query string, body or cookies. In place of the path
|
service. A report carries no query string, body or cookies. It carries the route
|
||||||
it carries the route pattern, the route as registered, such as `/h/{uuid}`.
|
pattern in place of the path. A route pattern is the path with its variable part
|
||||||
|
left as a name, such as `/h/{uuid}`.
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
@@ -411,9 +426,10 @@ sends them on in the background, and SQLite holds everything in between.
|
|||||||
|
|
||||||
The receiver stores the event and one pending delivery per active target in one
|
The receiver stores the event and one pending delivery per active target in one
|
||||||
database write, then queues the deliveries. Background workers run them and
|
database write, then queues the deliveries. Background workers run them and
|
||||||
record each attempt. At start and then every minute, a recovery routine queues
|
record each attempt. At start, a recovery routine queues again every delivery
|
||||||
again any delivery left `pending` or `retrying`, so a stop or a crash loses no
|
left `pending` or `retrying`. It then runs every minute. Each run sends again a
|
||||||
delivery.
|
`retrying` delivery the engine is not working on, and a `pending` one that has
|
||||||
|
sat untouched for 15 minutes. So a stop or a crash loses no delivery.
|
||||||
|
|
||||||
The main database holds configuration. Each webhook has its own event database,
|
The main database holds configuration. Each webhook has its own event database,
|
||||||
so writes for one webhook never wait on another webhook's database. The program
|
so writes for one webhook never wait on another webhook's database. The program
|
||||||
@@ -423,10 +439,9 @@ the binary.
|
|||||||
|
|
||||||
## Configuration reference
|
## Configuration reference
|
||||||
|
|
||||||
All configuration comes from environment variables. For development, variables
|
All configuration comes from environment variables. Variables can also go in a
|
||||||
can also go in a `.env` file in the working directory. A variable set in the
|
`.env` file in the working directory. A variable set in the real environment
|
||||||
real environment wins over the file. A `.env` file that cannot be parsed aborts
|
wins over the file. A `.env` file that cannot be parsed aborts startup.
|
||||||
startup.
|
|
||||||
|
|
||||||
A variable that is unset or empty takes its default. A variable that is set but
|
A variable that is unset or empty takes its default. A variable that is set but
|
||||||
cannot be parsed aborts startup with a message naming it. A Go duration is a
|
cannot be parsed aborts startup with a message naming it. A Go duration is a
|
||||||
@@ -440,10 +455,9 @@ is allowed. In `prod`, no CORS headers are sent.
|
|||||||
|
|
||||||
### `PORT`
|
### `PORT`
|
||||||
|
|
||||||
The TCP port the listener binds. Default `8080`. Docker marks the image's
|
The port the listener binds. Default `8080`. Docker marks the image's container
|
||||||
container healthy by a check every 30 seconds. The check requests port `8080`
|
healthy by a check every 30 seconds. The check requests port `8080` whatever
|
||||||
whatever `PORT` says, so a container started with another `PORT` reports
|
`PORT` says, so a container started with another `PORT` reports unhealthy.
|
||||||
unhealthy.
|
|
||||||
|
|
||||||
### `BIND_ADDRESS`
|
### `BIND_ADDRESS`
|
||||||
|
|
||||||
@@ -456,11 +470,11 @@ are refused. `0.0.0.0` listens on IPv4 only; `::` listens on IPv6 and IPv4.
|
|||||||
The directory for every SQLite database and the lock file. Default
|
The directory for every SQLite database and the lock file. Default
|
||||||
`/var/lib/webhooker`. It is created if it is missing.
|
`/var/lib/webhooker`. It is created if it is missing.
|
||||||
|
|
||||||
Exactly one webhooker process may use a `DATA_DIR` at a time; two would both run
|
Exactly one webhooker process may use a `DATA_DIR` at a time. Two would both run
|
||||||
the recovery routine and send every pending delivery twice. At startup the
|
the recovery routine and could send one delivery twice. At startup the process
|
||||||
process locks `{DATA_DIR}/webhooker.lock` with `flock`, the kernel's file lock,
|
locks `{DATA_DIR}/webhooker.lock` with `flock`, the kernel's file lock. A second
|
||||||
and a second process pointed at the same directory exits with a message naming
|
process pointed at the same directory exits with a message that names the
|
||||||
it.
|
directory and the lock file.
|
||||||
|
|
||||||
### `DEBUG`
|
### `DEBUG`
|
||||||
|
|
||||||
@@ -515,11 +529,11 @@ private ranges. A set value replaces the default.
|
|||||||
|
|
||||||
The list decides whose `X-Forwarded-For` header is believed. `X-Forwarded-For`
|
The list decides whose `X-Forwarded-For` header is believed. `X-Forwarded-For`
|
||||||
is the header a proxy uses to pass on the address of the client it is serving.
|
is the header a proxy uses to pass on the address of the client it is serving.
|
||||||
It is read only when the connecting machine is inside the list. `X-Real-IP` and
|
It is read only when the connecting machine is inside the list. webhooker never
|
||||||
`True-Client-IP` are never read. If any client can reach webhooker, or the proxy
|
reads `X-Real-IP` or `True-Client-IP`, two other headers with the same job. If
|
||||||
in front of it, from a private address, set the list to the proxy's address
|
any client can reach webhooker, or the proxy in front of it, from a private
|
||||||
alone. Otherwise those clients can choose their own `X-Forwarded-For` and bypass
|
address, set the list to the proxy's address alone. Otherwise those clients can
|
||||||
every rate limit.
|
choose their own `X-Forwarded-For` and bypass every rate limit.
|
||||||
|
|
||||||
### Allowing egress to your own network
|
### Allowing egress to your own network
|
||||||
|
|
||||||
@@ -529,8 +543,8 @@ Default empty. Example: `10.0.0.0/8, 172.17.0.0/16`. When the list is set,
|
|||||||
startup logs it at `WARN`.
|
startup logs it at `WARN`.
|
||||||
|
|
||||||
Anyone with the admin account can make webhooker send requests to every network
|
Anyone with the admin account can make webhooker send requests to every network
|
||||||
you list, and read the responses in the event log. List the smallest blocks that
|
you list, and read the responses in **Full Event Log**. List the smallest blocks
|
||||||
cover your destinations, and never `0.0.0.0/0` or `::/0`.
|
that cover your destinations, and never `0.0.0.0/0` or `::/0`.
|
||||||
|
|
||||||
These addresses stay blocked whatever you list. They are link-local space, the
|
These addresses stay blocked whatever you list. They are link-local space, the
|
||||||
unspecified addresses and cloud metadata endpoints. The unspecified addresses
|
unspecified addresses and cloud metadata endpoints. The unspecified addresses
|
||||||
@@ -555,19 +569,19 @@ default but is reopened by a list that covers it.
|
|||||||
|
|
||||||
## Delivery reference
|
## Delivery reference
|
||||||
|
|
||||||
An `http` target posts the event body to its URL with the event's `Content-Type`
|
An `http` target posts the event body to its URL. The request carries the
|
||||||
and headers, then the target's configured **Headers**, which replace event
|
event's `Content-Type` and headers. The target's configured **Headers** are
|
||||||
headers of the same name. Every `http` and `slack` request carries
|
added next and replace event headers of the same name. Every `http` and `slack`
|
||||||
`User-Agent: webhooker/{version}`. The **Headers** field refuses `User-Agent`. A
|
request carries `User-Agent: webhooker/{version}`. `User-Agent` is the header
|
||||||
`2xx` response is a success. The first 4 KiB of the response body is stored with
|
that names the sending program. The **Headers** field refuses `User-Agent`. A
|
||||||
the attempt. **Timeout** is at most 300 seconds, and blank means the 30 second
|
response with a status from 200 to 299 is a success. The first 4 KiB of the
|
||||||
default. **Pass the query string on to this target** appends the event's query
|
response body is stored with the attempt. **Timeout** is at most 300 seconds,
|
||||||
string to the URL.
|
and blank means the 30 second default. **Pass the query string on to this
|
||||||
|
target** appends the event's query string to the URL.
|
||||||
|
|
||||||
A `slack` target posts a message that describes the event and shows its body in
|
A `slack` target posts a message that describes the event and shows its body in
|
||||||
a code block, cut at 3500 bytes. An archive target, whose stored type is
|
a code block, cut at 3500 bytes. An archive target appends the event as a row to
|
||||||
`database`, appends the event as a row to its own SQLite file. A `log` target
|
its own SQLite file. A `log` target writes the event as one `INFO` line.
|
||||||
writes the event as one `INFO` line.
|
|
||||||
|
|
||||||
**Delivery attempts** is from 1 to 20. A failed attempt marks the delivery
|
**Delivery attempts** is from 1 to 20. A failed attempt marks the delivery
|
||||||
`retrying` and schedules the next attempt; the wait doubles each time, starting
|
`retrying` and schedules the next attempt; the wait doubles each time, starting
|
||||||
@@ -585,11 +599,12 @@ paused.
|
|||||||
An archive target writes to
|
An archive target writes to
|
||||||
`archive-{webhook_name}-{target_name}-{target_uuid}.db` in `DATA_DIR`, the two
|
`archive-{webhook_name}-{target_name}-{target_uuid}.db` in `DATA_DIR`, the two
|
||||||
names made safe for a file name. A rotation of `monthly`, `daily` or `hourly`
|
names made safe for a file name. A rotation of `monthly`, `daily` or `hourly`
|
||||||
starts a new file for each period, in UTC, with the period before `.db`, such as
|
starts a new file for each period. Periods follow UTC, the universal time that
|
||||||
`-2026-10-01.db`. Rows older than the expiry are deleted on each write and by
|
ignores local time zones. The period goes before `.db`, such as
|
||||||
the archive sweep. Renaming the webhook or the target renames the files.
|
`-2026-10-01.db`. Rows older than the expiry are deleted each time a file is
|
||||||
**Download** on the webhook page returns the target's whole archive as one
|
opened and by the archive sweep. Renaming the webhook or the target renames the
|
||||||
gzipped JSON file.
|
files. **Download** on the webhook page returns the target's whole archive as
|
||||||
|
one JSON file compressed with gzip.
|
||||||
|
|
||||||
## HTTP routes reference
|
## HTTP routes reference
|
||||||
|
|
||||||
@@ -613,9 +628,9 @@ gzipped JSON file.
|
|||||||
## Metrics reference
|
## Metrics reference
|
||||||
|
|
||||||
`/metrics` serves these metrics for Prometheus, the monitoring system, plus the
|
`/metrics` serves these metrics for Prometheus, the monitoring system, plus the
|
||||||
Go runtime and process metrics the Prometheus library provides. Each delivery
|
Go runtime and process metrics the Prometheus library provides. A label is a
|
||||||
metric carries one label, `target_type`, which is `http`, `slack`, `database`,
|
name and value attached to a metric. Each delivery metric carries one label,
|
||||||
`log` or `unknown`.
|
`target_type`, which is `http`, `slack`, `database`, `log` or `unknown`.
|
||||||
|
|
||||||
- `webhooker_events_received_total` and `webhooker_events_resubmitted_total`,
|
- `webhooker_events_received_total` and `webhooker_events_resubmitted_total`,
|
||||||
two counters (totals that only grow) with no label;
|
two counters (totals that only grow) with no label;
|
||||||
@@ -629,17 +644,17 @@ metric carries one label, `target_type`, which is `http`, `slack`, `database`,
|
|||||||
(current values) sampled every 30 seconds;
|
(current values) sampled every 30 seconds;
|
||||||
- `webhooker_circuit_breakers_open`.
|
- `webhooker_circuit_breakers_open`.
|
||||||
|
|
||||||
Two inbound HTTP metrics, `http_request_duration_seconds` and
|
`http_request_duration_seconds` and `http_response_size_bytes` measure inbound
|
||||||
`http_response_size_bytes`, carry the route pattern as their `handler` label,
|
requests. Their `handler` label is the route pattern, never the real path.
|
||||||
never the real path. A third, `http_requests_inflight`, counts all routes
|
`http_requests_inflight` counts all routes together.
|
||||||
together.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
You need the Go version named in `go.mod`, and Docker. The tests, the linters,
|
You need the Go version named in `go.mod`, and Docker. The tests, the linters,
|
||||||
prettier and the stylesheet build run in Docker, at versions pinned (fixed to
|
`prettier` and the stylesheet build run in Docker. Their versions are pinned
|
||||||
one exact release) in the `Dockerfile`, `package.json` and `yarn.lock`. `gofmt`
|
(fixed to one exact release) in the `Dockerfile`, `package.json` and
|
||||||
and `goimports` run on the host.
|
`yarn.lock`. `gofmt` runs on the host. `goimports` runs on the host too, when it
|
||||||
|
is installed. `script/bootstrap` does not install it.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make bootstrap # Install git, make, Go and the Go modules
|
make bootstrap # Install git, make, Go and the Go modules
|
||||||
@@ -652,25 +667,26 @@ make build # Build bin/webhooker, version-stamped
|
|||||||
make docker # Build the Docker image
|
make docker # Build the Docker image
|
||||||
```
|
```
|
||||||
|
|
||||||
Each `make` target except `build`, `run`, `dev`, `deps` and `clean` runs a
|
Most `make` targets run a script in `script/`, listed under
|
||||||
script in `script/`, listed under [Entrypoints](#entrypoints). Every
|
[Entrypoints](#entrypoints). `build`, `run`, `dev`, `deps` and `clean` run their
|
||||||
`docker build` in `script/` passes `--no-cache`, so a check that passes has
|
own commands. Every `docker build` in `script/` passes `--no-cache`, so a check
|
||||||
really run.
|
that passes has really run.
|
||||||
|
|
||||||
The binary's version comes from `script/version`: `git describe` on the
|
The binary's version comes from `script/version`: `git describe` on the
|
||||||
checkout, or `VERSION` when that is set. It is shown at
|
checkout, or `VERSION` when that is set. It is shown at
|
||||||
`/.well-known/healthcheck` and in the page footer.
|
`/.well-known/healthcheck` and in the page footer.
|
||||||
|
|
||||||
The pages use one third-party script, Alpine.js, in its CSP build
|
The pages use one third-party script, Alpine.js. They load `@alpinejs/csp`, the
|
||||||
`@alpinejs/csp`, which runs under a Content-Security-Policy that forbids `eval`.
|
version of it that works when the Content-Security-Policy forbids running text
|
||||||
The npm registry is where JavaScript packages are published. The package file in
|
as code. The npm registry is where JavaScript packages are published. The
|
||||||
`3p/` is byte for byte the one it publishes.
|
package file in `3p/` is byte for byte the one the npm registry publishes.
|
||||||
|
|
||||||
A browser test in `internal/server` loads the pages in a headless browser, one
|
A browser test in `internal/server` loads the pages in a headless browser, one
|
||||||
without a window, under the real Content-Security-Policy. `make test` leaves it
|
without a window, under the real Content-Security-Policy. `make test` leaves it
|
||||||
out; run `make test-browser` after changing `templates/` or `static/js/`.
|
out; run `make test-browser` after changing `templates/` or `static/js/`.
|
||||||
`static/css/tailwind.css` is generated by Tailwind and committed; `make check`
|
`static/css/tailwind.css` is generated by Tailwind, a CSS generator, and
|
||||||
fails when it is out of date, and `make css` regenerates it.
|
committed; `make check` fails when it is out of date, and `make css` regenerates
|
||||||
|
it.
|
||||||
|
|
||||||
## Entrypoints
|
## Entrypoints
|
||||||
|
|
||||||
@@ -687,22 +703,22 @@ application are under [Entrypoint URLs](#entrypoint-urls).
|
|||||||
- `script/test` runs the tests in Docker.
|
- `script/test` runs the tests in Docker.
|
||||||
- `script/test-browser` runs the browser test in Docker.
|
- `script/test-browser` runs the browser test in Docker.
|
||||||
- `script/lint` runs `gofmt`, `golangci-lint` and ESLint in Docker.
|
- `script/lint` runs `gofmt`, `golangci-lint` and ESLint in Docker.
|
||||||
- `script/fmt` formats the Go code with `gofmt` and `goimports` on the host, and
|
- `script/fmt` formats the Go code with `gofmt` on the host and the Markdown
|
||||||
the Markdown with prettier in Docker.
|
with `prettier` in Docker. It also runs `goimports` when that is installed.
|
||||||
- `script/fmt-check` fails when `gofmt` or prettier would change a file. It does
|
- `script/fmt-check` fails when `gofmt` or `prettier` would change a file. It
|
||||||
not run `goimports`, and it writes nothing.
|
does not run `goimports`, and it writes nothing.
|
||||||
- `script/css` regenerates `static/css/tailwind.css` in Docker.
|
- `script/css` regenerates `static/css/tailwind.css` in Docker.
|
||||||
- `script/css-check` fails when `static/css/tailwind.css` is out of date.
|
- `script/css-check` fails when `static/css/tailwind.css` is out of date.
|
||||||
- `script/check` runs `script/test`, `script/lint`, `script/fmt-check` and
|
- `script/check` runs `script/test`, `script/lint`, `script/fmt-check` and
|
||||||
`script/css-check`.
|
`script/css-check`.
|
||||||
- `script/version` prints the version to stamp into the binary.
|
- `script/version` prints the version to stamp into the binary.
|
||||||
- `script/docker` builds the image tagged `webhooker`.
|
- `script/docker` builds the image tagged `webhooker`.
|
||||||
- `script/cibuild` is what CI, the check run on every push, runs:
|
- CI is the check that runs on every push. It runs `script/cibuild`:
|
||||||
`script/bootstrap`, `script/check`, then the image build.
|
`script/bootstrap`, `script/check`, then the image build.
|
||||||
- `script/precommit` checks that `go mod tidy` changes nothing, then runs
|
- `script/precommit` checks that `go mod tidy` changes nothing, then runs
|
||||||
`script/check`.
|
`script/check`.
|
||||||
- `script/install-precommit` installs the git pre-commit hook that runs
|
- `script/install-precommit` installs the git hook that runs `script/precommit`
|
||||||
`script/precommit`.
|
before each commit.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user