Compare commits
2
Commits
8f0129d421
...
db63e51c98
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db63e51c98 | ||
|
|
8c05ec4378 |
+4
-23
@@ -76,8 +76,6 @@ RUN --network=none node_modules/.bin/prettier --check '**/*.md'
|
|||||||
# depends on it.
|
# depends on it.
|
||||||
#
|
#
|
||||||
# golangci/golangci-lint:v2.14.0 (Debian-based), 2026-09-24
|
# golangci/golangci-lint:v2.14.0 (Debian-based), 2026-09-24
|
||||||
# Using Debian-based image because mattn/go-sqlite3 (CGO) does not
|
|
||||||
# compile on Alpine musl (off64_t is a glibc type).
|
|
||||||
FROM golangci/golangci-lint:v2.14.0@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
|
FROM golangci/golangci-lint:v2.14.0@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@@ -167,9 +165,7 @@ RUN --mount=type=tmpfs,target=/tmp/tests,size=512m \
|
|||||||
|
|
||||||
# Build stage
|
# Build stage
|
||||||
# golang:1.26.1-bookworm (Debian-based), 2026-03-17
|
# golang:1.26.1-bookworm (Debian-based), 2026-03-17
|
||||||
# Using Debian-based image because gorm.io/driver/sqlite pulls in
|
# The image ships git and make, which the version step below uses.
|
||||||
# mattn/go-sqlite3 (CGO), which does not compile on Alpine musl. The image
|
|
||||||
# ships git and make, which the version step below uses.
|
|
||||||
FROM golang:1.26.1-bookworm@sha256:4465644228bc2857a954b092167e12aa59c006a3492282a6c820bf4755fd64a4 AS builder
|
FROM golang:1.26.1-bookworm@sha256:4465644228bc2857a954b092167e12aa59c006a3492282a6c820bf4755fd64a4 AS builder
|
||||||
|
|
||||||
# Nothing is wanted from the lint and test phases or from the stylesheet and
|
# Nothing is wanted from the lint and test phases or from the stylesheet and
|
||||||
@@ -211,26 +207,11 @@ RUN version="$(make version VERSION="$VERSION")"; \
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Builds through the Makefile's build target, which runs script/assets
|
# Builds through the Makefile's build target, which runs script/assets
|
||||||
# (Alpine.js, extracted from its tarball in 3p/) first.
|
# (Alpine.js, extracted from its tarball in 3p/) first. The target builds
|
||||||
|
# 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"
|
||||||
|
|
||||||
# Rebuild with static linking for the Alpine runtime. The cgo binary the
|
|
||||||
# step above builds is dynamically linked against glibc, which Alpine
|
|
||||||
# (musl) does not have.
|
|
||||||
#
|
|
||||||
# A statically linked glibc still loads shared libraries at run time for
|
|
||||||
# getaddrinfo and dlopen, and Alpine has none to load, so the build tags
|
|
||||||
# keep both out of the binary: netgo and osusergo select Go's own DNS
|
|
||||||
# resolver and user lookup instead of glibc's, and
|
|
||||||
# sqlite_omit_load_extension leaves out SQLite extension loading, which
|
|
||||||
# webhooker never uses.
|
|
||||||
#
|
|
||||||
# The static flags and tags go in through GO_LDFLAGS and GO_TAGS rather
|
|
||||||
# than a -ldflags of their own: the build target composes them with the
|
|
||||||
# -X that stamps the version, so this relink cannot silently drop the
|
|
||||||
# stamp.
|
|
||||||
RUN CGO_ENABLED=1 make build VERSION="$VERSION" GO_TAGS=netgo,osusergo,sqlite_omit_load_extension GO_LDFLAGS='-extldflags "-static"'
|
|
||||||
|
|
||||||
# Runtime stage
|
# Runtime stage
|
||||||
# alpine:3.21, 2026-03-17
|
# alpine:3.21, 2026-03-17
|
||||||
FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
||||||
|
|||||||
@@ -16,17 +16,6 @@ VERSION ?= $(shell script/version)
|
|||||||
# exists to correct.
|
# exists to correct.
|
||||||
override VERSION := $(or $(strip $(VERSION)),$(shell script/version))
|
override VERSION := $(or $(strip $(VERSION)),$(shell script/version))
|
||||||
|
|
||||||
# Extra linker flags and build tags for the build target. The static
|
|
||||||
# relink in the Dockerfile passes -extldflags and its build tags through
|
|
||||||
# these rather than a -ldflags of its own, so composing flags cannot drop
|
|
||||||
# the version stamp.
|
|
||||||
#
|
|
||||||
# The build target itself always passes -trimpath and -s -w, as the Go
|
|
||||||
# Dockerfile in REPO_POLICIES.md does: no build paths, symbol table or
|
|
||||||
# debug information in the binary.
|
|
||||||
GO_LDFLAGS ?=
|
|
||||||
GO_TAGS ?=
|
|
||||||
|
|
||||||
bootstrap:
|
bootstrap:
|
||||||
@script/bootstrap
|
@script/bootstrap
|
||||||
|
|
||||||
@@ -54,8 +43,12 @@ fmt-check:
|
|||||||
check:
|
check:
|
||||||
@script/check
|
@script/check
|
||||||
|
|
||||||
|
# Built with cgo off, -trimpath and -s -w, as the Go Dockerfile in
|
||||||
|
# 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
|
||||||
|
# linked.
|
||||||
build: assets
|
build: assets
|
||||||
go build -trimpath -tags '$(GO_TAGS)' -ldflags '$(strip -s -w -X main.version=$(VERSION) $(GO_LDFLAGS))' -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
|
||||||
|
|||||||
@@ -1393,10 +1393,10 @@ It uses:
|
|||||||
management
|
management
|
||||||
- **[go-chi](https://github.com/go-chi/chi)** for HTTP routing
|
- **[go-chi](https://github.com/go-chi/chi)** for HTTP routing
|
||||||
- **[GORM](https://gorm.io)** for database access with
|
- **[GORM](https://gorm.io)** for database access with
|
||||||
**[modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite)** as the runtime
|
**[modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite)**, which is pure
|
||||||
SQLite driver. Note: `gorm.io/driver/sqlite` transitively depends on
|
Go, as the SQLite driver. `gorm.io/driver/sqlite` also imports
|
||||||
`mattn/go-sqlite3`, which requires CGO at build time (see [Docker](#docker)
|
`mattn/go-sqlite3`, whose stub for builds without cgo is linked into the
|
||||||
section)
|
binary but never used
|
||||||
- **[slog](https://pkg.go.dev/log/slog)** (stdlib) for structured logging with
|
- **[slog](https://pkg.go.dev/log/slog)** (stdlib) for structured logging with
|
||||||
TTY detection (text for dev, JSON for prod)
|
TTY detection (text for dev, JSON for prod)
|
||||||
- **[gorilla/sessions](https://github.com/gorilla/sessions)** for encrypted
|
- **[gorilla/sessions](https://github.com/gorilla/sessions)** for encrypted
|
||||||
@@ -2135,9 +2135,7 @@ endpoint. That is the JSON key; the error text for a missing one reads
|
|||||||
`webhook_url is required`, which is the message, not the key.
|
`webhook_url is required`, which is the message, not the key.
|
||||||
|
|
||||||
The database uses the
|
The database uses the
|
||||||
[modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) driver at runtime,
|
[modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) driver.
|
||||||
though CGO is required at build time due to the transitive `mattn/go-sqlite3`
|
|
||||||
dependency from `gorm.io/driver/sqlite`.
|
|
||||||
|
|
||||||
#### Schema
|
#### Schema
|
||||||
|
|
||||||
@@ -2496,6 +2494,9 @@ are both taken unconditionally, the body precisely because these handlers call
|
|||||||
body with `(redacted)`, drops cookies and the remote-address environment, and
|
body with `(redacted)`, drops cookies and the remote-address environment, and
|
||||||
reduces the headers to a fixed allowlist — `Accept`, `Content-Length`,
|
reduces the headers to a fixed allowlist — `Accept`, `Content-Length`,
|
||||||
`Content-Type`, `Host`, `Origin`, `Referer`, `User-Agent` and `X-Request-Id`.
|
`Content-Type`, `Host`, `Origin`, `Referer`, `User-Agent` and `X-Request-Id`.
|
||||||
|
`Referer` is sent as its scheme and host only, because its path can be an
|
||||||
|
entrypoint URL; `internal/referer.SchemeAndHost` does that reduction for both
|
||||||
|
the access log's `referer` field and this hook.
|
||||||
|
|
||||||
The same hook rewrites the request URL. The SDK builds it as
|
The same hook rewrites the request URL. The SDK builds it as
|
||||||
`scheme://host/path` from the concrete path, which on the receiver route is
|
`scheme://host/path` from the concrete path, which on the receiver route is
|
||||||
@@ -2566,8 +2567,9 @@ receiver route. What survives still names the failing route — scheme, host,
|
|||||||
route pattern, method — and `X-Request-Id` ties the event to the local access
|
route pattern, method — and `X-Request-Id` ties the event to the local access
|
||||||
log line that holds the rest. Nothing dropped is needed for the likeliest use,
|
log line that holds the rest. Nothing dropped is needed for the likeliest use,
|
||||||
debugging a CSRF rejection. Its three inputs are the TLS decision, `Origin` and
|
debugging a CSRF rejection. Its three inputs are the TLS decision, `Origin` and
|
||||||
`Referer`; the latter two are kept, and the first is the scheme of the retained
|
`Referer`; the latter two are kept (`Referer` as its scheme and host, which is
|
||||||
URL, because the SDK derives that scheme from
|
all the check compares), and the first is the scheme of the retained URL,
|
||||||
|
because the SDK derives that scheme from
|
||||||
`r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"`. That predicate
|
`r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"`. That predicate
|
||||||
is the SDK's own and is stricter than `internal/reqtls.IsTLS`, which this
|
is the SDK's own and is stricter than `internal/reqtls.IsTLS`, which this
|
||||||
service now uses everywhere it decides transport: the SDK reports `http` for the
|
service now uses everywhere it decides transport: the SDK reports `http` for the
|
||||||
@@ -3099,6 +3101,8 @@ webhooker/
|
|||||||
│ │ ├── ratelimit.go # Per-IP rate limiting middleware (go-chi/httprate)
|
│ │ ├── ratelimit.go # Per-IP rate limiting middleware (go-chi/httprate)
|
||||||
│ │ ├── loginguard.go # Login failure counters and the Argon2id verification semaphore
|
│ │ ├── loginguard.go # Login failure counters and the Argon2id verification semaphore
|
||||||
│ │ └── middlewaretest/ # Test support: a Middleware for tests in other packages
|
│ │ └── middlewaretest/ # Test support: a Middleware for tests in other packages
|
||||||
|
│ ├── referer/
|
||||||
|
│ │ └── referer.go # SchemeAndHost: cuts a Referer down to its scheme and host, for the access log and the Sentry hook
|
||||||
│ ├── reqtls/
|
│ ├── reqtls/
|
||||||
│ │ └── reqtls.go # IsTLS: the one TLS predicate, r.TLS or X-Forwarded-Proto
|
│ │ └── reqtls.go # IsTLS: the one TLS predicate, r.TLS or X-Forwarded-Proto
|
||||||
│ ├── server/
|
│ ├── server/
|
||||||
@@ -3468,16 +3472,12 @@ fixed independently of the compiler's:
|
|||||||
a step's log, and then fails. `make test` builds this stage alone.
|
a step's log, and then fails. `make test` builds this stage alone.
|
||||||
6. **Builder stage** (`golang:1.26.1-bookworm`) — depends on the lint and test
|
6. **Builder stage** (`golang:1.26.1-bookworm`) — depends on the lint and test
|
||||||
phases and the `css-check` and `markdown-check` stages passing (it copies a
|
phases and the `css-check` and `markdown-check` stages passing (it copies a
|
||||||
file from each), runs `make build` (which extracts Alpine.js from `3p/`
|
file from each), and runs `make build` (which extracts Alpine.js from `3p/`
|
||||||
first), and then rebuilds the binary with `CGO_ENABLED=1` and static linking
|
first). `make build` compiles with cgo off, so the binary is statically
|
||||||
so it runs on musl, with the build tags `netgo`, `osusergo` and
|
linked and runs on the Alpine runtime image. The version is the `VERSION`
|
||||||
`sqlite_omit_load_extension` so it needs none of glibc's shared libraries at
|
build arg if one is given, otherwise derived from the `.git` in the context,
|
||||||
run time. Both builds go through `make build`, the relink adding its
|
and the stage fails if a context with `.git` would stamp an empty version,
|
||||||
`-extldflags` via `GO_LDFLAGS` and its tags via `GO_TAGS`, so neither can
|
`dev` or `unknown` (see [Version stamping](#version-stamping)).
|
||||||
drop the `-X` that stamps the version. The version is the `VERSION` build arg
|
|
||||||
if one is given, otherwise derived from the `.git` in the context, and the
|
|
||||||
stage fails if a context with `.git` would stamp an empty version, `dev` or
|
|
||||||
`unknown` (see [Version stamping](#version-stamping)).
|
|
||||||
7. **Runtime stage** (`alpine:3.21`) — copies the static binary and
|
7. **Runtime stage** (`alpine:3.21`) — copies the static binary and
|
||||||
`deploy/docker-entrypoint.sh`, creates the `/var/lib/webhooker` directory for
|
`deploy/docker-entrypoint.sh`, creates the `/var/lib/webhooker` directory for
|
||||||
all SQLite databases, exposes port 8080, and includes a health check against
|
all SQLite databases, exposes port 8080, and includes a health check against
|
||||||
@@ -3489,11 +3489,6 @@ The lint and test phases invoke `gofmt`, `golangci-lint` and `go test` directly
|
|||||||
rather than `make fmt-check`, `make lint` and `make test`: those targets build
|
rather than `make fmt-check`, `make lint` and `make test`: those targets build
|
||||||
docker stages, which would need a docker daemon inside this build.
|
docker stages, which would need a docker daemon inside this build.
|
||||||
|
|
||||||
The lint phase and the Go stages use Debian rather than Alpine because
|
|
||||||
`gorm.io/driver/sqlite` pulls in `mattn/go-sqlite3`, which needs CGO and does
|
|
||||||
not compile against musl. Only the final binary is statically linked, which is
|
|
||||||
what lets it run on the Alpine runtime image.
|
|
||||||
|
|
||||||
`script/cibuild` is the CI gate: it runs `script/bootstrap`, then
|
`script/cibuild` is the CI gate: it runs `script/bootstrap`, then
|
||||||
`script/check`, then builds the image, whose build runs the lint and test phases
|
`script/check`, then builds the image, whose build runs the lint and test phases
|
||||||
and the stylesheet and Markdown checks again. A build that succeeds is a repo
|
and the stylesheet and Markdown checks again. A build that succeeds is a repo
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/globals"
|
"sneak.berlin/go/webhooker/internal/globals"
|
||||||
"sneak.berlin/go/webhooker/internal/logfield"
|
"sneak.berlin/go/webhooker/internal/logfield"
|
||||||
|
"sneak.berlin/go/webhooker/internal/referer"
|
||||||
"sneak.berlin/go/webhooker/internal/session"
|
"sneak.berlin/go/webhooker/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,10 +49,6 @@ const (
|
|||||||
// pick the size of the line it writes.
|
// pick the size of the line it writes.
|
||||||
redactedQuery = "?(redacted)"
|
redactedQuery = "?(redacted)"
|
||||||
|
|
||||||
// redactedReferer stands in for a Referer the access log cannot
|
|
||||||
// reduce to its scheme and host; see accessLogReferer.
|
|
||||||
redactedReferer = "(redacted)"
|
|
||||||
|
|
||||||
// maxLogRequestIDBytes bounds the request id, which is also
|
// maxLogRequestIDBytes bounds the request id, which is also
|
||||||
// client-supplied: chi's RequestID middleware passes an inbound
|
// client-supplied: chi's RequestID middleware passes an inbound
|
||||||
// X-Request-Id header through verbatim. Its generated form is an
|
// X-Request-Id header through verbatim. Its generated form is an
|
||||||
@@ -342,27 +339,6 @@ func accessLogURL(r *http.Request, status int) string {
|
|||||||
return pattern
|
return pattern
|
||||||
}
|
}
|
||||||
|
|
||||||
// accessLogReferer returns the value for the access log's referer
|
|
||||||
// field: the Referer's scheme and host, never its path or query. The
|
|
||||||
// path can be an entrypoint URL, which carries the UUID: a sender that
|
|
||||||
// follows a redirect from http:// to https:// sends the URL it was
|
|
||||||
// redirected from, and a browser sends the 404 page's URL under /h/
|
|
||||||
// when it fetches that page's stylesheet. A Referer without both a
|
|
||||||
// scheme and a host is logged as redactedReferer.
|
|
||||||
func accessLogReferer(r *http.Request) string {
|
|
||||||
referer := r.Referer()
|
|
||||||
if referer == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed, err := url.Parse(referer)
|
|
||||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
||||||
return redactedReferer
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed.Scheme + "://" + parsed.Host
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logging returns middleware that logs each HTTP request with
|
// Logging returns middleware that logs each HTTP request with
|
||||||
// timing and metadata.
|
// timing and metadata.
|
||||||
func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
||||||
@@ -413,7 +389,8 @@ func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
|||||||
requestID, maxLogRequestIDBytes,
|
requestID, maxLogRequestIDBytes,
|
||||||
),
|
),
|
||||||
"referer", logfield.Truncate(
|
"referer", logfield.Truncate(
|
||||||
accessLogReferer(r), logfield.MaxBytes,
|
referer.SchemeAndHost(r.Referer()),
|
||||||
|
logfield.MaxBytes,
|
||||||
),
|
),
|
||||||
"proto", r.Proto,
|
"proto", r.Proto,
|
||||||
"remoteIP", RemoteIP(r),
|
"remoteIP", RemoteIP(r),
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Package referer reduces a Referer header to its scheme and host, so
|
||||||
|
// that a URL path, which on the receiver route carries the entrypoint
|
||||||
|
// UUID, never reaches a log line or an error report.
|
||||||
|
package referer
|
||||||
|
|
||||||
|
import "net/url"
|
||||||
|
|
||||||
|
// redacted stands in for a Referer that cannot be reduced to its
|
||||||
|
// scheme and host.
|
||||||
|
const redacted = "(redacted)"
|
||||||
|
|
||||||
|
// SchemeAndHost returns a Referer's scheme and host, never its path or
|
||||||
|
// query. The path can be an entrypoint URL, which carries the UUID: a
|
||||||
|
// sender that follows a redirect from http:// to https:// sends the
|
||||||
|
// URL it was redirected from, and a browser sends the 404 page's URL
|
||||||
|
// under /h/ when it fetches that page's stylesheet. A Referer without
|
||||||
|
// both a scheme and a host comes back as "(redacted)".
|
||||||
|
//
|
||||||
|
// It is the access log's referer field, and the Referer header the
|
||||||
|
// Sentry hook sends.
|
||||||
|
func SchemeAndHost(referer string) string {
|
||||||
|
if referer == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := url.Parse(referer)
|
||||||
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||||
|
return redacted
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.Scheme + "://" + parsed.Host
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package referer_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"sneak.berlin/go/webhooker/internal/referer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// entrypointUUID is the secret part of an entrypoint URL; no case may
|
||||||
|
// return it.
|
||||||
|
const entrypointUUID = "0b6f7d3e-6a39-4e3c-9f0a-2c4d8e1b5a77"
|
||||||
|
|
||||||
|
// redacted is what SchemeAndHost returns for a Referer it cannot
|
||||||
|
// reduce to a scheme and host.
|
||||||
|
const redacted = "(redacted)"
|
||||||
|
|
||||||
|
func TestSchemeAndHost(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
referer string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no Referer",
|
||||||
|
referer: "",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "entrypoint URL with a query",
|
||||||
|
referer: "http://webhooker.example/h/" + entrypointUUID +
|
||||||
|
"?src=ci",
|
||||||
|
want: "http://webhooker.example",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host with a port",
|
||||||
|
referer: "https://webhooker.example:8443/h/" + entrypointUUID,
|
||||||
|
want: "https://webhooker.example:8443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path without a scheme or host",
|
||||||
|
referer: "/h/" + entrypointUUID,
|
||||||
|
want: redacted,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host without a scheme",
|
||||||
|
referer: "webhooker.example/h/" + entrypointUUID,
|
||||||
|
want: redacted,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not a URL",
|
||||||
|
referer: "http://[::1/h/" + entrypointUUID,
|
||||||
|
want: redacted,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := referer.SchemeAndHost(tc.referer)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
|
assert.NotContains(t, got, entrypointUUID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
"sneak.berlin/go/webhooker/internal/referer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sentryRedacted stands in for a withheld field on every event shipped
|
// sentryRedacted stands in for a withheld field on every event shipped
|
||||||
@@ -253,6 +254,7 @@ func sentryTransactionName(name, pattern string) string {
|
|||||||
// gorilla/csrf accepts in place of the form field — and the shared
|
// gorilla/csrf accepts in place of the form field — and the shared
|
||||||
// secrets senders put on the receiver route (X-Gitlab-Token and the
|
// secrets senders put on the receiver route (X-Gitlab-Token and the
|
||||||
// per-provider signature headers) would otherwise ship verbatim.
|
// per-provider signature headers) would otherwise ship verbatim.
|
||||||
|
// The Referer it keeps is cut down to its scheme and host.
|
||||||
func keptSentryHeaders(headers map[string]string) map[string]string {
|
func keptSentryHeaders(headers map[string]string) map[string]string {
|
||||||
if len(headers) == 0 {
|
if len(headers) == 0 {
|
||||||
return headers
|
return headers
|
||||||
@@ -261,9 +263,15 @@ func keptSentryHeaders(headers map[string]string) map[string]string {
|
|||||||
kept := make(map[string]string, len(headers))
|
kept := make(map[string]string, len(headers))
|
||||||
|
|
||||||
for name, value := range headers {
|
for name, value := range headers {
|
||||||
if sentryKeepsHeader(name) {
|
if !sentryKeepsHeader(name) {
|
||||||
kept[name] = value
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if http.CanonicalHeaderKey(name) == "Referer" {
|
||||||
|
value = referer.SchemeAndHost(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
kept[name] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
return kept
|
return kept
|
||||||
@@ -271,11 +279,14 @@ func keptSentryHeaders(headers map[string]string) map[string]string {
|
|||||||
|
|
||||||
// sentryKeepsHeader reports whether a request header is routing or
|
// sentryKeepsHeader reports whether a request header is routing or
|
||||||
// content metadata rather than client-chosen payload. Referer is kept
|
// content metadata rather than client-chosen payload. Referer is kept
|
||||||
// on the reasoning that it is browser-set, that the only query
|
// only as its scheme and host, through referer.SchemeAndHost, which
|
||||||
// parameters in this service's own URLs are the login page's `next`,
|
// the access log uses too: its path can be an entrypoint URL, which
|
||||||
// `notice` and the event log's `show`, and that Referrer-Policy is set to
|
// carries the UUID, as when a sender follows a redirect from http://
|
||||||
// strict-origin-when-cross-origin. X-Request-Id ties the event to the
|
// to https:// or a browser fetches the 404 page's stylesheet from
|
||||||
// local access log line, which holds the rest of the detail.
|
// under /h/. The scheme and host are all gorilla/csrf compares a
|
||||||
|
// Referer by, so a CSRF rejection is still readable from the event.
|
||||||
|
// X-Request-Id ties the event to the local access log line, which
|
||||||
|
// holds the rest of the detail.
|
||||||
func sentryKeepsHeader(name string) bool {
|
func sentryKeepsHeader(name string) bool {
|
||||||
switch http.CanonicalHeaderKey(name) {
|
switch http.CanonicalHeaderKey(name) {
|
||||||
case "Accept",
|
case "Accept",
|
||||||
|
|||||||
@@ -325,6 +325,37 @@ func TestSentryScrub_ReplacesTheCapabilityPathWithTheRoutePattern(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSentryScrub_KeepsOnlyTheRefererSchemeAndHost sends a receiver
|
||||||
|
// request whose Referer is the entrypoint URL, as a sender's is after
|
||||||
|
// it follows the README's redirect from http:// to https://. The event
|
||||||
|
// keeps the Referer's scheme and host, and carries the UUID nowhere.
|
||||||
|
func TestSentryScrub_KeepsOnlyTheRefererSchemeAndHost(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
event := onlyEvent(t, sentryCase{
|
||||||
|
scrub: true,
|
||||||
|
panics: true,
|
||||||
|
request: func(client *sentry.Client) *http.Request {
|
||||||
|
req := sentryReceiverRequest(client)
|
||||||
|
req.Header.Set(
|
||||||
|
"Referer",
|
||||||
|
"http://webhooker.example/h/"+sentryReceiverUUID,
|
||||||
|
)
|
||||||
|
|
||||||
|
return req
|
||||||
|
},
|
||||||
|
}.capture(t))
|
||||||
|
|
||||||
|
assert.NotContains(
|
||||||
|
t, marshalEvent(t, event), sentryReceiverUUID,
|
||||||
|
)
|
||||||
|
assert.Equal(
|
||||||
|
t,
|
||||||
|
"http://webhooker.example",
|
||||||
|
event.Request.Headers["Referer"],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TestSentryScrub_KeepsTheRoutingContext checks the hook does not cost
|
// TestSentryScrub_KeepsTheRoutingContext checks the hook does not cost
|
||||||
// the debugging signal: the route, its scheme and host, the method and
|
// the debugging signal: the route, its scheme and host, the method and
|
||||||
// the metadata headers still identify what failed. On a static route
|
// the metadata headers still identify what failed. On a static route
|
||||||
|
|||||||
@@ -153,19 +153,15 @@ func TestVersion_IsStableAcrossInvocations(t *testing.T) {
|
|||||||
require.Equal(t, first, second)
|
require.Equal(t, first, second)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The build target is the only place composing linker flags. If a
|
// The build target is the only place that stamps the version. If a
|
||||||
// future edit drops either half, the binary silently reports "dev"
|
// future edit drops the -X flag, the binary silently reports "dev"
|
||||||
// again (the defect this package exists for) or fails to link on
|
// again, the defect this package exists for.
|
||||||
// Alpine. If it stops passing GO_TAGS, the image's static binary links
|
func TestMakefile_BuildStampsTheVersion(t *testing.T) {
|
||||||
// glibc's getaddrinfo and dlopen again.
|
|
||||||
func TestMakefile_BuildComposesVersionAndExtraFlags(t *testing.T) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
makefile := read(t, makefilePath)
|
makefile := read(t, makefilePath)
|
||||||
|
|
||||||
require.Contains(t, makefile, "-X main.version=$(VERSION)")
|
require.Contains(t, makefile, "-X main.version=$(VERSION)")
|
||||||
require.Contains(t, makefile, "$(GO_LDFLAGS)")
|
|
||||||
require.Contains(t, makefile, "-tags '$(GO_TAGS)'")
|
|
||||||
require.Contains(t, makefile, "VERSION ?= $(shell script/version)")
|
require.Contains(t, makefile, "VERSION ?= $(shell script/version)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,8 +220,8 @@ func requireMake(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every compile in the image goes through the build target, so the
|
// The image compiles through the build target, so it carries the
|
||||||
// static relink cannot replace the flags that carry the stamp.
|
// target's version stamp.
|
||||||
func TestDockerfile_BuildsThroughTheMakeTarget(t *testing.T) {
|
func TestDockerfile_BuildsThroughTheMakeTarget(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -234,10 +230,7 @@ func TestDockerfile_BuildsThroughTheMakeTarget(t *testing.T) {
|
|||||||
require.NotContains(t, dockerfile, "go build",
|
require.NotContains(t, dockerfile, "go build",
|
||||||
"a raw go build bypasses the Makefile's -X flag")
|
"a raw go build bypasses the Makefile's -X flag")
|
||||||
require.Contains(t, dockerfile, "ARG VERSION")
|
require.Contains(t, dockerfile, "ARG VERSION")
|
||||||
require.Contains(t, dockerfile,
|
require.Contains(t, dockerfile, `RUN make build VERSION="$VERSION"`)
|
||||||
`make build VERSION="$VERSION"`+
|
|
||||||
` GO_TAGS=netgo,osusergo,sqlite_omit_load_extension`+
|
|
||||||
` GO_LDFLAGS='-extldflags "-static"'`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func read(t *testing.T, path string) string {
|
func read(t *testing.T, path string) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user