Compare commits
2
Commits
6e652a223a
...
v1.0.0-rc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eda8e80844 | ||
|
|
e584718eb5 |
+1
-3
@@ -72,10 +72,8 @@
|
|||||||
**/*.sublime-*
|
**/*.sublime-*
|
||||||
|
|
||||||
# This repository's own host-built artifacts: the binary `make build`
|
# This repository's own host-built artifacts: the binary `make build`
|
||||||
# writes, and the Alpine.js file `make assets` extracts from 3p/ (the
|
# writes.
|
||||||
# build extracts its own).
|
|
||||||
/bin
|
/bin
|
||||||
/static/js/alpine.min.js
|
|
||||||
|
|
||||||
# SQLite databases, which hold the session key and webhook payloads, at
|
# SQLite databases, which hold the session key and webhook payloads, at
|
||||||
# any depth.
|
# any depth.
|
||||||
|
|||||||
@@ -77,7 +77,3 @@ data/
|
|||||||
# Temporary files
|
# Temporary files
|
||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
|
|
||||||
# Alpine.js, extracted by `make assets` from its tarball in 3p/, which is
|
|
||||||
# what is committed.
|
|
||||||
/static/js/alpine.min.js
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
static/js/alpine.min.js
|
||||||
|
|||||||
Binary file not shown.
+2
-11
@@ -92,10 +92,6 @@ COPY . .
|
|||||||
# fmt-check` is the markdown-check stage above.
|
# fmt-check` is the markdown-check stage above.
|
||||||
RUN if [ -n "$(gofmt -s -l .)" ]; then echo "gofmt needed on:"; gofmt -s -l .; exit 1; fi
|
RUN if [ -n "$(gofmt -s -l .)" ]; then echo "gofmt needed on:"; gofmt -s -l .; exit 1; fi
|
||||||
|
|
||||||
# static/static.go embeds the Alpine.js file this extracts from 3p/; without
|
|
||||||
# it the static package does not compile and cannot be linted.
|
|
||||||
RUN script/assets
|
|
||||||
|
|
||||||
# The golangci-lint steps run with --network=none. `golangci-lint config
|
# The golangci-lint steps run with --network=none. `golangci-lint config
|
||||||
# verify` is documented as fetching its JSON schema over HTTPS; this pinned
|
# verify` is documented as fetching its JSON schema over HTTPS; this pinned
|
||||||
# image resolves the schema without any network, and --network=none enforces
|
# image resolves the schema without any network, and --network=none enforces
|
||||||
@@ -130,9 +126,6 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# static/static.go embeds the Alpine.js file this extracts from 3p/.
|
|
||||||
RUN script/assets
|
|
||||||
|
|
||||||
# -timeout applies to each package on its own, so 90s has only to clear the
|
# -timeout applies to each package on its own, so 90s has only to clear the
|
||||||
# slowest one. -p 4 -parallel 8 keep the run under 2 GB of memory: at most
|
# slowest one. -p 4 -parallel 8 keep the run under 2 GB of memory: at most
|
||||||
# four test binaries build or run at once, each with at most eight parallel
|
# four test binaries build or run at once, each with at most eight parallel
|
||||||
@@ -211,10 +204,8 @@ RUN version="$(make version VERSION="$VERSION")"; \
|
|||||||
esac; \
|
esac; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Builds through the Makefile's build target, which runs script/assets
|
# The Makefile's build target builds with cgo off, so the binary is
|
||||||
# (Alpine.js, extracted from its tarball in 3p/) first. The target builds
|
# statically linked and runs on the Alpine runtime image below.
|
||||||
# with cgo off, so the binary is statically linked and runs on the Alpine
|
|
||||||
# runtime image below as it is.
|
|
||||||
RUN make build VERSION="$VERSION"
|
RUN make build VERSION="$VERSION"
|
||||||
|
|
||||||
# Runtime stage
|
# Runtime stage
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ COPY . .
|
|||||||
# The test binary embeds the templates and static files, so the browser
|
# The test binary embeds the templates and static files, so the browser
|
||||||
# stage needs nothing else. It is built with cgo off and -p 4 keeps the
|
# stage needs nothing else. It is built with cgo off and -p 4 keeps the
|
||||||
# compile's memory down, both as in the test phase of Dockerfile.
|
# compile's memory down, both as in the test phase of Dockerfile.
|
||||||
RUN make assets && CGO_ENABLED=0 go test -c -p 4 -tags browser -o /browser.test ./internal/server
|
RUN CGO_ENABLED=0 go test -c -p 4 -tags browser -o /browser.test ./internal/server
|
||||||
|
|
||||||
# chromedp/headless-shell:151.0.7922.109 (Debian trixie), 2026-08-11. The
|
# chromedp/headless-shell:151.0.7922.109 (Debian trixie), 2026-08-11. The
|
||||||
# browser is on PATH as headless-shell, where the test's browser library
|
# browser is on PATH as headless-shell, where the test's browser library
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: bootstrap setup assets test test-browser lint fmt fmt-check check build run dev deps docker clean hooks css css-check version
|
.PHONY: bootstrap setup test test-browser lint fmt fmt-check check build run dev deps docker clean hooks css css-check version
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
.DEFAULT_GOAL := check
|
.DEFAULT_GOAL := check
|
||||||
@@ -22,9 +22,6 @@ bootstrap:
|
|||||||
setup:
|
setup:
|
||||||
@script/setup
|
@script/setup
|
||||||
|
|
||||||
assets:
|
|
||||||
@script/assets
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@script/test
|
@script/test
|
||||||
|
|
||||||
@@ -47,13 +44,13 @@ check:
|
|||||||
# REPO_POLICIES.md does: the binary carries no build paths, symbol table or
|
# REPO_POLICIES.md does: the binary carries no build paths, symbol table or
|
||||||
# debug information, and on Linux, the image's included, it is statically
|
# debug information, and on Linux, the image's included, it is statically
|
||||||
# linked.
|
# linked.
|
||||||
build: assets
|
build:
|
||||||
CGO_ENABLED=0 go build -trimpath -ldflags '-s -w -X main.version=$(VERSION)' -o bin/webhooker ./cmd/webhooker
|
CGO_ENABLED=0 go build -trimpath -ldflags '-s -w -X main.version=$(VERSION)' -o bin/webhooker ./cmd/webhooker
|
||||||
|
|
||||||
run: build
|
run: build
|
||||||
./bin/webhooker
|
./bin/webhooker
|
||||||
|
|
||||||
dev: assets
|
dev:
|
||||||
go run ./cmd/webhooker
|
go run ./cmd/webhooker
|
||||||
|
|
||||||
deps:
|
deps:
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ identifier of its own to the request, so make your targets tolerate a duplicate.
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
You need the Go version named in `go.mod`, and Docker for the checks.
|
You need the Go version named in `go.mod`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.eeqj.de/sneak/webhooker.git
|
git clone https://git.eeqj.de/sneak/webhooker.git
|
||||||
@@ -57,9 +57,8 @@ DATA_DIR=../webhooker-data make dev
|
|||||||
```
|
```
|
||||||
|
|
||||||
`DATA_DIR` is where webhooker keeps its databases. It defaults to
|
`DATA_DIR` is where webhooker keeps its databases. It defaults to
|
||||||
`/var/lib/webhooker`, so point it at a directory you can write to. On a host
|
`/var/lib/webhooker`, so point it at a directory you can write to. `make build`
|
||||||
without Docker, `make build` writes the program to `bin/webhooker`, and running
|
writes the program to `bin/webhooker`. Running that file starts webhooker.
|
||||||
that file starts webhooker.
|
|
||||||
|
|
||||||
The first start creates the `admin` account and prints its password once, in a
|
The first start creates the `admin` account and prints its password once, in a
|
||||||
ruled box on standard output:
|
ruled box on standard output:
|
||||||
@@ -107,9 +106,9 @@ sooner after 24 hours without use, unless `SESSION_IDLE_TIMEOUT` says otherwise.
|
|||||||
On the webhook list at `/hooks`, **New Webhook** 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. Filling in **HTTP target URL** creates an `http` target with the
|
||||||
**Archive** checkbox; each creates that target with the webhook. Every new
|
webhook. Ticking **Archive** creates an archive target. Every new webhook gets
|
||||||
webhook gets one entrypoint, described as `Default entrypoint`.
|
one entrypoint, described as `Default entrypoint`.
|
||||||
|
|
||||||
### Entrypoint URLs
|
### Entrypoint URLs
|
||||||
|
|
||||||
@@ -198,9 +197,9 @@ Read it with `docker logs` and the name upaas gave the container.
|
|||||||
webhooker speaks plain HTTP. It does not handle TLS, the encryption behind
|
webhooker speaks plain HTTP. It does not handle TLS, the encryption behind
|
||||||
HTTPS: it holds no certificate and accepts no HTTPS connections. The reverse
|
HTTPS: it holds no certificate and accepts no HTTPS connections. The reverse
|
||||||
proxy holds the certificate and reaches webhooker over a private address. A bare
|
proxy holds the certificate and reaches webhooker over a private address. A bare
|
||||||
host is a host without Docker. On a bare host that is loopback, the address only
|
host is a host without Docker. On a bare host the private address is loopback
|
||||||
this host can reach. For the image it is the Docker bridge, the private network
|
(`127.0.0.1`), which only this host can reach. For the image it is the Docker
|
||||||
Docker puts containers on.
|
bridge, the private network Docker puts containers on.
|
||||||
|
|
||||||
Four things must be right.
|
Four things must be right.
|
||||||
|
|
||||||
@@ -371,13 +370,13 @@ writes it. The reverse proxy's access log does hold it, in the request path.
|
|||||||
|
|
||||||
### Sign-in limits
|
### Sign-in limits
|
||||||
|
|
||||||
The password is checked first, and only a failed sign-in is counted. A correct
|
From the fifth failure within a minute, each failure is answered `429`. Only
|
||||||
password is never answered `429`. Failures are counted per client and submitted
|
failures are counted. A correct password is never answered `429`. Failures are
|
||||||
username. From the fifth failure within a minute, each failure is answered
|
counted per client and submitted username. At most two password checks run at
|
||||||
`429`. At most two password checks run at once; a request that gets no turn
|
once; a request that gets no turn within 5 seconds is answered `503`. So
|
||||||
within 5 seconds is answered `503`. So guessing speed is bounded by the limit of
|
guessing speed is bounded by the limit of two checks at a time, not by the
|
||||||
two checks at a time, not by the failure count. Choose a long random `admin`
|
failure count. Choose a long random `admin` password, or rate-limit
|
||||||
password, or rate-limit `POST /pages/login` at the reverse proxy.
|
`POST /pages/login` at the reverse proxy.
|
||||||
|
|
||||||
### Delivery destinations
|
### Delivery destinations
|
||||||
|
|
||||||
@@ -433,10 +432,11 @@ 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, a recovery routine queues again every delivery
|
record each attempt. At start, a recovery routine resumes every delivery left
|
||||||
left `pending` or `retrying`. It then runs every minute. Each run sends again a
|
`pending` or `retrying`. It then runs every minute. Each run resumes every
|
||||||
`retrying` delivery the engine is not working on, and a `pending` one that has
|
`retrying` delivery that the engine is not already handling. It also resumes a
|
||||||
sat untouched for 15 minutes. So a stop or a crash loses no delivery.
|
`pending` delivery 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
|
||||||
@@ -689,8 +689,9 @@ checkout, or `VERSION` when that is set. It is shown at
|
|||||||
|
|
||||||
The pages use one third-party script, Alpine.js. They load `@alpinejs/csp`, the
|
The pages use one third-party script, Alpine.js. They load `@alpinejs/csp`, the
|
||||||
version of it that works when the Content-Security-Policy forbids running text
|
version of it that works when the Content-Security-Policy forbids running text
|
||||||
as code. The npm registry is where JavaScript packages are published. The
|
as code. The npm registry is where JavaScript packages are published.
|
||||||
package file in `3p/` is byte for byte the one the npm registry publishes.
|
`static/js/alpine.min.js` is committed byte for byte as the npm registry
|
||||||
|
publishes it.
|
||||||
|
|
||||||
A headless browser is a browser without a window. A browser test in
|
A headless browser is a browser without a window. A browser test in
|
||||||
`internal/server` loads the pages in one under the real Content-Security-Policy.
|
`internal/server` loads the pages in one under the real Content-Security-Policy.
|
||||||
@@ -710,7 +711,6 @@ application are under [Entrypoint URLs](#entrypoint-urls).
|
|||||||
- `script/bootstrap` installs git, make, Go and the Go modules.
|
- `script/bootstrap` installs git, make, Go and the Go modules.
|
||||||
- `script/setup` runs `script/bootstrap`, then `script/install-precommit`.
|
- `script/setup` runs `script/bootstrap`, then `script/install-precommit`.
|
||||||
- `script/projectname` prints `webhooker`.
|
- `script/projectname` prints `webhooker`.
|
||||||
- `script/assets` extracts Alpine.js from `3p/` to `static/js/alpine.min.js`.
|
|
||||||
- `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.
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
// REPO_POLICIES.md states that a linter can check: const for everything,
|
// REPO_POLICIES.md states that a linter can check: const for everything,
|
||||||
// let only for a variable that is reassigned, never var.
|
// let only for a variable that is reassigned, never var.
|
||||||
export default [
|
export default [
|
||||||
// Alpine.js, extracted from 3p/ by make assets; not ours to lint.
|
// Alpine.js, committed as published; not ours to lint.
|
||||||
{ ignores: ["static/js/alpine.min.js"] },
|
{ ignores: ["static/js/alpine.min.js"] },
|
||||||
{
|
{
|
||||||
// The pages load static/js/app.js as a classic script, not a module.
|
// The pages load static/js/app.js as a classic script, not a module.
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ import (
|
|||||||
|
|
||||||
// TestBaseTemplateScriptsAreServed walks every /s/ script the base
|
// TestBaseTemplateScriptsAreServed walks every /s/ script the base
|
||||||
// template loads on each page and fetches it through the real router.
|
// template loads on each page and fetches it through the real router.
|
||||||
// Alpine.js is extracted from its tarball in 3p/ at build time, so the
|
|
||||||
// file is not in the tree: this is the check that the page still gets
|
|
||||||
// the JavaScript it asks for.
|
|
||||||
func TestBaseTemplateScriptsAreServed(t *testing.T) {
|
func TestBaseTemplateScriptsAreServed(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/assets: extract Alpine.js from its npm package tarball, committed
|
|
||||||
# in 3p/, to static/js/alpine.min.js, where go:embed reads it. The package
|
|
||||||
# is @alpinejs/csp, Alpine's build for pages whose Content-Security-Policy
|
|
||||||
# forbids eval. The extracted file is not committed. make build, make dev
|
|
||||||
# and the Dockerfile's lint, test and build stages run this first.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
tar -xzOf 3p/alpinejs-csp-3.14.9.tgz package/dist/cdn.min.js \
|
|
||||||
>static/js/alpine.min.js
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
Vendored
+11
File diff suppressed because one or more lines are too long
+5
-3
@@ -6,9 +6,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Static holds the CSS and JavaScript files the web UI's pages load. They
|
// Static holds the CSS and JavaScript files the web UI's pages load. They
|
||||||
// are named one by one so that a missing js/alpine.min.js, which make
|
// are named one by one, so a missing file fails the build instead of
|
||||||
// assets extracts and git does not track, fails the build instead of
|
// leaving the pages without it.
|
||||||
// leaving the pages without Alpine.js.
|
//
|
||||||
|
// js/alpine.min.js is Alpine.js, committed unchanged. It is
|
||||||
|
// package/dist/cdn.min.js from the npm package @alpinejs/csp 3.14.9.
|
||||||
//
|
//
|
||||||
//go:embed css/tailwind.css css/style.css js/app.js js/alpine.min.js
|
//go:embed css/tailwind.css css/style.css js/app.js js/alpine.min.js
|
||||||
var Static embed.FS
|
var Static embed.FS
|
||||||
|
|||||||
Reference in New Issue
Block a user