25 Commits

Author SHA1 Message Date
158462136b CI attempt Uno
Some checks failed
CI/CD Pipeline / build (amd64) (push) Failing after 3m35s
CI/CD Pipeline / build (arm64) (push) Failing after 29s
CI/CD Pipeline / publish (push) Has been skipped
2025-07-22 14:05:51 +01:00
458022b3c2 Was that reaaaaaaally necessary?
Defining full path don't work and cause a panic, I'll have to patch that
up.
2025-07-19 03:14:54 +01:00
WeebDataHoarder
e1a318bc38 build/wasm: update script to build TinyGo v0.38.0, update resulting js-pow-sha256 wasm artifact 2025-07-03 02:41:50 +02:00
WeebDataHoarder
8323536e84 build/docker: disable PIE buildmode under riscv64 due to https://github.com/golang/go/issues/64875 2025-06-28 10:44:08 +02:00
WeebDataHoarder
99ddb2b62b build/docker: address legacy "ENV key value" form and RedundantTargetPlatform 2025-06-28 10:35:10 +02:00
WeebDataHoarder
e4e5b0bc5d build/docker: pass JWT_PRIVATE_KEY_SEED as a secret env, add alternate GOAWAY_JWT_PRIVATE_KEY_SEED env 2025-06-28 10:29:42 +02:00
WeebDataHoarder
057bca753d build: set -buildmode pie, -bindnow linker flag. Enables Full RELRO, NX, PIE, no RPATH/RUNPATH, nothing to FORTIFY 2025-06-28 10:19:38 +02:00
Geoffrey “Frogeye” Preud'homme
d1d80c5078 challenges/context: add JA4 fingerprint in the headers 2025-06-27 21:28:43 +02:00
WeebDataHoarder
9a6f25df59 http/query: preserve raw query state when modifying url query 2025-06-09 13:49:37 +02:00
Alan Orth
c16f0863ae examples/generic.yml: use path.matches in condition
The string here uses a character set with path.contains, which will
not work in CEL. We need to use path.matches to use regex syntax.
2025-05-17 23:50:36 +03:00
Alan Orth
85a8f0d9ec examples: remove erroneous whitespace 2025-05-17 23:45:39 +03:00
WeebDataHoarder
a5e2e6625b cmd: move http/backend error logs to debug level 2025-05-17 18:55:48 +02:00
WeebDataHoarder
d24e4b521a examples/snippets: add CGNAT range to networks-private 2025-05-14 21:12:48 +02:00
WeebDataHoarder
3ac6b9d366 cmd/go-away: log private key fingerprint on load 2025-05-14 01:30:48 +02:00
WeebDataHoarder
484a5e3535 challenge/context: clear cookies by issuing a new cookie instead of clearing it 2025-05-14 01:30:31 +02:00
WeebDataHoarder
6032ac0b78 http: add cache-control headers to prevent caching by other proxies elsewhere 2025-05-13 23:48:21 +02:00
WeebDataHoarder
163fce6cfc challenge/resource-load: use proper redirect URL to current issued challenge, add static/dynamic cache bust 2025-05-13 23:43:31 +02:00
WeebDataHoarder
3abdc2ee5b examples: add private / localhost networks to snippets and forgejo/generic examples 2025-05-13 03:06:23 +02:00
WeebDataHoarder
3b045e9608 state/template: fix not allowing external templates to be defined 2025-05-08 12:14:01 +02:00
WeebDataHoarder
1d2f4e8a5b challenge/context: use additional HTTP headers in challenge key generation if the challenge allows for it 2025-05-04 20:22:34 +02:00
Alan Orth
c6a1d50f39 examples/config.yml: fix YAML syntax 2025-05-04 12:25:44 +03:00
WeebDataHoarder
b1f1e9a54f challenge/http: fix setting request headers properly, add method header 2025-05-04 04:03:07 +02:00
WeebDataHoarder
e0c0f8745d readme: add latest release badge 2025-05-04 04:02:38 +02:00
WeebDataHoarder
fb6c5c3eb4 examples/forgejo: remove standard-bots rule, it's redundant 2025-05-03 22:43:09 +02:00
WeebDataHoarder
aebbfa4eaa context: set client network address without original port on backend-ip-header option 2025-05-03 22:32:25 +02:00
30 changed files with 369 additions and 96 deletions

View File

@@ -22,8 +22,8 @@ local Build(mirror, go, alpine, os, arch) = {
"apk update", "apk update",
"apk add --no-cache git", "apk add --no-cache git",
"mkdir .bin", "mkdir .bin",
"go build -v -pgo=auto -v -trimpath -ldflags=-buildid= -o ./.bin/go-away ./cmd/go-away", "go build -v -pgo=auto -v -trimpath -ldflags='-buildid= -bindnow' -buildmode pie -o ./.bin/go-away ./cmd/go-away",
"go build -v -o ./.bin/test-wasm-runtime ./cmd/test-wasm-runtime", "go build -v -trimpath -ldflags='-buildid= -bindnow' -buildmode pie -o ./.bin/test-wasm-runtime ./cmd/test-wasm-runtime",
], ],
}, },
{ {

View File

@@ -14,8 +14,10 @@ steps:
- apk update - apk update
- apk add --no-cache git - apk add --no-cache git
- mkdir .bin - mkdir .bin
- go build -v -pgo=auto -v -trimpath -ldflags=-buildid= -o ./.bin/go-away ./cmd/go-away - go build -v -pgo=auto -v -trimpath -ldflags='-buildid= -bindnow' -buildmode pie
- go build -v -o ./.bin/test-wasm-runtime ./cmd/test-wasm-runtime -o ./.bin/go-away ./cmd/go-away
- go build -v -trimpath -ldflags='-buildid= -bindnow' -buildmode pie -o ./.bin/test-wasm-runtime
./cmd/test-wasm-runtime
image: golang:1.24-alpine3.21 image: golang:1.24-alpine3.21
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: build name: build
@@ -86,8 +88,10 @@ steps:
- apk update - apk update
- apk add --no-cache git - apk add --no-cache git
- mkdir .bin - mkdir .bin
- go build -v -pgo=auto -v -trimpath -ldflags=-buildid= -o ./.bin/go-away ./cmd/go-away - go build -v -pgo=auto -v -trimpath -ldflags='-buildid= -bindnow' -buildmode pie
- go build -v -o ./.bin/test-wasm-runtime ./cmd/test-wasm-runtime -o ./.bin/go-away ./cmd/go-away
- go build -v -trimpath -ldflags='-buildid= -bindnow' -buildmode pie -o ./.bin/test-wasm-runtime
./cmd/test-wasm-runtime
image: golang:1.24-alpine3.21 image: golang:1.24-alpine3.21
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: build name: build
@@ -158,8 +162,10 @@ steps:
- apk update - apk update
- apk add --no-cache git - apk add --no-cache git
- mkdir .bin - mkdir .bin
- go build -v -pgo=auto -v -trimpath -ldflags=-buildid= -o ./.bin/go-away ./cmd/go-away - go build -v -pgo=auto -v -trimpath -ldflags='-buildid= -bindnow' -buildmode pie
- go build -v -o ./.bin/test-wasm-runtime ./cmd/test-wasm-runtime -o ./.bin/go-away ./cmd/go-away
- go build -v -trimpath -ldflags='-buildid= -bindnow' -buildmode pie -o ./.bin/test-wasm-runtime
./cmd/test-wasm-runtime
image: golang:1.24-alpine3.21 image: golang:1.24-alpine3.21
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: build name: build
@@ -503,6 +509,6 @@ trigger:
type: docker type: docker
--- ---
kind: signature kind: signature
hmac: df53e4ea6f1c47df4d2a3f89b931b8513e83daa9c6c15baba2662d8112a721c8 hmac: 9a3872c0b58810924c4342c9dbd338e16da20631c9a0848e3abd2bf6773f9ba6
... ...

View File

@@ -0,0 +1,71 @@
---
name: CI/CD Pipeline
on:
push:
branches: [master, build-test]
pull_request:
release:
types: [published]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [amd64, arm64]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.24'
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y git
- name: Build go-away
run: |
mkdir .bin
go build -v -pgo=auto -v -trimpath -ldflags='-buildid= -bindnow' -buildmode pie -o ./.bin/go-away ./cmd/go-away
go build -v -trimpath -ldflags='-buildid= -bindnow' -buildmode pie -o ./.bin/test-wasm-runtime ./cmd/test-wasm-runtime
- name: Check policy for Forgejo
run: |
./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80 --policy examples/forgejo.yml --policy-snippets examples/snippets/
- name: Check policy for Generic
run: |
./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80 --policy examples/generic.yml --policy-snippets examples/snippets/
- name: Check policy for SPA
run: |
./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80 --policy examples/spa.yml --policy-snippets examples/snippets/
- name: Test WASM Runtime Success
run: |
./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm -make-challenge ./embed/challenge/js-pow-sha256/test/make-challenge.json -make-challenge-out ./embed/challenge/js-pow-sha256/test/make-challenge-out.json -verify-challenge ./embed/challenge/js-pow-sha256/test/verify-challenge.json -verify-challenge-out 0
- name: Test WASM Runtime Fail
run: |
./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm -make-challenge ./embed/challenge/js-pow-sha256/test/make-challenge.json -make-challenge-out ./embed/challenge/js-pow-sha256/test/make-challenge-out.json -verify-challenge ./embed/challenge/js-pow-sha256/test/verify-challenge-fail.json -verify-challenge-out 1
publish:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Git Forge registry
uses: docker/login-action@v3
with:
registry: git.projectsegfau.lt
username: ${{ secrets.GIT_USERNAME }}
password: ${{ secrets.GIT_TOKEN }}
- name: Build and push Docker images
env:
SOURCE_DATE_EPOCH: 0
TZ: UTC
run: |-
docker buildx build \
--platform linux/amd64,linux/arm64,linux/riscv64 \
--tag git.projectsegfau.lt/${{ secrets.GIT_USERNAME }}/go-away:latest \
--push \
.

View File

@@ -24,18 +24,26 @@ ENV CGO_ENABLED=0
ENV GOOS=${TARGETOS} ENV GOOS=${TARGETOS}
ENV GOARCH=${TARGETARCH} ENV GOARCH=${TARGETARCH}
ENV GOTOOLCHAIN=${GOTOOLCHAIN} ENV GOTOOLCHAIN=${GOTOOLCHAIN}
ENV BUILDMODE=pie
# riscv64 requires GCC for pie buildmode
# see https://github.com/golang/go/issues/64875
RUN if [[ "$GOARCH" == "riscv64" ]]; then export BUILDMODE=exe; fi && \
go build -v \
-pgo=auto \
-trimpath -ldflags='-buildid= -bindnow' -buildmode $BUILDMODE \
-o "${GOBIN}/go-away" ./cmd/go-away
RUN go build -pgo=auto -v -trimpath -ldflags=-buildid= -o "${GOBIN}/go-away" ./cmd/go-away
RUN test -e "${GOBIN}/go-away" RUN test -e "${GOBIN}/go-away"
FROM --platform=$TARGETPLATFORM ${from} FROM ${from}
COPY --from=build /go/bin/go-away /bin/go-away COPY --from=build /go/bin/go-away /bin/go-away
COPY examples/snippets/ /snippets/ COPY examples/snippets/ /snippets/
COPY docker-entrypoint.sh / COPY docker-entrypoint.sh /
ENV TZ UTC ENV TZ=UTC
ENV GOAWAY_METRICS_BIND="" ENV GOAWAY_METRICS_BIND=""
ENV GOAWAY_DEBUG_BIND="" ENV GOAWAY_DEBUG_BIND=""
@@ -52,7 +60,6 @@ ENV GOAWAY_CHALLENGE_TEMPLATE_LOGO=""
ENV GOAWAY_SLOG_LEVEL="WARN" ENV GOAWAY_SLOG_LEVEL="WARN"
ENV GOAWAY_CLIENT_IP_HEADER="" ENV GOAWAY_CLIENT_IP_HEADER=""
ENV GOAWAY_BACKEND_IP_HEADER="" ENV GOAWAY_BACKEND_IP_HEADER=""
ENV GOAWAY_JWT_PRIVATE_KEY_SEED=""
ENV GOAWAY_BACKEND="" ENV GOAWAY_BACKEND=""
ENV GOAWAY_ACME_AUTOCERT="" ENV GOAWAY_ACME_AUTOCERT=""
ENV GOAWAY_CACHE="/cache" ENV GOAWAY_CACHE="/cache"
@@ -63,6 +70,6 @@ EXPOSE 8080/udp
EXPOSE 9090/tcp EXPOSE 9090/tcp
EXPOSE 6060/tcp EXPOSE 6060/tcp
ENV JWT_PRIVATE_KEY_SEED="${GOAWAY_JWT_PRIVATE_KEY_SEED}" # Use GOAWAY_JWT_PRIVATE_KEY_SEED or JWT_PRIVATE_KEY_SEED secret mount to expose this value to docker
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@@ -3,6 +3,7 @@
Self-hosted abuse detection and rule enforcement against low-effort mass AI scraping and bots. Uses conventional non-nuclear options. Self-hosted abuse detection and rule enforcement against low-effort mass AI scraping and bots. Uses conventional non-nuclear options.
[![Latest Release](https://img.shields.io/gitea/v/release/git/go-away?gitea_url=https%3A%2F%2Fgit.gammaspectra.live)](https://git.gammaspectra.live/git/go-away/releases)
[![Build Status](https://ci.gammaspectra.live/api/badges/git/go-away/status.svg)](https://ci.gammaspectra.live/git/go-away) [![Build Status](https://ci.gammaspectra.live/api/badges/git/go-away/status.svg)](https://ci.gammaspectra.live/git/go-away)
[![Go Reference](https://pkg.go.dev/badge/git.gammaspectra.live/git/go-away.svg)](https://pkg.go.dev/git.gammaspectra.live/git/go-away) [![Go Reference](https://pkg.go.dev/badge/git.gammaspectra.live/git/go-away.svg)](https://pkg.go.dev/git.gammaspectra.live/git/go-away)

View File

@@ -9,18 +9,17 @@ mkdir -p .bin/ 2>/dev/null
# Setup tinygo first # Setup tinygo first
if [[ ! -d .bin/tinygo ]]; then if [[ ! -d .bin/tinygo ]]; then
git clone --depth=1 --branch v0.37.0 https://github.com/tinygo-org/tinygo.git .bin/tinygo git clone --depth=1 --branch v0.38.0 https://github.com/tinygo-org/tinygo.git .bin/tinygo
pushd .bin/tinygo pushd .bin/tinygo
git submodule update --init --recursive git submodule update --init --recursive
go mod download -x && go mod verify go mod download -x && go mod verify
make binaryen STATIC=1
make wasi-libc
make llvm-source make llvm-source
make llvm-build make llvm-build
make binaryen STATIC=1
make build/release make build/release
else else
pushd .bin/tinygo pushd .bin/tinygo

View File

@@ -154,7 +154,9 @@ func main() {
var seed []byte var seed []byte
var kValue string var kValue string
if kValue = os.Getenv("JWT_PRIVATE_KEY_SEED"); kValue != "" { if kValue = os.Getenv("GOAWAY_JWT_PRIVATE_KEY_SEED"); kValue != "" {
// prefer first
} else if kValue = os.Getenv("JWT_PRIVATE_KEY_SEED"); kValue != "" {
} else if *jwtPrivateKeySeed != "" { } else if *jwtPrivateKeySeed != "" {
kValue = *jwtPrivateKeySeed kValue = *jwtPrivateKeySeed
@@ -210,7 +212,7 @@ func main() {
fatal(fmt.Errorf("backend %s: failed to make reverse proxy: %w", k, err)) fatal(fmt.Errorf("backend %s: failed to make reverse proxy: %w", k, err))
} }
backend.ErrorLog = slog.NewLogLogger(slog.With("backend", k).Handler(), slog.LevelError) backend.ErrorLog = slog.NewLogLogger(slog.With("backend", k).Handler(), slog.LevelDebug)
createdBackends[k] = backend createdBackends[k] = backend
} }
@@ -291,7 +293,7 @@ func main() {
fatal(fmt.Errorf("failed to create server: %w", err)) fatal(fmt.Errorf("failed to create server: %w", err))
} }
server.ErrorLog = slog.NewLogLogger(slog.With("server", "http").Handler(), slog.LevelError) server.ErrorLog = slog.NewLogLogger(slog.With("server", "http").Handler(), slog.LevelDebug)
go func() { go func() {
handler, err := loadPolicyState() handler, err := loadPolicyState()
@@ -302,6 +304,7 @@ func main() {
swap(handler) swap(handler)
slog.Warn( slog.Warn(
"handler configuration loaded", "handler configuration loaded",
"key_fingerprint", hex.EncodeToString(handler.PrivateKeyFingerprint()),
) )
// allow reloading from now on // allow reloading from now on
@@ -336,7 +339,7 @@ func main() {
debugServer := http.Server{ debugServer := http.Server{
Addr: opt.BindDebug, Addr: opt.BindDebug,
Handler: mux, Handler: mux,
ErrorLog: slog.NewLogLogger(slog.With("server", "debug").Handler(), slog.LevelError), ErrorLog: slog.NewLogLogger(slog.With("server", "debug").Handler(), slog.LevelDebug),
} }
slog.Warn( slog.Warn(
@@ -356,7 +359,7 @@ func main() {
metricsServer := http.Server{ metricsServer := http.Server{
Addr: opt.BindMetrics, Addr: opt.BindMetrics,
Handler: mux, Handler: mux,
ErrorLog: slog.NewLogLogger(slog.With("server", "metrics").Handler(), slog.LevelError), ErrorLog: slog.NewLogLogger(slog.With("server", "metrics").Handler(), slog.LevelDebug),
} }
slog.Warn( slog.Warn(

View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en" class="fixed_navbar">
<head>
<title>{{ .Title }}</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{ range .MetaTags }}
<meta {{ range $key, $value := . }}{{ $key | attr }}="{{ $value }}" {{end}}/>
{{ end }}
{{ range .LinkTags }}
<link {{ range $key, $value := . }}{{ $key | attr }}="{{ $value }}" {{end}}/>
{{ end }}
{{ range .HeaderTags }}
{{ . }}
{{ end }}
<link rel="stylesheet" type="text/css" href="/style.css?v=0.36.0">
</head>
<body class="fixed_navbar">
<!-- NAVIGATION BAR -->
<nav class="fixed_navbar">
<div id="logo">
<a id="redlib" href="/"><span id="red">red</span><span id="lib">lib.</span></a>
</div>
</nav>
<!-- MAIN CONTENT -->
<main>
<div id="error">
<h1 id="status">Please wait while we verify you aren't a robot!</h1>
{{ if .Challenge }}
<h3 id="status">{{ .Strings.Get "status_loading_challenge" }} : {{.Challenge }}...</h3>
{{ else if .Error }}
<h3 id="status">{{ .Strings.Get "status_error" }} {{ .Error }}</h3>
{{ else }}
<h3 id="status">{{ .Strings.Get "status_loading" }}</h3>
{{ end }}
<details style="padding-top: 5px;">
<summary>{{ .Strings.Get "details_title" }}</summary>
{{.Strings.Get "details_text"}}
</details>
{{ if .Redirect }}
<h3><a href="{{ .Redirect }}">{{ .Strings.Get "button_refresh_page" }}</a></h3>
</div>
{{ end }}
{{if .EndTags }}
<noscript>
{{ .Strings.Get "noscript_warning" }}
</noscript>
{{end}}
</main>
<!-- FOOTER -->
<footer>
<div class="footer-buttons">
<p><small>{{ .Strings.Get "details_contact_admin_with_request_id" }}: <em>{{ .Id }}</em></small></p>
</div>
</footer>
</body>
</html>

View File

@@ -24,7 +24,7 @@ bind:
#bind-debug: ":6060" #bind-debug: ":6060"
# Bind the Prometheus metrics onto /metrics path on this port # Bind the Prometheus metrics onto /metrics path on this port
#bind-metrics ":9090" #bind-metrics: ":9090"
# These links will be shown on the presented challenge or error pages # These links will be shown on the presented challenge or error pages
links: links:

View File

@@ -104,6 +104,15 @@ rules:
- *is-bot-yandexbot - *is-bot-yandexbot
action: pass action: pass
# Matches private networks and localhost.
# Uncomment this if you want to let your own tools this way
# - name: allow-private-networks
# conditions:
# # Allows localhost and private networks CIDR
# - *is-network-localhost
# - *is-network-private
# action: pass
- name: undesired-networks - name: undesired-networks
conditions: conditions:
- 'remoteAddress.network("huawei-cloud") || remoteAddress.network("alibaba-cloud") || remoteAddress.network("zenlayer-inc")' - 'remoteAddress.network("huawei-cloud") || remoteAddress.network("alibaba-cloud") || remoteAddress.network("zenlayer-inc")'
@@ -248,13 +257,6 @@ rules:
settings: settings:
challenges: [ resource-load, js-refresh, http-cookie-check ] challenges: [ resource-load, js-refresh, http-cookie-check ]
- name: standard-bots
action: check
settings:
challenges: [meta-refresh, resource-load]
conditions:
- '($is-generic-robot-ua)'
# Allow all source downloads not caught in browser above # Allow all source downloads not caught in browser above
# todo: limit this as needed? # todo: limit this as needed?
- name: source-download - name: source-download
@@ -294,7 +296,7 @@ rules:
# Map OpenGraph or similar <meta> tags back to the reply, even if denied/challenged # Map OpenGraph or similar <meta> tags back to the reply, even if denied/challenged
proxy-meta-tags: "true" proxy-meta-tags: "true"
# proxy-safe-link-tags: "true" # proxy-safe-link-tags: "true"
# Set additional response headers # Set additional response headers
#response-headers: #response-headers:
# X-Clacks-Overhead: # X-Clacks-Overhead:

View File

@@ -10,7 +10,7 @@ networks:
challenges: challenges:
# Challenges will get included from snippets # Challenges will get included from snippets
conditions: conditions:
# Conditions will get replaced on rules AST when found as ($condition-name) # Conditions will get replaced on rules AST when found as ($condition-name)
@@ -27,7 +27,7 @@ conditions:
# Old IE browsers # Old IE browsers
- 'userAgent.matches("MSIE ([2-9]|10|11)\\.")' - 'userAgent.matches("MSIE ([2-9]|10|11)\\.")'
# Old Linux browsers # Old Linux browsers
- 'userAgent.contains("Linux i[63]86") || userAgent.contains("FreeBSD i[63]86")' - 'userAgent.matches("Linux i[63]86") || userAgent.matches("FreeBSD i[63]86")'
# Old Windows browsers # Old Windows browsers
- 'userAgent.matches("Windows (3|95|98|CE)") || userAgent.matches("Windows NT [1-5]\\.")' - 'userAgent.matches("Windows (3|95|98|CE)") || userAgent.matches("Windows NT [1-5]\\.")'
# Old mobile browsers # Old mobile browsers
@@ -60,6 +60,15 @@ rules:
- *is-bot-yandexbot - *is-bot-yandexbot
action: pass action: pass
# Matches private networks and localhost.
# Uncomment this if you want to let your own tools this way
# - name: allow-private-networks
# conditions:
# # Allows localhost and private networks CIDR
# - *is-network-localhost
# - *is-network-private
# action: pass
- name: undesired-crawlers - name: undesired-crawlers
conditions: conditions:
- '($is-headless-chromium)' - '($is-headless-chromium)'

View File

@@ -0,0 +1,22 @@
networks:
localhost:
# localhost and loopback addresses
- prefixes:
- "127.0.0.0/8"
- "::1/128"
private:
# Private network CIDR blocks
- prefixes:
# private networks
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
- "fc00::/7"
# CGNAT
- "100.64.0.0/10"
conditions:
is-network-localhost:
- &is-network-localhost 'remoteAddress.network("localhost")'
is-network-private:
- &is-network-private 'remoteAddress.network("private")'

View File

@@ -28,7 +28,9 @@ func (a Block) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Reques
data := challenge.RequestDataFromContext(r.Context()) data := challenge.RequestDataFromContext(r.Context())
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Connection", "close") w.Header().Set("Connection", "close")
data.ResponseHeaders(w) data.ResponseHeaders(w)
w.WriteHeader(a.Code) w.WriteHeader(a.Code)
_, _ = w.Write([]byte(fmt.Errorf("access blocked: blocked by administrative rule %s/%s", data.Id.String(), a.RuleHash).Error())) _, _ = w.Write([]byte(fmt.Errorf("access blocked: blocked by administrative rule %s/%s", data.Id.String(), a.RuleHash).Error()))

View File

@@ -42,7 +42,11 @@ type CodeSettings struct {
type Code int type Code int
func (a Code) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Request, done func() (backend http.Handler)) (next bool, err error) { func (a Code) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Request, done func() (backend http.Handler)) (next bool, err error) {
challenge.RequestDataFromContext(r.Context()).ResponseHeaders(w) data := challenge.RequestDataFromContext(r.Context())
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
data.ResponseHeaders(w)
w.WriteHeader(int(a)) w.WriteHeader(int(a))
return false, nil return false, nil

View File

@@ -33,6 +33,8 @@ func (a Drop) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Request
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", "0") w.Header().Set("Content-Length", "0")
w.Header().Set("Connection", "close") w.Header().Set("Connection", "close")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
return false, nil return false, nil

View File

@@ -295,8 +295,8 @@ func (d *RequestData) EvaluateChallenges(w http.ResponseWriter, r *http.Request)
challengeMap, err := d.verifyChallengeState() challengeMap, err := d.verifyChallengeState()
if err != nil { if err != nil {
if !errors.Is(err, http.ErrNoCookie) { if !errors.Is(err, http.ErrNoCookie) {
//clear invalid cookie and continue //queue resend invalid cookie and continue
utils.ClearCookie(d.cookieName, w, r) d.challengeMapModified = true
} }
challengeMap = make(TokenChallengeMap) challengeMap = make(TokenChallengeMap)
} }
@@ -329,6 +329,9 @@ func (d *RequestData) ResponseHeaders(w http.ResponseWriter) {
//w.Header().Set("Accept-CH", "Sec-CH-UA, Sec-CH-UA-Platform") //w.Header().Set("Accept-CH", "Sec-CH-UA, Sec-CH-UA-Platform")
//w.Header().Set("Critical-CH", "Sec-CH-UA, Sec-CH-UA-Platform") //w.Header().Set("Critical-CH", "Sec-CH-UA, Sec-CH-UA-Platform")
// send Vary header to mark that response may vary based on Cookie values and other client headers
w.Header().Set("Vary", "Cookie, Accept, Accept-Encoding, Accept-Language, User-Agent")
if d.State.Settings().MainName != "" { if d.State.Settings().MainName != "" {
w.Header().Add("Via", fmt.Sprintf("%s %s@%s", d.r.Proto, d.State.Settings().MainName, d.State.Settings().MainVersion)) w.Header().Add("Via", fmt.Sprintf("%s %s@%s", d.r.Proto, d.State.Settings().MainName, d.State.Settings().MainVersion))
} }
@@ -350,7 +353,7 @@ func (d *RequestData) RequestHeaders(headers http.Header) {
if d.State.Settings().ClientIpHeader != "" { if d.State.Settings().ClientIpHeader != "" {
headers.Del(d.State.Settings().ClientIpHeader) headers.Del(d.State.Settings().ClientIpHeader)
} }
headers.Set(d.State.Settings().BackendIpHeader, d.RemoteAddress.String()) headers.Set(d.State.Settings().BackendIpHeader, d.RemoteAddress.Addr().Unmap().String())
} }
for id, result := range d.ChallengeVerify { for id, result := range d.ChallengeVerify {
@@ -365,7 +368,7 @@ func (d *RequestData) RequestHeaders(headers http.Header) {
} }
} }
if ja4, ok := d.fp["fp4"]; ok { if ja4, ok := d.fp["ja4"]; ok {
headers.Set("X-TLS-Fingerprint-JA4", ja4) headers.Set("X-TLS-Fingerprint-JA4", ja4)
} }

View File

@@ -8,7 +8,9 @@ import (
"git.gammaspectra.live/git/go-away/utils" "git.gammaspectra.live/git/go-away/utils"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time"
) )
var ErrInvalidToken = errors.New("invalid token") var ErrInvalidToken = errors.New("invalid token")
@@ -47,6 +49,7 @@ const (
QueryArgRequestId = QueryArgPrefix + "_id" QueryArgRequestId = QueryArgPrefix + "_id"
QueryArgChallenge = QueryArgPrefix + "_challenge" QueryArgChallenge = QueryArgPrefix + "_challenge"
QueryArgToken = QueryArgPrefix + "_token" QueryArgToken = QueryArgPrefix + "_token"
QueryArgBust = QueryArgPrefix + "_bust"
) )
const MakeChallengeUrlSuffix = "/make-challenge" const MakeChallengeUrlSuffix = "/make-challenge"
@@ -91,12 +94,13 @@ func VerifyUrl(r *http.Request, reg *Registration, token string) (*url.URL, erro
uri.Path = reg.Path + VerifyChallengeUrlSuffix uri.Path = reg.Path + VerifyChallengeUrlSuffix
data := RequestDataFromContext(r.Context()) data := RequestDataFromContext(r.Context())
values := uri.Query() values, _ := utils.ParseRawQuery(r.URL.RawQuery)
values.Set(QueryArgRequestId, data.Id.String()) values.Set(QueryArgRequestId, url.QueryEscape(data.Id.String()))
values.Set(QueryArgRedirect, redirectUrl.String()) values.Set(QueryArgRedirect, url.QueryEscape(redirectUrl.String()))
values.Set(QueryArgToken, token) values.Set(QueryArgToken, url.QueryEscape(token))
values.Set(QueryArgChallenge, reg.Name) values.Set(QueryArgChallenge, url.QueryEscape(reg.Name))
uri.RawQuery = values.Encode() values.Set(QueryArgBust, url.QueryEscape(strconv.FormatInt(time.Now().UTC().UnixMilli(), 10)))
uri.RawQuery = utils.EncodeRawQuery(values)
return uri, nil return uri, nil
} }
@@ -108,13 +112,13 @@ func RedirectUrl(r *http.Request, reg *Registration) (*url.URL, error) {
} }
data := RequestDataFromContext(r.Context()) data := RequestDataFromContext(r.Context())
values := uri.Query() values, _ := utils.ParseRawQuery(r.URL.RawQuery)
values.Set(QueryArgRequestId, data.Id.String()) values.Set(QueryArgRequestId, url.QueryEscape(data.Id.String()))
if ref := r.Referer(); ref != "" { if ref := r.Referer(); ref != "" {
values.Set(QueryArgReferer, r.Referer()) values.Set(QueryArgReferer, url.QueryEscape(r.Referer()))
} }
values.Set(QueryArgChallenge, reg.Name) values.Set(QueryArgChallenge, url.QueryEscape(reg.Name))
uri.RawQuery = values.Encode() uri.RawQuery = utils.EncodeRawQuery(values)
return uri, nil return uri, nil
} }

View File

@@ -108,12 +108,14 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
} }
} }
data := challenge.RequestDataFromContext(r.Context())
request, err := http.NewRequest(params.HttpMethod, params.Url, nil) request, err := http.NewRequest(params.HttpMethod, params.Url, nil)
if err != nil { if err != nil {
return challenge.VerifyResultFail return challenge.VerifyResultFail
} }
var excludeHeaders = []string{"Host", "Content-Length"} var excludeHeaders = []string{"Host", "Content-Length", "Upgrade", "Accept-Encoding", "Range"}
for k, v := range r.Header { for k, v := range r.Header {
if slices.Contains(excludeHeaders, k) { if slices.Contains(excludeHeaders, k) {
// skip these parameters // skip these parameters
@@ -121,10 +123,12 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
} }
request.Header[k] = v request.Header[k] = v
} }
// set id
request.Header.Set("X-Away-Id", challenge.RequestDataFromContext(r.Context()).Id.String()) // set id, ip, and other headers
data.RequestHeaders(request.Header)
// set request info in X headers // set request info in X headers
request.Header.Set("X-Away-Method", r.Method)
request.Header.Set("X-Away-Host", r.Host) request.Header.Set("X-Away-Host", r.Host)
request.Header.Set("X-Away-Path", r.URL.Path) request.Header.Set("X-Away-Path", r.URL.Path)
request.Header.Set("X-Away-Query", r.URL.RawQuery) request.Header.Set("X-Away-Query", r.URL.RawQuery)
@@ -136,8 +140,6 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
defer response.Body.Close() defer response.Body.Close()
defer io.Copy(io.Discard, response.Body) defer io.Copy(io.Discard, response.Body)
data := challenge.RequestDataFromContext(r.Context())
if response.StatusCode != params.HttpCode { if response.StatusCode != params.HttpCode {
data.IssueChallengeToken(reg, key, sum, expiry, false) data.IssueChallengeToken(reg, key, sum, expiry, false)
return challenge.VerifyResultNotOK return challenge.VerifyResultNotOK

View File

@@ -52,15 +52,13 @@ func GetChallengeKeyForRequest(state StateInterface, reg *Registration, until ti
hasher.Write([]byte{0}) hasher.Write([]byte{0})
// specific headers // specific headers
for _, k := range []string{ for _, k := range reg.KeyHeaders {
"Accept-Language", hasher.Write([]byte(k))
// General browser information hasher.Write([]byte{0})
"User-Agent", for _, v := range r.Header.Values(k) {
// TODO: not sent in preload hasher.Write([]byte(v))
//"Sec-Ch-Ua", hasher.Write([]byte{1})
//"Sec-Ch-Ua-Platform", }
} {
hasher.Write([]byte(r.Header.Get(k)))
hasher.Write([]byte{0}) hasher.Write([]byte{0})
} }
hasher.Write([]byte{0}) hasher.Write([]byte{0})

View File

@@ -44,6 +44,9 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
reg.Class = challenge.ClassTransparent reg.Class = challenge.ClassTransparent
// some of regular headers are not sent in default headers
reg.KeyHeaders = challenge.MinimalKeyHeaders
ob := challenge.NewAwaiter[string]() ob := challenge.NewAwaiter[string]()
reg.Object = ob reg.Object = ob
@@ -66,9 +69,9 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
} }
// remove redirect args // remove redirect args
values := uri.Query() values, _ := utils.ParseRawQuery(uri.RawQuery)
values.Del(challenge.QueryArgRedirect) values.Del(challenge.QueryArgRedirect)
uri.RawQuery = values.Encode() uri.RawQuery = utils.EncodeRawQuery(values)
// Redirect URI must be absolute to work // Redirect URI must be absolute to work
uri.Scheme = utils.GetRequestScheme(r) uri.Scheme = utils.GetRequestScheme(r)
@@ -98,6 +101,7 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
mux.HandleFunc("GET "+reg.Path+challenge.VerifyChallengeUrlSuffix, func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("GET "+reg.Path+challenge.VerifyChallengeUrlSuffix, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css; charset=utf-8") w.Header().Set("Content-Type", "text/css; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Content-Length", "0") w.Header().Set("Content-Length", "0")
data := challenge.RequestDataFromContext(r.Context()) data := challenge.RequestDataFromContext(r.Context())

View File

@@ -35,6 +35,24 @@ var idCounter Id
// DefaultDuration TODO: adjust // DefaultDuration TODO: adjust
const DefaultDuration = time.Hour * 24 * 7 const DefaultDuration = time.Hour * 24 * 7
var DefaultKeyHeaders = []string{
// General browser information
"User-Agent",
// Accept headers
"Accept-Language",
"Accept-Encoding",
// NOTE: not sent in preload
"Sec-Ch-Ua",
"Sec-Ch-Ua-Platform",
}
var MinimalKeyHeaders = []string{
"Accept-Language",
// General browser information
"User-Agent",
}
func (r Register) Create(state StateInterface, name string, pol policy.Challenge, replacer *strings.Replacer) (*Registration, Id, error) { func (r Register) Create(state StateInterface, name string, pol policy.Challenge, replacer *strings.Replacer) (*Registration, Id, error) {
runtime, ok := Runtimes[pol.Runtime] runtime, ok := Runtimes[pol.Runtime]
if !ok { if !ok {
@@ -42,9 +60,10 @@ func (r Register) Create(state StateInterface, name string, pol policy.Challenge
} }
reg := &Registration{ reg := &Registration{
Name: name, Name: name,
Path: path.Join(state.UrlPath(), "challenge", name), Path: path.Join(state.UrlPath(), "challenge", name),
Duration: pol.Duration, Duration: pol.Duration,
KeyHeaders: DefaultKeyHeaders,
} }
if reg.Duration == 0 { if reg.Duration == 0 {
@@ -126,6 +145,9 @@ type Registration struct {
Verify VerifyFunc Verify VerifyFunc
VerifyProbability float64 VerifyProbability float64
// KeyHeaders The client headers used in key generation, in this order
KeyHeaders []string
// IssueChallenge Issues a challenge to a request. // IssueChallenge Issues a challenge to a request.
// If Class is ClassTransparent and VerifyResult is !VerifyResult.Ok(), continue with other challenges // If Class is ClassTransparent and VerifyResult is !VerifyResult.Ok(), continue with other challenges
// TODO: have this return error as well // TODO: have this return error as well

View File

@@ -23,9 +23,13 @@ func FillRegistrationHeader(state challenge.StateInterface, reg *challenge.Regis
return challenge.VerifyResultFail return challenge.VerifyResultFail
} }
redirectUri, err := challenge.RedirectUrl(r, reg)
if err != nil {
return challenge.VerifyResultFail
}
// self redirect! // self redirect!
//TODO: adjust deadline //TODO: adjust deadline
w.Header().Set("Refresh", "2; url="+r.URL.String()) w.Header().Set("Refresh", "2; url="+redirectUri.String())
state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{ state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{
"LinkTags": []map[string]string{ "LinkTags": []map[string]string{
@@ -44,6 +48,7 @@ func FillRegistrationHeader(state challenge.StateInterface, reg *challenge.Regis
mux.HandleFunc("GET "+reg.Path+challenge.VerifyChallengeUrlSuffix, challenge.VerifyHandlerFunc(state, reg, nil, func(state challenge.StateInterface, data *challenge.RequestData, w http.ResponseWriter, r *http.Request, verifyResult challenge.VerifyResult, err error, redirect string) { mux.HandleFunc("GET "+reg.Path+challenge.VerifyChallengeUrlSuffix, challenge.VerifyHandlerFunc(state, reg, nil, func(state challenge.StateInterface, data *challenge.RequestData, w http.ResponseWriter, r *http.Request, verifyResult challenge.VerifyResult, err error, redirect string) {
//TODO: add other types inside css that need to be loaded! //TODO: add other types inside css that need to be loaded!
w.Header().Set("Content-Type", "text/css; charset=utf-8") w.Header().Set("Content-Type", "text/css; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Content-Length", "0") w.Header().Set("Content-Length", "0")
data.ResponseHeaders(w) data.ResponseHeaders(w)

View File

@@ -23,6 +23,7 @@ func ServeChallengeScript(w http.ResponseWriter, r *http.Request, reg *Registrat
//TODO: log //TODO: log
panic(err) panic(err)
} }
data.ResponseHeaders(w) data.ResponseHeaders(w)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@@ -30,7 +31,7 @@ func ServeChallengeScript(w http.ResponseWriter, r *http.Request, reg *Registrat
"Id": data.Id.String(), "Id": data.Id.String(),
"Path": reg.Path, "Path": reg.Path,
"Parameters": paramData, "Parameters": paramData,
"Random": utils.CacheBust(), "Random": utils.StaticCacheBust(),
"Challenge": reg.Name, "Challenge": reg.Name,
"ChallengeScript": script, "ChallengeScript": script,
"Strings": data.State.Strings(), "Strings": data.State.Strings(),

View File

@@ -97,7 +97,7 @@ func FillJavaScriptRegistration(state challenge.StateInterface, reg *challenge.R
reg.IssueChallenge = func(w http.ResponseWriter, r *http.Request, key challenge.Key, expiry time.Time) challenge.VerifyResult { reg.IssueChallenge = func(w http.ResponseWriter, r *http.Request, key challenge.Key, expiry time.Time) challenge.VerifyResult {
state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{ state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{
"EndTags": []template.HTML{ "EndTags": []template.HTML{
template.HTML(fmt.Sprintf("<script async type=\"module\" src=\"%s?cacheBust=%s\"></script>", reg.Path+"/script.mjs", utils.CacheBust())), template.HTML(fmt.Sprintf("<script async type=\"module\" src=\"%s?cacheBust=%s\"></script>", reg.Path+"/script.mjs", utils.StaticCacheBust())),
}, },
}) })
return challenge.VerifyResultNone return challenge.VerifyResultNone
@@ -164,6 +164,9 @@ func FillJavaScriptRegistration(state challenge.StateInterface, reg *challenge.R
w.Header()[k] = v w.Header()[k] = v
} }
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(out.Data))) w.Header().Set("Content-Length", fmt.Sprintf("%d", len(out.Data)))
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
data.ResponseHeaders(w)
w.WriteHeader(out.Code) w.WriteHeader(out.Code)
_, _ = w.Write(out.Data) _, _ = w.Write(out.Data)
return nil return nil

View File

@@ -246,19 +246,20 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
if fromChallenge { if fromChallenge {
r.Header.Del("Referer") r.Header.Del("Referer")
} }
q := r.URL.Query()
q := r.URL.Query()
if ref := q.Get(challenge.QueryArgReferer); ref != "" { if ref := q.Get(challenge.QueryArgReferer); ref != "" {
r.Header.Set("Referer", ref) r.Header.Set("Referer", ref)
} }
rawQ, _ := utils.ParseRawQuery(r.URL.RawQuery)
// delete query parameters that were set by go-away // delete query parameters that were set by go-away
for k := range q { for k := range rawQ {
if strings.HasPrefix(k, challenge.QueryArgPrefix) { if strings.HasPrefix(k, challenge.QueryArgPrefix) {
q.Del(k) rawQ.Del(k)
} }
} }
r.URL.RawQuery = q.Encode() r.URL.RawQuery = utils.EncodeRawQuery(rawQ)
data.ExtraHeaders.Set("X-Away-Rule", ruleName) data.ExtraHeaders.Set("X-Away-Rule", ruleName)
data.ExtraHeaders.Set("X-Away-Action", string(ruleAction)) data.ExtraHeaders.Set("X-Away-Action", string(ruleAction))

View File

@@ -106,6 +106,12 @@ func (b Backend) Create() (*httputil.ReverseProxy, error) {
if b.IpHeader != "" || b.Host != "" || !b.Transparent { if b.IpHeader != "" || b.Host != "" || !b.Transparent {
director := proxy.Director director := proxy.Director
proxy.Director = func(req *http.Request) { proxy.Director = func(req *http.Request) {
if !b.Transparent {
if data := challenge.RequestDataFromContext(req.Context()); data != nil {
data.RequestHeaders(req.Header)
}
}
if b.IpHeader != "" && !b.Transparent { if b.IpHeader != "" && !b.Transparent {
if ip := utils.GetRemoteAddress(req.Context()); ip != nil { if ip := utils.GetRemoteAddress(req.Context()); ip != nil {
req.Header.Set(b.IpHeader, ip.Addr().Unmap().String()) req.Header.Set(b.IpHeader, ip.Addr().Unmap().String())
@@ -114,12 +120,6 @@ func (b Backend) Create() (*httputil.ReverseProxy, error) {
if b.Host != "" { if b.Host != "" {
req.Host = b.Host req.Host = b.Host
} }
if !b.Transparent {
if data := challenge.RequestDataFromContext(req.Context()); data != nil {
data.RequestHeaders(req.Header)
}
}
director(req) director(req)
} }
} }

View File

@@ -114,9 +114,9 @@ func NewState(p policy.Policy, opt settings.Settings, settings policy.StateSetti
return nil, fmt.Errorf("error loading template %s: %w", state.opt.ChallengeTemplate, err) return nil, fmt.Errorf("error loading template %s: %w", state.opt.ChallengeTemplate, err)
} }
state.opt.ChallengeTemplate = name state.opt.ChallengeTemplate = name
} else {
return nil, fmt.Errorf("no template defined for %s", state.opt.ChallengeTemplate)
} }
return nil, fmt.Errorf("no template defined for %s", state.opt.ChallengeTemplate)
} }
state.networks = make(map[string]func() cidranger.Ranger) state.networks = make(map[string]func() cidranger.Ranger)

View File

@@ -78,7 +78,7 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status
data := challenge.RequestDataFromContext(r.Context()) data := challenge.RequestDataFromContext(r.Context())
input := make(map[string]any) input := make(map[string]any)
input["Id"] = data.Id.String() input["Id"] = data.Id.String()
input["Random"] = utils.CacheBust() input["Random"] = utils.StaticCacheBust()
input["Path"] = state.UrlPath() input["Path"] = state.UrlPath()
input["Links"] = state.opt.Links input["Links"] = state.opt.Links
@@ -100,6 +100,7 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status
state.addCachedTags(data, r, input) state.addCachedTags(data, r, input)
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
buf := bytes.NewBuffer(make([]byte, 0, 8192)) buf := bytes.NewBuffer(make([]byte, 0, 8192))
@@ -116,12 +117,13 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status
func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int, err error, redirect string) { func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int, err error, redirect string) {
data := challenge.RequestDataFromContext(r.Context()) data := challenge.RequestDataFromContext(r.Context())
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
buf := bytes.NewBuffer(make([]byte, 0, 8192)) buf := bytes.NewBuffer(make([]byte, 0, 8192))
input := map[string]any{ input := map[string]any{
"Id": data.Id.String(), "Id": data.Id.String(),
"Random": utils.CacheBust(), "Random": utils.StaticCacheBust(),
"Error": err.Error(), "Error": err.Error(),
"Path": state.UrlPath(), "Path": state.UrlPath(),
"Theme": "", "Theme": "",

View File

@@ -7,11 +7,13 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"maps"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/netip" "net/netip"
"net/url" "net/url"
"slices"
"strings" "strings"
"time" "time"
) )
@@ -167,15 +169,51 @@ func GetRemoteAddress(ctx context.Context) *netip.AddrPort {
return &ip return &ip
} }
func CacheBust() string { func RandomCacheBust(n int) string {
return cacheBust buf := make([]byte, n)
}
var cacheBust string
func init() {
buf := make([]byte, 16)
_, _ = rand.Read(buf) _, _ = rand.Read(buf)
cacheBust = base64.RawURLEncoding.EncodeToString(buf) return base64.RawURLEncoding.EncodeToString(buf)
}
var staticCacheBust = RandomCacheBust(16)
func StaticCacheBust() string {
return staticCacheBust
}
func ParseRawQuery(rawQuery string) (m url.Values, err error) {
m = make(url.Values)
for rawQuery != "" {
var key string
key, rawQuery, _ = strings.Cut(rawQuery, "&")
if strings.Contains(key, ";") {
err = fmt.Errorf("invalid semicolon separator in query")
continue
}
if key == "" {
continue
}
key, value, _ := strings.Cut(key, "=")
m[key] = append(m[key], value)
}
return m, err
}
func EncodeRawQuery(v url.Values) string {
if len(v) == 0 {
return ""
}
var buf strings.Builder
for _, k := range slices.Sorted(maps.Keys(v)) {
vs := v[k]
for _, v := range vs {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
buf.WriteByte('=')
buf.WriteString(v)
}
}
return buf.String()
} }