34 Commits

Author SHA1 Message Date
WeebDataHoarder
0473109e60 http: allow specifying Go DNS resolver on config backends 2025-04-27 13:16:42 +02:00
WeebDataHoarder
eb96acb559 cmd: have -check use same logger as fatal errors 2025-04-27 12:18:49 +02:00
WeebDataHoarder
c33531d7eb cmd: log errors with ERROR severity via slog, additionally print newline string, fixes #12 2025-04-27 12:17:18 +02:00
WeebDataHoarder
b3eb0ab4b7 docker: remove GOAWAY_POLICY_SNIPPETS by default 2025-04-27 11:51:17 +02:00
WeebDataHoarder
45692ec6c0 readme: use proper forge for powxy 2025-04-26 00:03:43 +02:00
WeebDataHoarder
32b7c578f6 readme: add CSSWAF, rewrite table 2025-04-25 23:56:29 +02:00
WeebDataHoarder
01ef63abea challenge: quote expected challenge name on error 2025-04-25 23:20:53 +02:00
WeebDataHoarder
0b9f077b6c context: delete query parameters set by go-away 2025-04-25 22:48:34 +02:00
WeebDataHoarder
a85aa95dbd cmd: support changing path from well-known prefix, allow configuring full path 2025-04-25 22:16:09 +02:00
WeebDataHoarder
a1f97adde8 metrics: fix global state reset on policy reload 2025-04-25 22:11:08 +02:00
WeebDataHoarder
bca5b25f28 docker: include default snippets onto Dockerfile, allow multiple snippets folders, closes #8 2025-04-25 18:09:25 +02:00
WeebDataHoarder
d665036d98 examples: move desired-crawlers before undesired-networks 2025-04-25 17:59:16 +02:00
WeebDataHoarder
9300132a4b readme: mark string support and https listeners off todo list 2025-04-25 17:52:32 +02:00
WeebDataHoarder
9ebb78f09f readme: note support for string editing under templates 2025-04-25 17:35:22 +02:00
WeebDataHoarder
398675aa3c config: Add string replacement for templates, add example config.yml (close #10) 2025-04-25 17:32:45 +02:00
WeebDataHoarder
01df790e30 docker: added config/metrics/debug options 2025-04-25 13:07:34 +02:00
WeebDataHoarder
13c0c5473e ci/readme: update Codeberg mirror path 2025-04-25 12:18:29 +02:00
WeebDataHoarder
4d7436c51b cel: use generic env from https://codeberg.org/gone/http-cel 2025-04-25 12:08:55 +02:00
WeebDataHoarder
bc0eaeca21 metrics: Add rule action metrics 2025-04-25 11:40:39 +02:00
WeebDataHoarder
d6d69d0192 metrics: track DEFAULT rule hit 2025-04-25 11:40:38 +02:00
WeebDataHoarder
47f9f6fee6 metrics: Added prometheus metrics for rules and challenges 2025-04-25 11:27:42 +02:00
Alan Orth
6f3d81618c examples: add TikTokSpider
Requests using this user agent are coming from the same Amazon net-
works as Bytespider.
2025-04-25 11:02:48 +03:00
WeebDataHoarder
1f84f5e981 examples: forgejo: Add branches/tags listing on repo to API endpoints 2025-04-24 20:51:15 +02:00
WeebDataHoarder
1e569571a0 readme: cleanup other project forge icons 2025-04-24 18:34:25 +02:00
WeebDataHoarder
ef89de8914 readme: Added https://git.sequentialread.com/forest/pow-bot-deterrent to other projects 2025-04-24 18:26:06 +02:00
WeebDataHoarder
9541c58eeb settings: introduce settings YAML file to complement cmd arguments 2025-04-24 18:26:06 +02:00
Alan Orth
fc7d67ad70 Add examples/snippets/bot-uptimerobot.yml
Add network prefixes and user agent for UptimeRobot.

Source: https://uptimerobot.com/help/locations/
2025-04-24 13:39:33 +03:00
WeebDataHoarder
96870cc192 dnsbl: normal error handling on resolution error 2025-04-24 00:02:06 +02:00
WeebDataHoarder
74a067ae10 ci: use mirror for image fetches 2025-04-23 23:45:43 +02:00
WeebDataHoarder
3bbd50764a challenge: add cookie prefix to cookies tied to host/pubkey to prevent reuse 2025-04-23 22:38:14 +02:00
WeebDataHoarder
49e46e7e9f condition: fix http query values context 2025-04-23 22:29:17 +02:00
WeebDataHoarder
cd372e1512 challenge: Skip already issued challenges 2025-04-23 22:06:11 +02:00
WeebDataHoarder
cef915b353 http: use Query.Get instead of FormValue, allows POST through 2025-04-23 21:30:39 +02:00
WeebDataHoarder
10ceca02c9 docker: Remove outdated DNSBL argument 2025-04-23 21:15:56 +02:00
42 changed files with 1429 additions and 1014 deletions

View File

@@ -1,5 +1,5 @@
// yaml_stream.jsonnet // yaml_stream.jsonnet
local Build(go, alpine, os, arch) = { local Build(mirror, go, alpine, os, arch) = {
kind: "pipeline", kind: "pipeline",
type: "docker", type: "docker",
name: "build-" + go + "-alpine" + alpine + "-" + arch, name: "build-" + go + "-alpine" + alpine + "-" + arch,
@@ -17,6 +17,7 @@ local Build(go, alpine, os, arch) = {
{ {
name: "build", name: "build",
image: "golang:" + go +"-alpine" + alpine, image: "golang:" + go +"-alpine" + alpine,
mirror: mirror,
commands: [ commands: [
"apk update", "apk update",
"apk add --no-cache git", "apk add --no-cache git",
@@ -28,6 +29,7 @@ local Build(go, alpine, os, arch) = {
{ {
name: "check-policy-forgejo", name: "check-policy-forgejo",
image: "alpine:" + alpine, image: "alpine:" + alpine,
mirror: mirror,
depends_on: ["build"], depends_on: ["build"],
commands: [ commands: [
"./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80 --policy examples/forgejo.yml --policy-snippets examples/snippets/" "./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80 --policy examples/forgejo.yml --policy-snippets examples/snippets/"
@@ -36,6 +38,7 @@ local Build(go, alpine, os, arch) = {
{ {
name: "check-policy-generic", name: "check-policy-generic",
image: "alpine:" + alpine, image: "alpine:" + alpine,
mirror: mirror,
depends_on: ["build"], depends_on: ["build"],
commands: [ commands: [
"./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80 --policy examples/generic.yml --policy-snippets examples/snippets/" "./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80 --policy examples/generic.yml --policy-snippets examples/snippets/"
@@ -44,6 +47,7 @@ local Build(go, alpine, os, arch) = {
{ {
name: "test-wasm-success", name: "test-wasm-success",
image: "alpine:" + alpine, image: "alpine:" + alpine,
mirror: mirror,
depends_on: ["build"], depends_on: ["build"],
commands: [ commands: [
"./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm " + "./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm " +
@@ -56,6 +60,7 @@ local Build(go, alpine, os, arch) = {
{ {
name: "test-wasm-fail", name: "test-wasm-fail",
image: "alpine:" + alpine, image: "alpine:" + alpine,
mirror: mirror,
depends_on: ["build"], depends_on: ["build"],
commands: [ commands: [
"./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm " + "./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm " +
@@ -68,7 +73,7 @@ local Build(go, alpine, os, arch) = {
] ]
}; };
local Publish(registry, repo, secret, go, alpine, os, arch, trigger, platforms, extra) = { local Publish(mirror, registry, repo, secret, go, alpine, os, arch, trigger, platforms, extra) = {
kind: "pipeline", kind: "pipeline",
type: "docker", type: "docker",
name: "publish-" + go + "-alpine" + alpine + "-" + secret, name: "publish-" + go + "-alpine" + alpine + "-" + secret,
@@ -78,6 +83,15 @@ local Publish(registry, repo, secret, go, alpine, os, arch, trigger, platforms,
}, },
trigger: trigger, trigger: trigger,
steps: [ steps: [
{
name: "setup-buildkitd",
image: "alpine:" + alpine,
mirror: mirror,
commands: [
"echo '[registry.\"docker.io\"]' > buildkitd.toml",
"echo ' mirrors = [\"mirror.gcr.io\"]' >> buildkitd.toml"
],
},
{ {
name: "docker", name: "docker",
image: "plugins/buildx", image: "plugins/buildx",
@@ -87,13 +101,15 @@ local Publish(registry, repo, secret, go, alpine, os, arch, trigger, platforms,
SOURCE_DATE_EPOCH: 0, SOURCE_DATE_EPOCH: 0,
TZ: "UTC", TZ: "UTC",
LC_ALL: "C", LC_ALL: "C",
PLUGIN_BUILDER_CONFIG: "buildkitd.toml",
PLUGIN_BUILDER_DRIVER: "docker-container",
}, },
settings: { settings: {
registry: registry, registry: registry,
repo: repo, repo: repo,
mirror: mirror,
compress: true, compress: true,
platform: platforms, platform: platforms,
builder_driver: "docker-container",
build_args: { build_args: {
from_builder: "golang:" + go +"-alpine" + alpine, from_builder: "golang:" + go +"-alpine" + alpine,
from: "alpine:" + alpine, from: "alpine:" + alpine,
@@ -116,17 +132,19 @@ local containerArchitectures = ["linux/amd64", "linux/arm64", "linux/riscv64"];
local alpineVersion = "3.21"; local alpineVersion = "3.21";
local goVersion = "1.24"; local goVersion = "1.24";
local mirror = "https://mirror.gcr.io";
[ [
Build(goVersion, alpineVersion, "linux", "amd64"), Build(mirror, goVersion, alpineVersion, "linux", "amd64"),
Build(goVersion, alpineVersion, "linux", "arm64"), Build(mirror, goVersion, alpineVersion, "linux", "arm64"),
# latest # latest
Publish("git.gammaspectra.live", "git.gammaspectra.live/git/go-away", "git", goVersion, alpineVersion, "linux", "amd64", {event: ["push"], branch: ["master"], }, containerArchitectures, {tags: ["latest"],}) + {name: "publish-latest-git"}, Publish(mirror, "git.gammaspectra.live", "git.gammaspectra.live/git/go-away", "git", goVersion, alpineVersion, "linux", "amd64", {event: ["push"], branch: ["master"], }, containerArchitectures, {tags: ["latest"],}) + {name: "publish-latest-git"},
Publish("codeberg.org", "codeberg.org/weebdatahoarder/go-away", "codeberg", goVersion, alpineVersion, "linux", "amd64", {event: ["push"], branch: ["master"], }, containerArchitectures, {tags: ["latest"],}) + {name: "publish-latest-codeberg"}, Publish(mirror, "codeberg.org", "codeberg.org/gone/go-away", "codeberg", goVersion, alpineVersion, "linux", "amd64", {event: ["push"], branch: ["master"], }, containerArchitectures, {tags: ["latest"],}) + {name: "publish-latest-codeberg"},
Publish("ghcr.io", "ghcr.io/weebdatahoarder/go-away", "github", goVersion, alpineVersion, "linux", "amd64", {event: ["push"], branch: ["master"], }, containerArchitectures, {tags: ["latest"],}) + {name: "publish-latest-github"}, Publish(mirror, "ghcr.io", "ghcr.io/weebdatahoarder/go-away", "github", goVersion, alpineVersion, "linux", "amd64", {event: ["push"], branch: ["master"], }, containerArchitectures, {tags: ["latest"],}) + {name: "publish-latest-github"},
# modern # modern
Publish("git.gammaspectra.live", "git.gammaspectra.live/git/go-away", "git", goVersion, alpineVersion, "linux", "amd64", {event: ["promote", "tag"], target: ["production"], }, containerArchitectures, {auto_tag: true,}), Publish(mirror, "git.gammaspectra.live", "git.gammaspectra.live/git/go-away", "git", goVersion, alpineVersion, "linux", "amd64", {event: ["promote", "tag"], target: ["production"], }, containerArchitectures, {auto_tag: true,}),
Publish("codeberg.org", "codeberg.org/weebdatahoarder/go-away", "codeberg", goVersion, alpineVersion, "linux", "amd64", {event: ["promote", "tag"], target: ["production"], }, containerArchitectures, {auto_tag: true,}), Publish(mirror, "codeberg.org", "codeberg.org/gone/go-away", "codeberg", goVersion, alpineVersion, "linux", "amd64", {event: ["promote", "tag"], target: ["production"], }, containerArchitectures, {auto_tag: true,}),
Publish("ghcr.io", "ghcr.io/weebdatahoarder/go-away", "github", goVersion, alpineVersion, "linux", "amd64", {event: ["promote", "tag"], target: ["production"], }, containerArchitectures, {auto_tag: true,}), Publish(mirror, "ghcr.io", "ghcr.io/weebdatahoarder/go-away", "github", goVersion, alpineVersion, "linux", "amd64", {event: ["promote", "tag"], target: ["production"], }, containerArchitectures, {auto_tag: true,}),
] ]

View File

@@ -17,6 +17,7 @@ steps:
- 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= -o ./.bin/go-away ./cmd/go-away
- go build -v -o ./.bin/test-wasm-runtime ./cmd/test-wasm-runtime - go build -v -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
name: build name: build
- commands: - commands:
- ./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80 - ./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80
@@ -24,6 +25,7 @@ steps:
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.21
mirror: https://mirror.gcr.io
name: check-policy-forgejo name: check-policy-forgejo
- commands: - commands:
- ./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80 - ./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80
@@ -31,6 +33,7 @@ steps:
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.21
mirror: https://mirror.gcr.io
name: check-policy-generic name: check-policy-generic
- commands: - commands:
- ./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm - ./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm
@@ -41,6 +44,7 @@ steps:
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.21
mirror: https://mirror.gcr.io
name: test-wasm-success name: test-wasm-success
- commands: - commands:
- ./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm - ./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm
@@ -51,6 +55,7 @@ steps:
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.21
mirror: https://mirror.gcr.io
name: test-wasm-fail name: test-wasm-fail
type: docker type: docker
--- ---
@@ -72,6 +77,7 @@ steps:
- 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= -o ./.bin/go-away ./cmd/go-away
- go build -v -o ./.bin/test-wasm-runtime ./cmd/test-wasm-runtime - go build -v -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
name: build name: build
- commands: - commands:
- ./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80 - ./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80
@@ -79,6 +85,7 @@ steps:
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.21
mirror: https://mirror.gcr.io
name: check-policy-forgejo name: check-policy-forgejo
- commands: - commands:
- ./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80 - ./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80
@@ -86,6 +93,7 @@ steps:
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.21
mirror: https://mirror.gcr.io
name: check-policy-generic name: check-policy-generic
- commands: - commands:
- ./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm - ./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm
@@ -96,6 +104,7 @@ steps:
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.21
mirror: https://mirror.gcr.io
name: test-wasm-success name: test-wasm-success
- commands: - commands:
- ./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm - ./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm
@@ -106,6 +115,7 @@ steps:
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.21
mirror: https://mirror.gcr.io
name: test-wasm-fail name: test-wasm-fail
type: docker type: docker
--- ---
@@ -115,9 +125,17 @@ platform:
arch: amd64 arch: amd64
os: linux os: linux
steps: steps:
- commands:
- echo '[registry."docker.io"]' > buildkitd.toml
- echo ' mirrors = ["mirror.gcr.io"]' >> buildkitd.toml
image: alpine:3.21
mirror: https://mirror.gcr.io
name: setup-buildkitd
- environment: - environment:
DOCKER_BUILDKIT: "1" DOCKER_BUILDKIT: "1"
LC_ALL: C LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0 SOURCE_DATE_EPOCH: 0
TZ: UTC TZ: UTC
image: plugins/buildx image: plugins/buildx
@@ -128,8 +146,8 @@ steps:
build_args: build_args:
from: alpine:3.21 from: alpine:3.21
from_builder: golang:1.24-alpine3.21 from_builder: golang:1.24-alpine3.21
builder_driver: docker-container
compress: true compress: true
mirror: https://mirror.gcr.io
password: password:
from_secret: git_password from_secret: git_password
platform: platform:
@@ -155,9 +173,17 @@ platform:
arch: amd64 arch: amd64
os: linux os: linux
steps: steps:
- commands:
- echo '[registry."docker.io"]' > buildkitd.toml
- echo ' mirrors = ["mirror.gcr.io"]' >> buildkitd.toml
image: alpine:3.21
mirror: https://mirror.gcr.io
name: setup-buildkitd
- environment: - environment:
DOCKER_BUILDKIT: "1" DOCKER_BUILDKIT: "1"
LC_ALL: C LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0 SOURCE_DATE_EPOCH: 0
TZ: UTC TZ: UTC
image: plugins/buildx image: plugins/buildx
@@ -168,8 +194,8 @@ steps:
build_args: build_args:
from: alpine:3.21 from: alpine:3.21
from_builder: golang:1.24-alpine3.21 from_builder: golang:1.24-alpine3.21
builder_driver: docker-container
compress: true compress: true
mirror: https://mirror.gcr.io
password: password:
from_secret: codeberg_password from_secret: codeberg_password
platform: platform:
@@ -177,7 +203,7 @@ steps:
- linux/arm64 - linux/arm64
- linux/riscv64 - linux/riscv64
registry: codeberg.org registry: codeberg.org
repo: codeberg.org/weebdatahoarder/go-away repo: codeberg.org/gone/go-away
tags: tags:
- latest - latest
username: username:
@@ -195,9 +221,17 @@ platform:
arch: amd64 arch: amd64
os: linux os: linux
steps: steps:
- commands:
- echo '[registry."docker.io"]' > buildkitd.toml
- echo ' mirrors = ["mirror.gcr.io"]' >> buildkitd.toml
image: alpine:3.21
mirror: https://mirror.gcr.io
name: setup-buildkitd
- environment: - environment:
DOCKER_BUILDKIT: "1" DOCKER_BUILDKIT: "1"
LC_ALL: C LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0 SOURCE_DATE_EPOCH: 0
TZ: UTC TZ: UTC
image: plugins/buildx image: plugins/buildx
@@ -208,8 +242,8 @@ steps:
build_args: build_args:
from: alpine:3.21 from: alpine:3.21
from_builder: golang:1.24-alpine3.21 from_builder: golang:1.24-alpine3.21
builder_driver: docker-container
compress: true compress: true
mirror: https://mirror.gcr.io
password: password:
from_secret: github_password from_secret: github_password
platform: platform:
@@ -235,9 +269,17 @@ platform:
arch: amd64 arch: amd64
os: linux os: linux
steps: steps:
- commands:
- echo '[registry."docker.io"]' > buildkitd.toml
- echo ' mirrors = ["mirror.gcr.io"]' >> buildkitd.toml
image: alpine:3.21
mirror: https://mirror.gcr.io
name: setup-buildkitd
- environment: - environment:
DOCKER_BUILDKIT: "1" DOCKER_BUILDKIT: "1"
LC_ALL: C LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0 SOURCE_DATE_EPOCH: 0
TZ: UTC TZ: UTC
image: plugins/buildx image: plugins/buildx
@@ -249,8 +291,8 @@ steps:
build_args: build_args:
from: alpine:3.21 from: alpine:3.21
from_builder: golang:1.24-alpine3.21 from_builder: golang:1.24-alpine3.21
builder_driver: docker-container
compress: true compress: true
mirror: https://mirror.gcr.io
password: password:
from_secret: git_password from_secret: git_password
platform: platform:
@@ -275,9 +317,17 @@ platform:
arch: amd64 arch: amd64
os: linux os: linux
steps: steps:
- commands:
- echo '[registry."docker.io"]' > buildkitd.toml
- echo ' mirrors = ["mirror.gcr.io"]' >> buildkitd.toml
image: alpine:3.21
mirror: https://mirror.gcr.io
name: setup-buildkitd
- environment: - environment:
DOCKER_BUILDKIT: "1" DOCKER_BUILDKIT: "1"
LC_ALL: C LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0 SOURCE_DATE_EPOCH: 0
TZ: UTC TZ: UTC
image: plugins/buildx image: plugins/buildx
@@ -289,8 +339,8 @@ steps:
build_args: build_args:
from: alpine:3.21 from: alpine:3.21
from_builder: golang:1.24-alpine3.21 from_builder: golang:1.24-alpine3.21
builder_driver: docker-container
compress: true compress: true
mirror: https://mirror.gcr.io
password: password:
from_secret: codeberg_password from_secret: codeberg_password
platform: platform:
@@ -298,7 +348,7 @@ steps:
- linux/arm64 - linux/arm64
- linux/riscv64 - linux/riscv64
registry: codeberg.org registry: codeberg.org
repo: codeberg.org/weebdatahoarder/go-away repo: codeberg.org/gone/go-away
username: username:
from_secret: codeberg_username from_secret: codeberg_username
trigger: trigger:
@@ -315,9 +365,17 @@ platform:
arch: amd64 arch: amd64
os: linux os: linux
steps: steps:
- commands:
- echo '[registry."docker.io"]' > buildkitd.toml
- echo ' mirrors = ["mirror.gcr.io"]' >> buildkitd.toml
image: alpine:3.21
mirror: https://mirror.gcr.io
name: setup-buildkitd
- environment: - environment:
DOCKER_BUILDKIT: "1" DOCKER_BUILDKIT: "1"
LC_ALL: C LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0 SOURCE_DATE_EPOCH: 0
TZ: UTC TZ: UTC
image: plugins/buildx image: plugins/buildx
@@ -329,8 +387,8 @@ steps:
build_args: build_args:
from: alpine:3.21 from: alpine:3.21
from_builder: golang:1.24-alpine3.21 from_builder: golang:1.24-alpine3.21
builder_driver: docker-container
compress: true compress: true
mirror: https://mirror.gcr.io
password: password:
from_secret: github_password from_secret: github_password
platform: platform:
@@ -350,6 +408,6 @@ trigger:
type: docker type: docker
--- ---
kind: signature kind: signature
hmac: 8aed9810938e4aa4b34c4afb35e1101f27f98a61ffe5349be9a30f22ce7480ed hmac: 7d15ec708707d96b5741471555875d0001b84da74a7688baf0bae6fea0dbf138
... ...

View File

@@ -32,14 +32,19 @@ RUN test -e "${GOBIN}/go-away"
FROM --platform=$TARGETPLATFORM ${from} FROM --platform=$TARGETPLATFORM ${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/
ENV TZ UTC ENV TZ UTC
ENV GOAWAY_METRICS_BIND=""
ENV GOAWAY_DEBUG_BIND=""
ENV GOAWAY_BIND=":8080" ENV GOAWAY_BIND=":8080"
ENV GOAWAY_BIND_NETWORK="tcp" ENV GOAWAY_BIND_NETWORK="tcp"
ENV GOAWAY_SOCKET_MODE="0770" ENV GOAWAY_SOCKET_MODE="0770"
ENV GOAWAY_CONFIG=""
ENV GOAWAY_POLICY="/policy.yml" ENV GOAWAY_POLICY="/policy.yml"
ENV GOAWAY_POLICY_SNIPPETS="/policy/snippets" ENV GOAWAY_POLICY_SNIPPETS=""
ENV GOAWAY_CHALLENGE_TEMPLATE="anubis" ENV GOAWAY_CHALLENGE_TEMPLATE="anubis"
ENV GOAWAY_CHALLENGE_TEMPLATE_THEME="" ENV GOAWAY_CHALLENGE_TEMPLATE_THEME=""
ENV GOAWAY_SLOG_LEVEL="WARN" ENV GOAWAY_SLOG_LEVEL="WARN"
@@ -47,20 +52,23 @@ ENV GOAWAY_CLIENT_IP_HEADER=""
ENV GOAWAY_BACKEND_IP_HEADER="" ENV GOAWAY_BACKEND_IP_HEADER=""
ENV GOAWAY_JWT_PRIVATE_KEY_SEED="" ENV GOAWAY_JWT_PRIVATE_KEY_SEED=""
ENV GOAWAY_BACKEND="" ENV GOAWAY_BACKEND=""
ENV GOAWAY_DNSBL="dnsbl.dronebl.org"
ENV GOAWAY_ACME_AUTOCERT="" ENV GOAWAY_ACME_AUTOCERT=""
ENV GOAWAY_CACHE="/cache" ENV GOAWAY_CACHE="/cache"
EXPOSE 8080/tcp EXPOSE 8080/tcp
EXPOSE 8080/udp EXPOSE 8080/udp
EXPOSE 9090/tcp
EXPOSE 6060/tcp
ENV JWT_PRIVATE_KEY_SEED="${GOAWAY_JWT_PRIVATE_KEY_SEED}" ENV JWT_PRIVATE_KEY_SEED="${GOAWAY_JWT_PRIVATE_KEY_SEED}"
ENTRYPOINT /bin/go-away --bind "${GOAWAY_BIND}" --bind-network "${GOAWAY_BIND_NETWORK}" --socket-mode "${GOAWAY_SOCKET_MODE}" \ ENTRYPOINT /bin/go-away --bind "${GOAWAY_BIND}" --bind-network "${GOAWAY_BIND_NETWORK}" --socket-mode "${GOAWAY_SOCKET_MODE}" \
--policy "${GOAWAY_POLICY}" --policy-snippets "${GOAWAY_POLICY_SNIPPETS}" \ --metrics-bind "${GOAWAY_METRICS_BIND}" --debug-bind "${GOAWAY_DEBUG_BIND}" \
--config "${GOAWAY_CONFIG}" \
--policy "${GOAWAY_POLICY}" --policy-snippets "/snippets" --policy-snippets "${GOAWAY_POLICY_SNIPPETS}" \
--client-ip-header "${GOAWAY_CLIENT_IP_HEADER}" --backend-ip-header "${GOAWAY_BACKEND_IP_HEADER}" \ --client-ip-header "${GOAWAY_CLIENT_IP_HEADER}" --backend-ip-header "${GOAWAY_BACKEND_IP_HEADER}" \
--cache "${GOAWAY_CACHE}" \ --cache "${GOAWAY_CACHE}" \
--dnsbl "${GOAWAY_DNSBL}" \
--challenge-template "${GOAWAY_CHALLENGE_TEMPLATE}" --challenge-template-theme "${GOAWAY_CHALLENGE_TEMPLATE_THEME}" \ --challenge-template "${GOAWAY_CHALLENGE_TEMPLATE}" --challenge-template-theme "${GOAWAY_CHALLENGE_TEMPLATE_THEME}" \
--slog-level "${GOAWAY_SLOG_LEVEL}" \ --slog-level "${GOAWAY_SLOG_LEVEL}" \
--acme-autocert "${GOAWAY_ACME_AUTOCERT}" \ --acme-autocert "${GOAWAY_ACME_AUTOCERT}" \

View File

@@ -32,7 +32,7 @@ Source code is automatically pushed to the following mirrors. Packages are also
[![GammaSpectra.live](https://img.shields.io/badge/GammaSpectra.live-main+packages-green?style=flat&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjYwMCIgaGVpZ2h0PSI2MDAiIHZpZXdCb3g9Ii0zMDAgLTMwMCA2MDAgNjAwIj4KPGNpcmNsZSByPSI1MCIvPgo8cGF0aCBkPSJNNzUsMCBBIDc1LDc1IDAgMCwwIDM3LjUsLTY0Ljk1MiBMIDEyNSwtMjE2LjUwNiBBIDI1MCwyNTAgMCAwLDEgMjUwLDAgeiIgaWQ9ImJsZCIvPgo8dXNlIHhsaW5rOmhyZWY9IiNibGQiIHRyYW5zZm9ybT0icm90YXRlKDEyMCkiLz4KPHVzZSB4bGluazpocmVmPSIjYmxkIiB0cmFuc2Zvcm09InJvdGF0ZSgyNDApIi8+Cjwvc3ZnPg==&labelColor=fff)](https://git.gammaspectra.live/git/go-away) ![](https://git.gammaspectra.live/git/go-away/badges/stars.svg?style=flat) [![](https://git.gammaspectra.live/git/go-away/badges/issues/open.svg?style=flat)](https://git.gammaspectra.live/git/go-away/issues?state=open) [![](https://git.gammaspectra.live/git/go-away/badges/pulls/open.svg?style=flat)](https://git.gammaspectra.live/git/go-away/pulls?state=open) [![GammaSpectra.live](https://img.shields.io/badge/GammaSpectra.live-main+packages-green?style=flat&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjYwMCIgaGVpZ2h0PSI2MDAiIHZpZXdCb3g9Ii0zMDAgLTMwMCA2MDAgNjAwIj4KPGNpcmNsZSByPSI1MCIvPgo8cGF0aCBkPSJNNzUsMCBBIDc1LDc1IDAgMCwwIDM3LjUsLTY0Ljk1MiBMIDEyNSwtMjE2LjUwNiBBIDI1MCwyNTAgMCAwLDEgMjUwLDAgeiIgaWQ9ImJsZCIvPgo8dXNlIHhsaW5rOmhyZWY9IiNibGQiIHRyYW5zZm9ybT0icm90YXRlKDEyMCkiLz4KPHVzZSB4bGluazpocmVmPSIjYmxkIiB0cmFuc2Zvcm09InJvdGF0ZSgyNDApIi8+Cjwvc3ZnPg==&labelColor=fff)](https://git.gammaspectra.live/git/go-away) ![](https://git.gammaspectra.live/git/go-away/badges/stars.svg?style=flat) [![](https://git.gammaspectra.live/git/go-away/badges/issues/open.svg?style=flat)](https://git.gammaspectra.live/git/go-away/issues?state=open) [![](https://git.gammaspectra.live/git/go-away/badges/pulls/open.svg?style=flat)](https://git.gammaspectra.live/git/go-away/pulls?state=open)
[![Codeberg](https://img.shields.io/badge/Codeberg-mirror+packages-2185D0?style=flat&logo=codeberg&labelColor=fff)](https://codeberg.org/WeebDataHoarder/go-away) ![](https://codeberg.org/WeebDataHoarder/go-away/badges/stars.svg?style=flat) [![Codeberg](https://img.shields.io/badge/Codeberg-mirror+packages-2185D0?style=flat&logo=codeberg&labelColor=fff)](https://codeberg.org/gone/go-away) ![](https://codeberg.org/gone/go-away/badges/stars.svg?style=flat)
[![GitHub](https://img.shields.io/badge/GitHub-mirror+packages-blue?style=flat&logo=github&labelColor=fff&logoColor=24292f)](https://github.com/WeebDataHoarder/go-away) ![](https://img.shields.io/github/stars/WeebDataHoarder/go-away?style=flat) [![GitHub](https://img.shields.io/badge/GitHub-mirror+packages-blue?style=flat&logo=github&labelColor=fff&logoColor=24292f)](https://github.com/WeebDataHoarder/go-away) ![](https://img.shields.io/github/stars/WeebDataHoarder/go-away?style=flat)
@@ -80,6 +80,8 @@ These templates are included by default:
External templates for your site can be loaded specifying a full path to the `.gohtml` file. See [embed/templates/](embed/templates/) for examples to follow. External templates for your site can be loaded specifying a full path to the `.gohtml` file. See [embed/templates/](embed/templates/) for examples to follow.
You can alter the language and strings in the templates directly from the [config.yml](#config) file if specified.
### Extended rule actions ### Extended rule actions
In addition to the common PASS / CHALLENGE / DENY rules, go-away offers more actions that can be extended via code. In addition to the common PASS / CHALLENGE / DENY rules, go-away offers more actions that can be extended via code.
@@ -294,17 +296,18 @@ However, a few points are left before go-away can be called v1.0.0:
* [x] Several parts of the code are going through a refactor, which won't impact end users or operators. * [x] Several parts of the code are going through a refactor, which won't impact end users or operators.
* [ ] Documentation is lacking and a more extensive one with inline example is in the works. * [ ] Documentation is lacking and a more extensive one with inline example is in the works.
* [x] Policy file syntax is going to stay mostly unchanged, except in the challenges definition section. * [x] Policy file syntax is going to stay mostly unchanged, except in the challenges definition section.
* [ ] Allow users to pick fallback challenges if any fail, specially with custom ones. * [ ] Allow end users to pick fallback challenges if any fail, specially with custom ones.
* [ ] Replace Anubis-like default template with own one. * [ ] Replace Anubis-like default template with own one.
* [ ] Define strings and multi-language support for quick modification by operators without custom templates. * [x] Define strings and multi-language support for quick modification by operators without custom templates.
* [ ] Have highly tested paths that match examples. * [ ] Have highly tested paths that match examples.
* [x] Caching of temporary fetches, for example, network ranges. * [x] Caching of temporary fetches, for example, network ranges.
* [x] Allow live and dynamic policy reloading. * [x] Allow live and dynamic policy reloading.
* [x] Multiple domains / subdomains -> one backend handling, CEL rules for backends * [x] Multiple domains / subdomains -> one backend handling, CEL rules for backends
* [ ] Merge all rules and conditions into one large AST for higher performance. * [ ] Merge all rules and conditions into one large AST for higher performance.
* [ ] Explore exposing a module for direct Caddy usage. * [ ] Explore exposing a module for direct Caddy usage.
* [ ] More defined way of picking HTTP/HTTP(s) listeners and certificates. * [x] More defined way of picking HTTP/HTTP(s) listeners and certificates.
* [ ] Expose metrics for gathering common network ranges, challenge solve rates and acting on them. * [x] Expose metrics for challenge solve rates and acting on them.
* [ ] Metrics for common network ranges / AS / useragent
## Setup ## Setup
@@ -312,6 +315,10 @@ go-away can take plaintext HTTP/1 and _HTTP/2_ / _h2c_ connections if desired ov
We also support the `autocert` parameter to configure HTTP(s). This will also allow TLS Fingerprinting to be done on incoming clients. This doesn't require any upstream proxies, and we recommend it's exposed directly or via SNI / Layer 4 proxying. We also support the `autocert` parameter to configure HTTP(s). This will also allow TLS Fingerprinting to be done on incoming clients. This doesn't require any upstream proxies, and we recommend it's exposed directly or via SNI / Layer 4 proxying.
### Config
While most basic configuration can be passed via the command line, we support passing a [config.yml](examples/config.yml) with more advanced setup, including string replacement or custom backends configuration.
### Binary / Go ### Binary / Go
Requires Go 1.24+. Builds statically without CGo usage. Requires Go 1.24+. Builds statically without CGo usage.
@@ -341,7 +348,7 @@ Available under [Dockerfile](Dockerfile). See the _docker compose_ below for the
Example follows a hypothetical Forgejo server running on `http://forgejo:3000` serving `git.example.com` Example follows a hypothetical Forgejo server running on `http://forgejo:3000` serving `git.example.com`
Container images are published under `git.gammaspectra.live/git/go-away`, `codeberg.org/weebdatahoarder/go-away` and `ghcr.io/weebdatahoarder/go-away` Container images are published under `git.gammaspectra.live/git/go-away`, `codeberg.org/gone/go-away` and `ghcr.io/weebdatahoarder/go-away`
```yaml ```yaml
networks: networks:
@@ -353,7 +360,7 @@ volumes:
services: services:
go-away: go-away:
# image: codeberg.org/weebdatahoarder/go-away:latest # image: codeberg.org/gone/go-away:latest
# image: ghcr.io/weebdatahoarder/go-away:latest # image: ghcr.io/weebdatahoarder/go-away:latest
image: git.gammaspectra.live/git/go-away:latest image: git.gammaspectra.live/git/go-away:latest
restart: always restart: always
@@ -366,12 +373,17 @@ services:
volumes: volumes:
- "goaway_cache:/cache" - "goaway_cache:/cache"
- "./examples/forgejo.yml:/policy.yml:ro" - "./examples/forgejo.yml:/policy.yml:ro"
- "./examples/snippets/:/policy/snippets/:ro" #- "./your/snippets/:/policy/snippets/:ro"
environment: environment:
#GOAWAY_BIND: ":8080" #GOAWAY_BIND: ":8080"
# Supported tcp, unix, and proxy (for enabling PROXY module for request unwrapping) # Supported tcp, unix, and proxy (for enabling PROXY module for request unwrapping)
#GOAWAY_BIND_NETWORK: "tcp" #GOAWAY_BIND_NETWORK: "tcp"
#GOAWAY_SOCKET_MODE: "0770" #GOAWAY_SOCKET_MODE: "0770"
# Enable Prometheus metrics under /metrics on this bind
#GOAWAY_METRICS_BIND: ":9090"
# Enable Go debug profiles under this bind
#GOAWAY_DEBUG_BIND: ":6060"
# set to letsencrypt or other directory URL to enable HTTPS. Above ports will be TLS only. # set to letsencrypt or other directory URL to enable HTTPS. Above ports will be TLS only.
# enables request JA3N / JA4 client TLS fingerprinting # enables request JA3N / JA4 client TLS fingerprinting
@@ -400,18 +412,21 @@ services:
# If left empty, the header on GOAWAY_CLIENT_IP_HEADER will be left as-is # If left empty, the header on GOAWAY_CLIENT_IP_HEADER will be left as-is
#GOAWAY_BACKEND_IP_HEADER: "" #GOAWAY_BACKEND_IP_HEADER: ""
# Alternate way of specifying parameters or more advanced settings
# Pass path to YAML file
#GOAWAY_CONFIG: ""
GOAWAY_POLICY: "/policy.yml" GOAWAY_POLICY: "/policy.yml"
GOAWAY_POLICY_SNIPPETS: "/policy/snippets" # Include extra snippets to load from this path.
# Note that the default snippets from example/snippets/ are included by default
#GOAWAY_POLICY_SNIPPETS: "/policy/snippets"
# Template, and theme for the template to pick. defaults to an anubis-like one # Template, and theme for the template to pick. defaults to an anubis-like one
# An file path can be specified. See embed/templates for a few examples # An file path can be specified. See embed/templates for a few examples
GOAWAY_CHALLENGE_TEMPLATE: forgejo GOAWAY_CHALLENGE_TEMPLATE: forgejo
GOAWAY_CHALLENGE_TEMPLATE_THEME: forgejo-dark GOAWAY_CHALLENGE_TEMPLATE_THEME: forgejo-dark
# specify a DNSBL for usage in conditions. Defaults to DroneBL
# GOAWAY_DNSBL: "dnsbl.dronebl.org"
# Backend to match. Can be subdomain or full wildcards, "*.example.com" or "*" # Backend to match. Can be subdomain or full wildcards, "*.example.com" or "*"
GOAWAY_BACKEND: "git.example.com=http://forgejo:3000" GOAWAY_BACKEND: "git.example.com=http://forgejo:3000"
@@ -426,9 +441,14 @@ services:
## Other Similar Projects ## Other Similar Projects
* [Anubis](https://anubis.techaro.lol/): Proxy that uses JavaScript proof of work to weight request based on rules [[source]](https://github.com/TecharoHQ/anubis)
* [powxy](https://sr.ht/~runxiyu/powxy/): Powxy is a reverse proxy that protects your upstream service by challenging clients with SHA-256 proof-of-work. [[source](https://git.sr.ht/~runxiyu/powxy)] | Project | Source Code | Description | Method |
* [anticrawl](https://flak.tedunangst.com/post/anticrawl): Go http handler / proxy for regex based rules [[source]](https://humungus.tedunangst.com/r/anticrawl) |:-----------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------|:---------------------------------------------|
| [Anubis](https://anubis.techaro.lol/) | [![GitHub](https://img.shields.io/badge/GitHub-TecharoHQ/anubis-blue?style=flat&logo=github&labelColor=fff&logoColor=24292f)](https://github.com/TecharoHQ/anubis)<br/>Go / [MIT](https://github.com/TecharoHQ/anubis/blob/main/LICENSE) | Proxy that uses JavaScript proof of work to weight request based on simple match rules | JavaScript PoW (SHA-256) |
| [powxy](https://forge.lindenii.runxiyu.org/powxy/-/repos/powxy/) | [![lindenii.runxiyu.org](https://img.shields.io/badge/lindenii-powxy-blue?style=flat&logo=git&labelColor=fff&logoColor=000)](https://forge.lindenii.runxiyu.org/powxy/-/repos/powxy/)<br/> Go / [BSD 2-Clause](https://forge.lindenii.runxiyu.org/powxy/-/repos/powxy/tree/LICENSE) | Powxy is a reverse proxy that protects your upstream service by challenging clients with proof-of-work. | JavaScript PoW (SHA-256) with manual program |
| [PoW! Bot Deterrent](https://git.sequentialread.com/forest/pow-bot-deterrent) | [![SequentialRead](https://img.shields.io/badge/SequentialRead-forest/pow--bot--deterrent-blue?style=flat&logo=gitea&labelColor=fff&logoColor=000)](https://git.sequentialread.com/forest/pow-bot-deterrent)<br/> Go / [GPL v3.0](https://git.sequentialread.com/forest/pow-bot-deterrent/src/branch/main/LICENSE.md) | A proof-of-work based bot deterrent. Lightweight, self-hosted and copyleft licensed. | JavaScript PoW (WASM scrypt) |
| [CSSWAF](https://github.com/yzqzss/csswaf) | [![GitHub](https://img.shields.io/badge/GitHub-yzqzss/csswaf-blue?style=flat&logo=github&labelColor=fff&logoColor=24292f)](https://github.com/yzqzss/csswaf)<br/>Go / [MIT](https://github.com/yzqzss/csswaf/blob/main/LICENSE) | A CSS-based NoJS Anti-BOT WAF (Proof of Concept) | Non-JS CSS Subresource loading order |
| [anticrawl](https://flak.tedunangst.com/post/anticrawl) | [![humungus.tedunangst.com](https://img.shields.io/badge/tedunangst-anticrawl-blue?style=flat&logo=mercurial&labelColor=fff&logoColor=000)](https://humungus.tedunangst.com/r/anticrawl)<br/>Go / None | Go http handler / proxy for regex based rules | Non-JS manual Challenge/Response |
## Development ## Development

View File

@@ -4,78 +4,27 @@ import (
"bytes" "bytes"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "crypto/rand"
"crypto/tls"
"encoding/hex" "encoding/hex"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"git.gammaspectra.live/git/go-away/lib" "git.gammaspectra.live/git/go-away/lib"
"git.gammaspectra.live/git/go-away/lib/policy" "git.gammaspectra.live/git/go-away/lib/policy"
"git.gammaspectra.live/git/go-away/lib/settings"
"git.gammaspectra.live/git/go-away/utils" "git.gammaspectra.live/git/go-away/utils"
"github.com/pires/go-proxyproto" "github.com/goccy/go-yaml"
"golang.org/x/crypto/acme" "github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/crypto/acme/autocert"
"log"
"log/slog" "log/slog"
"net"
"net/http" "net/http"
"net/http/pprof"
"os" "os"
"os/signal" "os/signal"
"path" "path"
"runtime/debug" "runtime/debug"
"strconv"
"strings" "strings"
"sync/atomic"
"syscall" "syscall"
) )
func setupListener(network, address, socketMode string, proxy bool) (net.Listener, string) {
if network == "proxy" {
network = "tcp"
proxy = true
}
formattedAddress := ""
switch network {
case "unix":
formattedAddress = "unix:" + address
case "tcp":
formattedAddress = "http://localhost" + address
default:
formattedAddress = fmt.Sprintf(`(%s) %s`, network, address)
}
listener, err := net.Listen(network, address)
if err != nil {
log.Fatal(fmt.Errorf("failed to bind to %s: %w", formattedAddress, err))
}
// additional permission handling for unix sockets
if network == "unix" {
mode, err := strconv.ParseUint(socketMode, 8, 0)
if err != nil {
listener.Close()
log.Fatal(fmt.Errorf("could not parse socket mode %s: %w", socketMode, err))
}
err = os.Chmod(address, os.FileMode(mode))
if err != nil {
listener.Close()
log.Fatal(fmt.Errorf("could not change socket mode: %w", err))
}
}
if proxy {
slog.Warn("listener PROXY enabled")
formattedAddress += " +PROXY"
listener = &proxyproto.Listener{
Listener: listener,
}
}
return listener, formattedAddress
}
var internalCmdName = "go-away" var internalCmdName = "go-away"
var internalMainName = "go-away" var internalMainName = "go-away"
var internalMainVersion = "dev" var internalMainVersion = "dev"
@@ -101,40 +50,29 @@ func (v *MultiVar) Set(value string) error {
return nil return nil
} }
func newACMEManager(clientDirectory string, backends map[string]http.Handler) *autocert.Manager { func fatal(err error) {
slog.Error(err.Error())
var domains []string _, _ = fmt.Fprintln(os.Stderr, "================================================")
for d := range backends { _, _ = fmt.Fprintln(os.Stderr, "Fatal error:")
parts := strings.Split(d, ":") _, _ = fmt.Fprintln(os.Stderr, err.Error())
d = parts[0] os.Exit(1)
if net.ParseIP(d) != nil {
continue
}
domains = append(domains, d)
}
manager := &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(domains...),
Client: &acme.Client{
HTTPClient: http.DefaultClient,
DirectoryURL: clientDirectory,
},
}
return manager
} }
func main() { func main() {
bind := flag.String("bind", ":8080", "network address to bind HTTP/HTTP(s) to")
bindNetwork := flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp") opt := settings.DefaultSettings
bindProxy := flag.Bool("bind-proxy", false, "use PROXY protocol in front of the listener")
socketMode := flag.String("socket-mode", "0770", "socket mode (permissions) for unix domain sockets.") flag.StringVar(&opt.Bind.Address, "bind", opt.Bind.Address, "network address to bind HTTP/HTTP(s) to")
flag.StringVar(&opt.Bind.Network, "bind-network", opt.Bind.Network, "network family to bind HTTP to, e.g. unix, tcp")
flag.BoolVar(&opt.Bind.Proxy, "bind-proxy", opt.Bind.Proxy, "use PROXY protocol in front of the listener")
flag.StringVar(&opt.Bind.SocketMode, "socket-mode", opt.Bind.SocketMode, "socket mode (permissions) for unix domain sockets.")
flag.StringVar(&opt.BindMetrics, "metrics-bind", opt.BindMetrics, "network address to bind metrics on")
flag.StringVar(&opt.BindDebug, "debug-bind", opt.BindDebug, "network address to bind debug on")
slogLevel := flag.String("slog-level", "WARN", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)") slogLevel := flag.String("slog-level", "WARN", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
debugMode := flag.Bool("debug", false, "debug mode with logs and server timings") flag.BoolVar(&opt.Bind.Passthrough, "passthrough", opt.Bind.Passthrough, "passthrough mode sends all requests to matching backends until state is loaded")
passThrough := flag.Bool("passthrough", false, "passthrough mode sends all requests to matching backends until state is loaded")
check := flag.Bool("check", false, "check configuration and policies, then exit") check := flag.Bool("check", false, "check configuration and policies, then exit")
acmeAutocert := flag.String("acme-autocert", "", "enables HTTP(s) mode and uses the provided ACME server URL or available service (available: letsencrypt)") flag.StringVar(&opt.Bind.TLSAcmeAutoCert, "acme-autocert", opt.Bind.TLSAcmeAutoCert, "enables HTTP(s) mode and uses the provided ACME server URL or available service (available: letsencrypt)")
clientIpHeader := flag.String("client-ip-header", "", "Client HTTP header to fetch their IP address from (X-Real-Ip, X-Client-Ip, X-Forwarded-For, Cf-Connecting-Ip, etc.)") clientIpHeader := flag.String("client-ip-header", "", "Client HTTP header to fetch their IP address from (X-Real-Ip, X-Client-Ip, X-Forwarded-For, Cf-Connecting-Ip, etc.)")
backendIpHeader := flag.String("backend-ip-header", "", "Backend HTTP header to set the client IP address from, if empty defaults to leaving Client header alone (X-Real-Ip, X-Client-Ip, X-Forwarded-For, Cf-Connecting-Ip, etc.)") backendIpHeader := flag.String("backend-ip-header", "", "Backend HTTP header to set the client IP address from, if empty defaults to leaving Client header alone (X-Real-Ip, X-Client-Ip, X-Forwarded-For, Cf-Connecting-Ip, etc.)")
@@ -142,19 +80,28 @@ func main() {
cachePath := flag.String("cache", path.Join(os.TempDir(), "go_away_cache"), "path to temporary cache directory") cachePath := flag.String("cache", path.Join(os.TempDir(), "go_away_cache"), "path to temporary cache directory")
policyFile := flag.String("policy", "", "path to policy YAML file") policyFile := flag.String("policy", "", "path to policy YAML file")
policySnippets := flag.String("policy-snippets", "", "path to YAML snippets folder") var policySnippets MultiVar
challengeTemplate := flag.String("challenge-template", "anubis", "name or path of the challenge template to use (anubis, forgejo)") flag.Var(&policySnippets, "policy-snippets", "path to YAML snippets folder (can be specified multiple times)")
challengeTemplateTheme := flag.String("challenge-template-theme", "", "name of the challenge template theme to use (forgejo => [forgejo-auto, forgejo-dark, forgejo-light, gitea...])")
packageName := flag.String("package-path", internalCmdName, "package name to expose in .well-known url path") flag.StringVar(&opt.ChallengeTemplate, "challenge-template", opt.ChallengeTemplate, "name or path of the challenge template to use (anubis, forgejo)")
templateTheme := flag.String("challenge-template-theme", opt.ChallengeTemplateOverrides["Theme"], "name of the challenge template theme to use (forgejo => [forgejo-auto, forgejo-dark, forgejo-light, gitea...])")
basePath := flag.String("path", "/.well-known/."+internalCmdName, "base path where to expose go-away package onto, challenges will be served from here")
jwtPrivateKeySeed := flag.String("jwt-private-key-seed", "", "Seed for the jwt private key, or on JWT_PRIVATE_KEY_SEED env. One be generated by passing \"generate\" as a value, follows RFC 8032 private key definition. Defaults to random") jwtPrivateKeySeed := flag.String("jwt-private-key-seed", "", "Seed for the jwt private key, or on JWT_PRIVATE_KEY_SEED env. One be generated by passing \"generate\" as a value, follows RFC 8032 private key definition. Defaults to random")
var backends MultiVar var backends MultiVar
flag.Var(&backends, "backend", "backend definition in the form of an.example.com=http://backend:1234 (can be specified multiple times)") flag.Var(&backends, "backend", "backend definition in the form of an.example.com=http://backend:1234 (can be specified multiple times)")
settingsFile := flag.String("config", "", "path to config override YAML file")
flag.Parse() flag.Parse()
if *backendIpHeader == "" {
*backendIpHeader = *clientIpHeader
}
var err error var err error
{ {
@@ -168,14 +115,39 @@ func main() {
leveler.Set(programLevel) leveler.Set(programLevel)
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
AddSource: *debugMode, AddSource: programLevel <= slog.LevelDebug,
Level: leveler, Level: leveler,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == "source" {
if src, ok := a.Value.Any().(*slog.Source); ok {
return slog.String(a.Key, fmt.Sprintf("%s:%d", src.File, src.Line))
}
}
return a
},
}) })
slog.SetDefault(slog.New(h)) slog.SetDefault(slog.New(h))
// set default log logger to slog logger level
slog.SetLogLoggerLevel(programLevel)
} }
slog.Info("go-away", "package", internalMainName, "version", internalMainVersion, "cmd", internalCmdName) slog.Info("go-away", "package", internalMainName, "version", internalMainVersion, "cmd", internalCmdName)
// preload missing settings
opt.ChallengeTemplateOverrides["Theme"] = *templateTheme
// load overrides
if *settingsFile != "" {
settingsData, err := os.ReadFile(*settingsFile)
if err != nil {
fatal(fmt.Errorf("could not read settings file: %w", err))
}
err = yaml.Unmarshal(settingsData, &opt)
if err != nil {
fatal(fmt.Errorf("could not parse settings file: %w", err))
}
}
var seed []byte var seed []byte
var kValue string var kValue string
@@ -189,7 +161,7 @@ func main() {
if strings.ToLower(kValue) == "generate" { if strings.ToLower(kValue) == "generate" {
_, priv, err := ed25519.GenerateKey(rand.Reader) _, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil { if err != nil {
log.Fatal(fmt.Errorf("failed to generate private key: %w", err)) fatal(fmt.Errorf("failed to generate private key: %w", err))
} }
fmt.Printf("%x\n", priv.Seed()) fmt.Printf("%x\n", priv.Seed())
os.Exit(0) os.Exit(0)
@@ -197,30 +169,42 @@ func main() {
seed, err = hex.DecodeString(kValue) seed, err = hex.DecodeString(kValue)
if err != nil { if err != nil {
log.Fatal(fmt.Errorf("failed to decode seed: %w", err)) fatal(fmt.Errorf("failed to decode seed: %w", err))
} }
if len(seed) != ed25519.SeedSize { if len(seed) != ed25519.SeedSize {
log.Fatal(fmt.Errorf("invalid seed length: %d, expected %d", len(seed), ed25519.SeedSize)) fatal(fmt.Errorf("invalid seed length: %d, expected %d", len(seed), ed25519.SeedSize))
} }
} }
createdBackends := make(map[string]http.Handler) createdBackends := make(map[string]http.Handler)
parsedBackends := make(map[string]string)
for _, backend := range backends { for _, backend := range backends {
if backend == "" {
// skip empty to allow no values
continue
}
parts := strings.Split(backend, "=") parts := strings.Split(backend, "=")
if len(parts) != 2 { if len(parts) != 2 {
log.Fatal(fmt.Errorf("invalid backend definition: %s, expected 2 parts, got %v", backend, parts)) fatal(fmt.Errorf("invalid backend definition: %s, expected 2 parts, got %v", backend, parts))
}
// make no-settings, default backend
opt.Backends[parts[0]] = settings.Backend{
URL: parts[1],
IpHeader: *backendIpHeader,
} }
parsedBackends[parts[0]] = parts[1]
} }
for k, v := range parsedBackends { for k, v := range opt.Backends {
backend, err := utils.MakeReverseProxy(v) if v.IpHeader == "" {
//set default value
v.IpHeader = *backendIpHeader
}
backend, err := v.Create()
if err != nil { if err != nil {
log.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.LevelError)
@@ -228,49 +212,29 @@ func main() {
} }
if len(createdBackends) == 0 { if len(createdBackends) == 0 {
log.Fatal(fmt.Errorf("no backends defined in policy file")) fatal(fmt.Errorf("no backends defined in cmdline or settings file"))
} }
var cache utils.Cache var cache utils.Cache
var acmeCache string
if *cachePath != "" { if *cachePath != "" {
err = os.MkdirAll(*cachePath, 0755) err = os.MkdirAll(*cachePath, 0755)
if err != nil { if err != nil {
log.Fatal(fmt.Errorf("failed to create cache directory: %w", err)) fatal(fmt.Errorf("failed to create cache directory: %w", err))
} }
for _, n := range []string{"networks", "acme"} { for _, n := range []string{"networks", "acme"} {
err = os.MkdirAll(path.Join(*cachePath, n), 0755) err = os.MkdirAll(path.Join(*cachePath, n), 0755)
if err != nil { if err != nil {
log.Fatal(fmt.Errorf("failed to create cache sub directory %s: %w", n, err)) fatal(fmt.Errorf("failed to create cache sub directory %s: %w", n, err))
} }
} }
cache, err = utils.CacheDirectory(*cachePath) cache, err = utils.CacheDirectory(*cachePath)
if err != nil { if err != nil {
log.Fatal(fmt.Errorf("failed to open cache directory: %w", err)) fatal(fmt.Errorf("failed to open cache directory: %w", err))
}
}
var tlsConfig *tls.Config
if *acmeAutocert != "" {
switch *acmeAutocert {
case "letsencrypt":
*acmeAutocert = acme.LetsEncryptURL
} }
acmeManager := newACMEManager(*acmeAutocert, createdBackends) acmeCache = path.Join(*cachePath, "acme")
if *cachePath != "" {
err = os.MkdirAll(path.Join(*cachePath, "acme"), 0755)
if err != nil {
log.Fatal(fmt.Errorf("failed to create acme cache directory: %w", err))
}
acmeManager.Cache = autocert.DirCache(path.Join(*cachePath, "acme"))
}
slog.Warn(
"acme-autocert enabled",
"directory", *acmeAutocert,
)
tlsConfig = acmeManager.TLSConfig()
} }
loadPolicyState := func() (http.Handler, error) { loadPolicyState := func() (http.Handler, error) {
@@ -279,27 +243,24 @@ func main() {
return nil, fmt.Errorf("failed to read policy file: %w", err) return nil, fmt.Errorf("failed to read policy file: %w", err)
} }
p, err := policy.NewPolicy(bytes.NewReader(policyData), *policySnippets) p, err := policy.NewPolicy(bytes.NewReader(policyData), policySnippets...)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse policy file: %w", err) return nil, fmt.Errorf("failed to parse policy file: %w", err)
} }
settings := policy.Settings{ stateSettings := policy.StateSettings{
Cache: cache, Cache: cache,
Backends: createdBackends, Backends: createdBackends,
Debug: *debugMode, MainName: internalMainName,
MainName: internalMainName, MainVersion: internalMainVersion,
MainVersion: internalMainVersion, BasePath: *basePath,
PackageName: *packageName, PrivateKeySeed: seed,
ChallengeTemplate: *challengeTemplate, ClientIpHeader: *clientIpHeader,
ChallengeTemplateTheme: *challengeTemplateTheme, BackendIpHeader: *backendIpHeader,
PrivateKeySeed: seed, ChallengeResponseCode: http.StatusTeapot,
ClientIpHeader: *clientIpHeader,
BackendIpHeader: *backendIpHeader,
ChallengeResponseCode: http.StatusTeapot,
} }
state, err := lib.NewState(*p, settings) state, err := lib.NewState(*p, opt, stateSettings)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create state: %w", err) return nil, fmt.Errorf("failed to create state: %w", err)
@@ -310,48 +271,30 @@ func main() {
if *check { if *check {
_, err := loadPolicyState() _, err := loadPolicyState()
if err != nil { if err != nil {
slog.Error(err.Error()) fatal(err)
os.Exit(1)
} }
slog.Info("load ok") slog.Info("load ok")
os.Exit(0) os.Exit(0)
} }
listener, listenUrl := setupListener(*bindNetwork, *bind, *socketMode, *bindProxy) listener, listenUrl := opt.Bind.Listener()
slog.Warn( slog.Warn(
"listening", "listening",
"url", listenUrl, "url", listenUrl,
) )
var serverHandler atomic.Pointer[http.Handler] server, swap, err := opt.Bind.Server(createdBackends, acmeCache)
server := utils.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err != nil {
if handler := serverHandler.Load(); handler == nil { fatal(fmt.Errorf("failed to create server: %w", err))
http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
} else {
(*handler).ServeHTTP(w, r)
}
}), tlsConfig)
if *passThrough {
// setup a passthrough handler temporarily
fn := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
backend := utils.SelectHTTPHandler(createdBackends, r.Host)
if backend == nil {
http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
} else {
backend.ServeHTTP(w, r)
}
}))
serverHandler.Store(&fn)
} }
go func() { go func() {
handler, err := loadPolicyState() handler, err := loadPolicyState()
if err != nil { if err != nil {
log.Fatal(fmt.Errorf("failed to load policy state: %w", err)) fatal(fmt.Errorf("failed to load policy state: %w", err))
} }
serverHandler.Store(&handler) swap(handler)
slog.Warn( slog.Warn(
"handler configuration loaded", "handler configuration loaded",
) )
@@ -369,18 +312,59 @@ func main() {
continue continue
} }
serverHandler.Store(&handler) swap(handler)
slog.Warn("handler configuration reloaded") slog.Warn("handler configuration reloaded")
} }
}() }()
if tlsConfig != nil { if opt.BindDebug != "" {
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
debugServer := http.Server{
Addr: opt.BindDebug,
Handler: mux,
}
slog.Warn(
"listening debug",
"bind", opt.BindDebug,
)
if err = debugServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
fatal(err)
}
}()
}
if opt.BindMetrics != "" {
go func() {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
metricsServer := http.Server{
Addr: opt.BindMetrics,
Handler: mux,
}
slog.Warn(
"listening metrics",
"bind", opt.BindMetrics,
)
if err = metricsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
fatal(err)
}
}()
}
if server.TLSConfig != nil {
if err := server.ServeTLS(listener, "", ""); !errors.Is(err, http.ErrServerClosed) { if err := server.ServeTLS(listener, "", ""); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err) fatal(err)
} }
} else { } else {
if err := server.Serve(listener); !errors.Is(err, http.ErrServerClosed) { if err := server.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err) fatal(err)
} }
} }

View File

@@ -103,3 +103,17 @@ footer {
padding: 0.5em 10px; padding: 0.5em 10px;
} }
} }
body,
html {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
margin-left: auto;
margin-right: auto;
}
.centered-div {
text-align: center;
}

View File

@@ -4,6 +4,7 @@
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
<link rel="stylesheet" href="{{ .Path }}/assets/static/anubis/style.css?cacheBust={{ .Random }}"/> <link rel="stylesheet" href="{{ .Path }}/assets/static/anubis/style.css?cacheBust={{ .Random }}"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="referrer" content="origin"/>
{{ range $key, $value := .Meta }} {{ range $key, $value := .Meta }}
{{ if eq $key "refresh"}} {{ if eq $key "refresh"}}
<meta http-equiv="{{ $key }}" content="{{ $value }}"/> <meta http-equiv="{{ $key }}" content="{{ $value }}"/>
@@ -14,132 +15,6 @@
{{ range .HeaderTags }} {{ range .HeaderTags }}
{{ . }} {{ . }}
{{ end }} {{ end }}
<style>
body,
html {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
margin-left: auto;
margin-right: auto;
}
.centered-div {
text-align: center;
}
.lds-roller,
.lds-roller div,
.lds-roller div:after {
box-sizing: border-box;
}
.lds-roller {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-roller div {
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
transform-origin: 40px 40px;
}
.lds-roller div:after {
content: " ";
display: block;
position: absolute;
width: 7.2px;
height: 7.2px;
border-radius: 50%;
background: currentColor;
margin: -3.6px 0 0 -3.6px;
}
.lds-roller div:nth-child(1) {
animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
top: 62.62742px;
left: 62.62742px;
}
.lds-roller div:nth-child(2) {
animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
top: 67.71281px;
left: 56px;
}
.lds-roller div:nth-child(3) {
animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
top: 70.90963px;
left: 48.28221px;
}
.lds-roller div:nth-child(4) {
animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
top: 72px;
left: 40px;
}
.lds-roller div:nth-child(5) {
animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
top: 70.90963px;
left: 31.71779px;
}
.lds-roller div:nth-child(6) {
animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
top: 67.71281px;
left: 24px;
}
.lds-roller div:nth-child(7) {
animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
top: 62.62742px;
left: 17.37258px;
}
.lds-roller div:nth-child(8) {
animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
top: 56px;
left: 12.28719px;
}
@keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head> </head>
<body id="top"> <body id="top">
<main> <main>
@@ -154,43 +29,29 @@
src="{{ .Path }}/assets/static/logo.png?cacheBust={{ .Random }}" src="{{ .Path }}/assets/static/logo.png?cacheBust={{ .Random }}"
/> />
{{if .Challenge }} {{if .Challenge }}
<p id="status">Loading challenge <em>{{ .Challenge }}</em>...</p> <p id="status">{{ .Strings.Get "status_loading_challenge" }} <em>{{ .Challenge }}</em>...</p>
{{else if .Error}} {{else if .Error}}
<p id="status">Error: {{ .Error }}</p> <p id="status">{{ .Strings.Get "status_error" }} {{ .Error }}</p>
{{else}} {{else}}
<p id="status">Loading...</p> <p id="status">{{ .Strings.Get "status_loading" }}</p>
{{end}} {{end}}
{{if not .HideSpinner }} <details>
<div id="spinner" class="lds-roller"> <summary>{{ .Strings.Get "details_title" }}</summary>
<div></div>
<div></div> {{.Strings.Get "details_text"}}
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
{{end}}
<details style="padding-bottom: 2em;">
<summary>Why am I seeing this?</summary>
<p>You are seeing this because the administrator of this website has set up <a href="https://git.gammaspectra.live/git/go-away">go-away</a> to protect the server against the scourge of <a href="https://thelibre.news/foss-infrastructure-is-under-attack-by-ai-companies/">AI companies aggressively scraping websites</a>. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.</p>
<p>Please note that some challenges requires the use of modern JavaScript features and some plugins may will disable. Please disable such plugins for this domain (for example, JShelter).</p>
<p>If you have any issues contact the administrator and provide this Request Id: <em>{{ .Id }}</em></p>
</details> </details>
<noscript>
<p>
Sadly, you may need to enable JavaScript to get past this challenge. This is required because AI companies have changed
the social contract around how website hosting works.
</p>
</noscript>
{{if .Redirect }} {{if .Redirect }}
<a role="button" href="{{ .Redirect }}">Refresh page</a> <a style="margin-top: 2em; margin-bottom: 2em;" role="button" href="{{ .Redirect }}">{{ .Strings.Get "button_refresh_page" }}</a>
{{end}} {{end}}
<div id="testarea"></div> {{if .EndTags }}
<noscript>
{{ .Strings.Get "noscript_warning" }}
</noscript>
{{end}}
<p><small>{{ .Strings.Get "details_contact_admin_with_request_id" }}: <em>{{ .Id }}</em></small></p>
</div> </div>
@@ -198,6 +59,10 @@
<center> <center>
<p> <p>
Protected by <a href="https://git.gammaspectra.live/git/go-away">go-away</a> :: Request Id <em>{{ .Id }}</em> Protected by <a href="https://git.gammaspectra.live/git/go-away">go-away</a> :: Request Id <em>{{ .Id }}</em>
{{ range .Links }}
:: <a href="{{ .URL }}">{{ .Name }}</a>
{{ end }}
</p> </p>
</center> </center>
</footer> </footer>

View File

@@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
<meta name="referrer" content="no-referrer"> <meta name="referrer" content="origin">
{{ range $key, $value := .Meta }} {{ range $key, $value := .Meta }}
{{ if eq $key "refresh"}} {{ if eq $key "refresh"}}
@@ -61,36 +61,30 @@
</h2> </h2>
{{if .Challenge }} {{if .Challenge }}
<h3 id="status">Loading challenge <em>{{ .Challenge }}</em>...</h3> <h3 id="status">{{ .Strings.Get "status_loading_challenge" }} <em>{{ .Challenge }}</em>...</h3>
{{else if .Error}} {{else if .Error}}
<h3 id="status">Error: {{ .Error }}</h3> <h3 id="status">{{ .Strings.Get "status_error" }} {{ .Error }}</h3>
{{else}} {{else}}
<h3 id="status">Loading...</h3> <h3 id="status">{{ .Strings.Get "status_loading" }}</h3>
{{end}} {{end}}
<div id="spinner"></div>
<details style="padding-bottom: 2em;"> <details>
<summary>Why am I seeing this?</summary> <summary>{{ .Strings.Get "details_title" }}</summary>
<p>You are seeing this because the administrator of this website has set up <a href="https://git.gammaspectra.live/git/go-away">go-away</a> to protect the server against the scourge of <a href="https://thelibre.news/foss-infrastructure-is-under-attack-by-ai-companies/">AI companies aggressively scraping websites</a>. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.</p>
<p>Please note that some challenges requires the use of modern JavaScript features and some plugins may will disable. Please disable such plugins for this domain (for example, JShelter).</p> {{.Strings.Get "details_text"}}
<p>If you have any issues contact the administrator and provide the Request Id: <em>{{ .Id }}</em></p>
</details> </details>
<noscript>
<p>
Sadly, you may need to enable JavaScript to get past this challenge. This is required because AI companies have changed
the social contract around how website hosting works.
</p>
</noscript>
{{if .Redirect }} {{if .Redirect }}
<div class="button-row"> <div class="button-row" style="margin-top: 2em; margin-bottom: 2em;" >
<a role="button" class="ui small primary button" href="{{ .Redirect }}">Refresh page</a> <a role="button" class="ui small primary button" href="{{ .Redirect }}">{{ .Strings.Get "button_refresh_page" }}</a>
</div> </div>
{{end}} {{end}}
<noscript>
{{ .Strings.Get "noscript" }}
</noscript>
<div id="testarea"></div> <p><small>{{ .Strings.Get "details_contact_admin_with_request_id" }}: <em>{{ .Id }}</em></small></p>
</div> </div>
</div> </div>
</div> </div>
@@ -106,6 +100,9 @@
<footer class="page-footer" role="group" aria-label=""> <footer class="page-footer" role="group" aria-label="">
<div class="left-links" role="contentinfo" aria-label=""> <div class="left-links" role="contentinfo" aria-label="">
Protected by <a href="https://git.gammaspectra.live/git/go-away">go-away</a> :: Request Id <em>{{ .Id }}</em> Protected by <a href="https://git.gammaspectra.live/git/go-away">go-away</a> :: Request Id <em>{{ .Id }}</em>
{{ range .Links }}
:: <a href="{{ .URL }}">{{ .Name }}</a>
{{ end }}
</div> </div>
</footer> </footer>

101
examples/config.yml Normal file
View File

@@ -0,0 +1,101 @@
# Configuration file
# Parameters that exist both on config and cmdline will have cmdline as preference
bind:
#address: ":8080"
#network: "tcp"
#socket-mode": "0770"
# Enable PROXY mode on this listener, to allow passing origin info. Default false
#proxy: true
# Enable passthrough mode, which will allow traffic onto the backends while rules load. Default false
#passthrough: true
# Enable TLS on this listener and obtain certificates via an ACME directory URL, or letsencrypt
#tls-acme-autocert: "letsencrypt"
# Enable TLS on this listener and obtain certificates via a certificate and key file on disk
# Only set one of tls-acme-autocert or tls-certificate+tls-key
#tls-certificate: ""
#tls-key: ""
# Bind the Go debug port
#bind-debug: ":6060"
# Bind the Prometheus metrics onto /metrics path on this port
#bind-metrics ":9090"
# These links will be shown on the presented challenge or error pages
links:
#- name: Privacy
# url: "/privacy.html"
#- name: Contact
# url: "mailto:admin@example.com"
#- name: Donations
# url: "https://donations.example.com/abcd"
# HTML Template to use for challenge or error pages
# External templates can be included by providing a disk path
# Bundled templates:
# anubis: An Anubis-like template with no configuration parameters
# forgejo: Looks like native Forgejo. Includes logos and resources from your instance. Supports Theme.
#
#challenge-template: "anubis"
# Allows overriding specific settings set on templates. Key-Values will be passed to templates as-is
challenge-template-overrides:
# Set template theme if supported
#Theme: "forgejo-auto"
# Advanced backend configuration
# Backends setup via cmdline will be added here
backends:
# Example HTTP backend and setting client ip header
#"git.example.com":
# url: "http://forgejo:3000"
# ip-header: "X-Client-Ip"
# Example HTTPS backend with host/SNI override, HTTP/2 and no certificate verification
#"ssl.example.com":
# url: "https://127.0.0.1:8443"
# host: ssl.example.com
# http2-enabled: true
# tls-skip-verify: true
# List of strings you can replace to alter the presentation on challenge/error templates
# Can use other languages.
# Note raw HTML is allowed, be careful with it.
# Default strings exist in code, uncomment any to set it
strings:
#title_challenge: "Checking you are not a bot"
#title_error: "Oh no!"
#noscript_warning: "<p>Sadly, you may need to enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works.</p>"
#details_title: "Why am I seeing this?"
#details_text: >
# <p>
# You are seeing this because the administrator of this website has set up <a href="https://git.gammaspectra.live/git/go-away">go-away</a>
# to protect the server against the scourge of <a href="https://thelibre.news/foss-infrastructure-is-under-attack-by-ai-companies/">AI companies aggressively scraping websites</a>.
# </p>
# <p>
# Mass scraping can and does cause downtime for the websites, which makes their resources inaccessible for everyone.
# </p>
# <p>
# Please note that some challenges requires the use of modern JavaScript features and some plugins may disable these.
# Disable such plugins for this domain (for example, JShelter) if you encounter any issues.
# </p>
#details_contact_admin_with_request_id: "If you have any issues contact the site administrator and provide the following Request Id"
#button_refresh_page: "Refresh page"
#status_loading_challenge: "Loading challenge"
#status_starting_challenge: "Starting challenge"
#status_loading: "Loading..."
#status_calculating: "Calculating..."
#status_challenge_success: "Challenge success!"
#status_challenge_done_took: "Done! Took"
#status_error: "Error:"

View File

@@ -92,6 +92,16 @@ rules:
- '($is-static-asset)' - '($is-static-asset)'
action: pass action: pass
- name: desired-crawlers
conditions:
- *is-bot-googlebot
- *is-bot-bingbot
- *is-bot-duckduckbot
- *is-bot-kagibot
- *is-bot-qwantbot
- *is-bot-yandexbot
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")'
@@ -106,7 +116,7 @@ rules:
- 'userAgent.matches("^Opera/[0-9.]+\\.\\(")' - 'userAgent.matches("^Opera/[0-9.]+\\.\\(")'
# AI bullshit stuff, they do not respect robots.txt even while they read it # AI bullshit stuff, they do not respect robots.txt even while they read it
# TikTok Bytedance AI training # TikTok Bytedance AI training
- 'userAgent.contains("Bytedance") || userAgent.contains("Bytespider")' - 'userAgent.contains("Bytedance") || userAgent.contains("Bytespider") || userAgent.contains("TikTokSpider")'
# Meta AI training; The Meta-ExternalAgent crawler crawls the web for use cases such as training AI models or improving products by indexing content directly. # Meta AI training; The Meta-ExternalAgent crawler crawls the web for use cases such as training AI models or improving products by indexing content directly.
- 'userAgent.contains("meta-externalagent/") || userAgent.contains("meta-externalfetcher/") || userAgent.contains("FacebookBot")' - 'userAgent.contains("meta-externalagent/") || userAgent.contains("meta-externalfetcher/") || userAgent.contains("FacebookBot")'
# Anthropic AI training and usage # Anthropic AI training and usage
@@ -196,6 +206,7 @@ rules:
# OCI packages API and package managers # OCI packages API and package managers
- 'path.startsWith("/api/packages/") || path == "/api/packages"' - 'path.startsWith("/api/packages/") || path == "/api/packages"'
- 'path.startsWith("/v2/") || path == "/v2"' - 'path.startsWith("/v2/") || path == "/v2"'
- 'path.endsWith("/branches/list") || path.endsWith("/tags/list")'
action: pass action: pass
- name: preview-fetchers - name: preview-fetchers
@@ -220,16 +231,6 @@ rules:
- '(path.matches("^/[^/]+/[^/]+/?$") || path.matches("^/[^/]+/[^/]+/badges/") || path.matches("^/[^/]+/[^/]+/(issues|pulls)/[0-9]+$") || (path.matches("^/[^/]+/?$") && size(query) == 0)) && !path.matches("(?i)^/(api|metrics|v2|assets|attachments|avatar|avatars|repo-avatars|captcha|login|org|repo|user|admin|devtest|explore|issues|pulls|milestones|notifications|ghost)(/|$)")' - '(path.matches("^/[^/]+/[^/]+/?$") || path.matches("^/[^/]+/[^/]+/badges/") || path.matches("^/[^/]+/[^/]+/(issues|pulls)/[0-9]+$") || (path.matches("^/[^/]+/?$") && size(query) == 0)) && !path.matches("(?i)^/(api|metrics|v2|assets|attachments|avatar|avatars|repo-avatars|captcha|login|org|repo|user|admin|devtest|explore|issues|pulls|milestones|notifications|ghost)(/|$)")'
action: pass action: pass
- name: desired-crawlers
conditions:
- *is-bot-googlebot
- *is-bot-bingbot
- *is-bot-duckduckbot
- *is-bot-kagibot
- *is-bot-qwantbot
- *is-bot-yandexbot
action: pass
# check a sequence of challenges # check a sequence of challenges
- name: heavy-operations - name: heavy-operations
conditions: ['($is-heavy-resource)'] conditions: ['($is-heavy-resource)']

View File

@@ -50,6 +50,16 @@ rules:
- '($is-static-asset)' - '($is-static-asset)'
action: pass action: pass
- name: desired-crawlers
conditions:
- *is-bot-googlebot
- *is-bot-bingbot
- *is-bot-duckduckbot
- *is-bot-kagibot
- *is-bot-qwantbot
- *is-bot-yandexbot
action: pass
- name: undesired-crawlers - name: undesired-crawlers
conditions: conditions:
- '($is-headless-chromium)' - '($is-headless-chromium)'
@@ -59,7 +69,7 @@ rules:
- 'userAgent.matches("^Opera/[0-9.]+\\.\\(")' - 'userAgent.matches("^Opera/[0-9.]+\\.\\(")'
# AI bullshit stuff, they do not respect robots.txt even while they read it # AI bullshit stuff, they do not respect robots.txt even while they read it
# TikTok Bytedance AI training # TikTok Bytedance AI training
- 'userAgent.contains("Bytedance") || userAgent.contains("Bytespider")' - 'userAgent.contains("Bytedance") || userAgent.contains("Bytespider") || userAgent.contains("TikTokSpider")'
# Meta AI training; The Meta-ExternalAgent crawler crawls the web for use cases such as training AI models or improving products by indexing content directly. # Meta AI training; The Meta-ExternalAgent crawler crawls the web for use cases such as training AI models or improving products by indexing content directly.
- 'userAgent.contains("meta-externalagent/") || userAgent.contains("meta-externalfetcher/") || userAgent.contains("FacebookBot")' - 'userAgent.contains("meta-externalagent/") || userAgent.contains("meta-externalfetcher/") || userAgent.contains("FacebookBot")'
# Anthropic AI training and usage # Anthropic AI training and usage
@@ -98,16 +108,6 @@ rules:
settings: settings:
challenges: [header-refresh] challenges: [header-refresh]
- name: desired-crawlers
conditions:
- *is-bot-googlebot
- *is-bot-bingbot
- *is-bot-duckduckbot
- *is-bot-kagibot
- *is-bot-qwantbot
- *is-bot-yandexbot
action: pass
- name: homesite - name: homesite
conditions: conditions:
- 'path == "/"' - 'path == "/"'

View File

@@ -0,0 +1,8 @@
networks:
uptimerobot:
- url: https://uptimerobot.com/inc/files/ips/IPv4andIPv6.txt
regex: "(?P<prefix>[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+(/[0-9]+)?|[0-9a-f:]+:.+)"
conditions:
is-bot-uptimerobot:
- &is-bot-uptimerobot 'userAgent.contains("http://www.uptimerobot.com/") && remoteAddress.network("uptimerobot")'

9
go.mod
View File

@@ -5,6 +5,7 @@ go 1.24.0
toolchain go1.24.2 toolchain go1.24.2
require ( require (
codeberg.org/gone/http-cel v1.0.0
codeberg.org/meta/gzipped/v2 v2.0.0-20231111234332-aa70c3194756 codeberg.org/meta/gzipped/v2 v2.0.0-20231111234332-aa70c3194756
github.com/alphadose/haxmap v1.4.1 github.com/alphadose/haxmap v1.4.1
github.com/go-jose/go-jose/v4 v4.1.0 github.com/go-jose/go-jose/v4 v4.1.0
@@ -12,6 +13,7 @@ require (
github.com/google/cel-go v0.25.0 github.com/google/cel-go v0.25.0
github.com/itchyny/gojq v0.12.17 github.com/itchyny/gojq v0.12.17
github.com/pires/go-proxyproto v0.8.0 github.com/pires/go-proxyproto v0.8.0
github.com/prometheus/client_golang v1.22.0
github.com/tetratelabs/wazero v1.9.0 github.com/tetratelabs/wazero v1.9.0
github.com/yl2chen/cidranger v1.0.2 github.com/yl2chen/cidranger v1.0.2
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.37.0
@@ -20,11 +22,18 @@ require (
require ( require (
cel.dev/expr v0.23.1 // indirect cel.dev/expr v0.23.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/itchyny/timefmt-go v0.1.6 // indirect
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 // indirect github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/net v0.39.0 // indirect golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/text v0.24.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect

28
go.sum
View File

@@ -1,11 +1,17 @@
cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
codeberg.org/gone/http-cel v1.0.0 h1:flEv/KzEye4W7vjwkdAkwo7VCbuj9xZLjyTn/rjWFDQ=
codeberg.org/gone/http-cel v1.0.0/go.mod h1:uRkxygsQp5EFE3e9dRkJ4HK453G5YZDHCq9DEG5CoDw=
codeberg.org/meta/gzipped/v2 v2.0.0-20231111234332-aa70c3194756 h1:bDqEUEYt4UJy8mfLCZeJuXx+xNJvdqTbkE4Ci11NQYU= codeberg.org/meta/gzipped/v2 v2.0.0-20231111234332-aa70c3194756 h1:bDqEUEYt4UJy8mfLCZeJuXx+xNJvdqTbkE4Ci11NQYU=
codeberg.org/meta/gzipped/v2 v2.0.0-20231111234332-aa70c3194756/go.mod h1:aJ/ghJW7viYfwZ6OizDst+uJgbb6r/Hvoqhmi1OPTTw= codeberg.org/meta/gzipped/v2 v2.0.0-20231111234332-aa70c3194756/go.mod h1:aJ/ghJW7viYfwZ6OizDst+uJgbb6r/Hvoqhmi1OPTTw=
github.com/alphadose/haxmap v1.4.1 h1:VtD6VCxUkjNIfJk/aWdYFfOzrRddDFjmvmRmILg7x8Q= github.com/alphadose/haxmap v1.4.1 h1:VtD6VCxUkjNIfJk/aWdYFfOzrRddDFjmvmRmILg7x8Q=
github.com/alphadose/haxmap v1.4.1/go.mod h1:rjHw1IAqbxm0S3U5tD16GoKsiAd8FWx5BJ2IYqXwgmM= github.com/alphadose/haxmap v1.4.1/go.mod h1:rjHw1IAqbxm0S3U5tD16GoKsiAd8FWx5BJ2IYqXwgmM=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -13,8 +19,6 @@ github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -25,10 +29,24 @@ github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/my
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 h1:Pdirg1gwhEcGjMLyuSxGn9664p+P8J9SrfMgpFwrDyg= github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 h1:Pdirg1gwhEcGjMLyuSxGn9664p+P8J9SrfMgpFwrDyg=
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43/go.mod h1:ahLMuLCUyDdXqtqGyuwGev7/PGtO7r7ocvdwDuEN/3E= github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43/go.mod h1:ahLMuLCUyDdXqtqGyuwGev7/PGtO7r7ocvdwDuEN/3E=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0= github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0=
github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -50,14 +68,12 @@ golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5Z
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY=
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f h1:tjZsroqekhC63+WMqzmWyW5Twj/ZfR5HAlpd5YQ1Vs0= google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f h1:tjZsroqekhC63+WMqzmWyW5Twj/ZfR5HAlpd5YQ1Vs0=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ= google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=

View File

@@ -43,7 +43,8 @@ func init() {
return nil, fmt.Errorf("no registered challenges found in rule %s", ruleName) return nil, fmt.Errorf("no registered challenges found in rule %s", ruleName)
} }
passHandler, ok := Register[policy.RuleAction(strings.ToUpper(params.PassAction))] passAction := policy.RuleAction(strings.ToUpper(params.PassAction))
passHandler, ok := Register[passAction]
if !ok { if !ok {
return nil, fmt.Errorf("unknown pass action %s", params.PassAction) return nil, fmt.Errorf("unknown pass action %s", params.PassAction)
} }
@@ -53,7 +54,8 @@ func init() {
return nil, err return nil, err
} }
failHandler, ok := Register[policy.RuleAction(strings.ToUpper(params.FailAction))] failAction := policy.RuleAction(strings.ToUpper(params.FailAction))
failHandler, ok := Register[failAction]
if !ok { if !ok {
return nil, fmt.Errorf("unknown pass action %s", params.FailAction) return nil, fmt.Errorf("unknown pass action %s", params.FailAction)
} }
@@ -69,8 +71,10 @@ func init() {
Continue: cont, Continue: cont,
Challenges: regs, Challenges: regs,
PassAction: passActionHandler, PassAction: passAction,
FailAction: failActionHandler, PassActionHandler: passActionHandler,
FailAction: failAction,
FailActionHandler: failActionHandler,
}, nil }, nil
} }
Register[policy.RuleActionCHALLENGE] = func(state challenge.StateInterface, ruleName, ruleHash string, settings ast.Node) (Handler, error) { Register[policy.RuleActionCHALLENGE] = func(state challenge.StateInterface, ruleName, ruleHash string, settings ast.Node) (Handler, error) {
@@ -104,20 +108,26 @@ type Challenge struct {
Continue bool Continue bool
Challenges []*challenge.Registration Challenges []*challenge.Registration
PassAction Handler PassAction policy.RuleAction
FailAction Handler PassActionHandler Handler
FailAction policy.RuleAction
FailActionHandler Handler
} }
func (a Challenge) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Request, done func() (backend http.Handler)) (next bool, err error) { func (a Challenge) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Request, done func() (backend http.Handler)) (next bool, err error) {
data := challenge.RequestDataFromContext(r.Context()) data := challenge.RequestDataFromContext(r.Context())
for _, reg := range a.Challenges { for _, reg := range a.Challenges {
if data.HasValidChallenge(reg.Id()) { if data.HasValidChallenge(reg.Id()) {
data.State.ChallengeChecked(r, reg, r.URL.String(), logger)
if a.Continue { if a.Continue {
return true, nil return true, nil
} }
// we passed! // we passed!
return a.PassAction.Handle(logger.With("challenge", reg.Name), w, r, done) data.State.ActionHit(r, a.PassAction, logger)
return a.PassActionHandler.Handle(logger.With("challenge", reg.Name), w, r, done)
} }
} }
// none matched, issue challenges in sequential priority // none matched, issue challenges in sequential priority
@@ -143,7 +153,8 @@ func (a Challenge) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Re
return true, nil return true, nil
} }
return a.PassAction.Handle(logger.With("challenge", reg.Name), w, r, done) data.State.ActionHit(r, a.PassAction, logger)
return a.PassActionHandler.Handle(logger.With("challenge", reg.Name), w, r, done)
case challenge.VerifyResultNotOK: case challenge.VerifyResultNotOK:
// we have had the challenge checked, but it's not ok! // we have had the challenge checked, but it's not ok!
// safe to continue // safe to continue
@@ -157,7 +168,8 @@ func (a Challenge) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Re
continue continue
} }
return a.FailAction.Handle(logger, w, r, done) data.State.ActionHit(r, a.FailAction, logger)
return a.FailActionHandler.Handle(logger, w, r, done)
case challenge.VerifyResultNone: case challenge.VerifyResultNone:
// challenge was issued // challenge was issued
if reg.Class == challenge.ClassTransparent { if reg.Class == challenge.ClassTransparent {
@@ -174,5 +186,6 @@ func (a Challenge) Handle(logger *slog.Logger, w http.ResponseWriter, r *http.Re
} }
// nothing matched, execute default action // nothing matched, execute default action
return a.FailAction.Handle(logger, w, r, done) data.State.ActionHit(r, a.FailAction, logger)
return a.FailActionHandler.Handle(logger, w, r, done)
} }

View File

@@ -23,7 +23,7 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
return challenge.VerifyResultFail return challenge.VerifyResultFail
} }
utils.SetCookie(utils.CookiePrefix+reg.Name, token, expiry, w, r) utils.SetCookie(challenge.RequestDataFromContext(r.Context()).CookiePrefix+reg.Name, token, expiry, w, r)
uri, err := challenge.RedirectUrl(r, reg) uri, err := challenge.RedirectUrl(r, reg)
if err != nil { if err != nil {

View File

@@ -1,19 +1,21 @@
package challenge package challenge
import ( import (
http_cel "codeberg.org/gone/http-cel"
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"git.gammaspectra.live/git/go-away/lib/condition"
"git.gammaspectra.live/git/go-away/utils" "git.gammaspectra.live/git/go-away/utils"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/traits" "github.com/google/cel-go/common/types/traits"
"net"
"net/http" "net/http"
"net/netip"
"net/textproto" "net/textproto"
"strings"
"time" "time"
) )
@@ -35,8 +37,9 @@ type RequestData struct {
Time time.Time Time time.Time
ChallengeVerify map[Id]VerifyResult ChallengeVerify map[Id]VerifyResult
ChallengeState map[Id]VerifyState ChallengeState map[Id]VerifyState
RemoteAddress net.IP RemoteAddress netip.AddrPort
State StateInterface State StateInterface
CookiePrefix string
r *http.Request r *http.Request
@@ -55,7 +58,6 @@ func CreateRequestData(r *http.Request, state StateInterface) (*http.Request, *R
data.ChallengeState = make(map[Id]VerifyState, len(state.GetChallenges())) data.ChallengeState = make(map[Id]VerifyState, len(state.GetChallenges()))
data.Time = time.Now().UTC() data.Time = time.Now().UTC()
data.State = state data.State = state
data.r = r
data.fp = make(map[string]string, 2) data.fp = make(map[string]string, 2)
@@ -72,10 +74,27 @@ func CreateRequestData(r *http.Request, state StateInterface) (*http.Request, *R
} }
} }
data.query = condition.NewValuesMap(r.URL.Query()) q := r.URL.Query()
data.header = condition.NewMIMEMap(textproto.MIMEHeader(r.Header)) // delete query parameters that were set by go-away
for k := range q {
if strings.HasPrefix(k, QueryArgPrefix) {
q.Del(k)
}
}
data.query = http_cel.NewValuesMap(q)
data.header = http_cel.NewMIMEMap(textproto.MIMEHeader(r.Header))
sum := sha256.New()
sum.Write([]byte(r.Host))
sum.Write([]byte{0})
sum.Write(state.PublicKey())
sum.Write([]byte{0})
data.CookiePrefix = utils.CookiePrefix + hex.EncodeToString(sum.Sum(nil)[:4]) + "-"
r = r.WithContext(context.WithValue(r.Context(), requestDataContextKey{}, &data)) r = r.WithContext(context.WithValue(r.Context(), requestDataContextKey{}, &data))
r = utils.SetRemoteAddress(r, data.RemoteAddress)
data.r = r
return r, &data return r, &data
} }
@@ -87,7 +106,7 @@ func (d *RequestData) ResolveName(name string) (any, bool) {
case "method": case "method":
return d.r.Method, true return d.r.Method, true
case "remoteAddress": case "remoteAddress":
return d.RemoteAddress, true return d.RemoteAddress.Addr().AsSlice(), true
case "userAgent": case "userAgent":
return d.r.UserAgent(), true return d.r.UserAgent(), true
case "path": case "path":
@@ -108,12 +127,17 @@ func (d *RequestData) Parent() cel.Activation {
} }
func (d *RequestData) EvaluateChallenges(w http.ResponseWriter, r *http.Request) { func (d *RequestData) EvaluateChallenges(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
var issuedChallenge string
if q.Has(QueryArgChallenge) {
issuedChallenge = q.Get(QueryArgChallenge)
}
for _, reg := range d.State.GetChallenges() { for _, reg := range d.State.GetChallenges() {
key := GetChallengeKeyForRequest(d.State, reg, d.Expiration(reg.Duration), r) key := GetChallengeKeyForRequest(d.State, reg, d.Expiration(reg.Duration), r)
verifyResult, verifyState, err := reg.VerifyChallengeToken(d.State.PublicKey(), key, r) verifyResult, verifyState, err := reg.VerifyChallengeToken(d.State.PublicKey(), key, r)
if err != nil && !errors.Is(err, http.ErrNoCookie) { if err != nil && !errors.Is(err, http.ErrNoCookie) {
// clear invalid cookie // clear invalid cookie
utils.ClearCookie(utils.CookiePrefix+reg.Name, w, r) utils.ClearCookie(d.CookiePrefix+reg.Name, w, r)
} }
// prevent evaluating the challenge if not solved // prevent evaluating the challenge if not solved
@@ -130,6 +154,11 @@ func (d *RequestData) EvaluateChallenges(w http.ResponseWriter, r *http.Request)
} }
} }
} }
if !verifyResult.Ok() && issuedChallenge == reg.Name {
// we issued the challenge, must skip to prevent loops
verifyResult = VerifyResultSkip
}
d.ChallengeVerify[reg.Id()] = verifyResult d.ChallengeVerify[reg.Id()] = verifyResult
d.ChallengeState[reg.Id()] = verifyState d.ChallengeState[reg.Id()] = verifyState
} }

View File

@@ -119,13 +119,9 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
data := challenge.RequestDataFromContext(r.Context()) data := challenge.RequestDataFromContext(r.Context())
result, err := lookup(r.Context(), params.Decay, params.Timeout, dnsbl, decayMap, data.RemoteAddress) result, err := lookup(r.Context(), params.Decay, params.Timeout, dnsbl, decayMap, data.RemoteAddress.Addr().Unmap().AsSlice())
if err != nil { if err != nil {
data.State.Logger(r).Debug("dnsbl lookup failed", "address", data.RemoteAddress.String(), "result", result, "err", err) data.State.Logger(r).Debug("dnsbl lookup failed", "address", data.RemoteAddress.Addr().String(), "result", result, "err", err)
}
if err != nil {
return challenge.VerifyResultFail
} }
if result.Bad() { if result.Bad() {
@@ -133,14 +129,14 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
if err != nil { if err != nil {
return challenge.VerifyResultFail return challenge.VerifyResultFail
} }
utils.SetCookie(utils.CookiePrefix+reg.Name, token, expiry, w, r) utils.SetCookie(data.CookiePrefix+reg.Name, token, expiry, w, r)
return challenge.VerifyResultNotOK return challenge.VerifyResultNotOK
} else { } else {
token, err := reg.IssueChallengeToken(state.PrivateKey(), key, nil, expiry, true) token, err := reg.IssueChallengeToken(state.PrivateKey(), key, nil, expiry, true)
if err != nil { if err != nil {
return challenge.VerifyResultFail return challenge.VerifyResultFail
} }
utils.SetCookie(utils.CookiePrefix+reg.Name, token, expiry, w, r) utils.SetCookie(data.CookiePrefix+reg.Name, token, expiry, w, r)
return challenge.VerifyResultOK return challenge.VerifyResultOK
} }
} }

View File

@@ -39,11 +39,13 @@ const VerifyChallengeUrlSuffix = "/verify-challenge"
func GetVerifyInformation(r *http.Request, reg *Registration) (requestId RequestId, redirect, token string, err error) { func GetVerifyInformation(r *http.Request, reg *Registration) (requestId RequestId, redirect, token string, err error) {
if r.FormValue(QueryArgChallenge) != reg.Name { q := r.URL.Query()
return RequestId{}, "", "", fmt.Errorf("unexpected challenge: got %s", r.FormValue(QueryArgChallenge))
if q.Get(QueryArgChallenge) != reg.Name {
return RequestId{}, "", "", fmt.Errorf("unexpected challenge: got \"%s\"", q.Get(QueryArgChallenge))
} }
requestIdHex := r.FormValue(QueryArgRequestId) requestIdHex := q.Get(QueryArgRequestId)
if len(requestId) != hex.DecodedLen(len(requestIdHex)) { if len(requestId) != hex.DecodedLen(len(requestIdHex)) {
return RequestId{}, "", "", errors.New("invalid request id") return RequestId{}, "", "", errors.New("invalid request id")
@@ -55,8 +57,8 @@ func GetVerifyInformation(r *http.Request, reg *Registration) (requestId Request
return RequestId{}, "", "", errors.New("invalid request id") return RequestId{}, "", "", errors.New("invalid request id")
} }
token = r.FormValue(QueryArgToken) token = q.Get(QueryArgToken)
redirect, err = utils.EnsureNoOpenRedirect(r.FormValue(QueryArgRedirect)) redirect, err = utils.EnsureNoOpenRedirect(q.Get(QueryArgRedirect))
if err != nil { if err != nil {
return RequestId{}, "", "", err return RequestId{}, "", "", err
} }
@@ -136,7 +138,7 @@ func VerifyHandlerFunc(state StateInterface, reg *Registration, verify VerifyFun
if err != nil { if err != nil {
return err return err
} else if !verifyResult.Ok() { } else if !verifyResult.Ok() {
utils.ClearCookie(utils.CookiePrefix+reg.Name, w, r) utils.ClearCookie(data.CookiePrefix+reg.Name, w, r)
state.ChallengeFailed(r, reg, nil, redirect, nil) state.ChallengeFailed(r, reg, nil, redirect, nil)
responseFunc(state, data, w, r, verifyResult, nil, redirect) responseFunc(state, data, w, r, verifyResult, nil, redirect)
return nil return nil
@@ -144,9 +146,9 @@ func VerifyHandlerFunc(state StateInterface, reg *Registration, verify VerifyFun
challengeToken, err := reg.IssueChallengeToken(state.PrivateKey(), key, []byte(token), expiration, true) challengeToken, err := reg.IssueChallengeToken(state.PrivateKey(), key, []byte(token), expiration, true)
if err != nil { if err != nil {
utils.ClearCookie(utils.CookiePrefix+reg.Name, w, r) utils.ClearCookie(data.CookiePrefix+reg.Name, w, r)
} else { } else {
utils.SetCookie(utils.CookiePrefix+reg.Name, challengeToken, expiration, w, r) utils.SetCookie(data.CookiePrefix+reg.Name, challengeToken, expiration, w, r)
} }
data.ChallengeVerify[reg.id] = verifyResult data.ChallengeVerify[reg.id] = verifyResult
state.ChallengePassed(r, reg, redirect, nil) state.ChallengePassed(r, reg, redirect, nil)
@@ -155,7 +157,7 @@ func VerifyHandlerFunc(state StateInterface, reg *Registration, verify VerifyFun
return nil return nil
}() }()
if err != nil { if err != nil {
utils.ClearCookie(utils.CookiePrefix+reg.Name, w, r) utils.ClearCookie(data.CookiePrefix+reg.Name, w, r)
state.ChallengeFailed(r, reg, err, redirect, nil) state.ChallengeFailed(r, reg, err, redirect, nil)
responseFunc(state, data, w, r, VerifyResultFail, fmt.Errorf("access denied: error in challenge %s: %w", reg.Name, err), redirect) responseFunc(state, data, w, r, VerifyResultFail, fmt.Errorf("access denied: error in challenge %s: %w", reg.Name, err), redirect)
return return

View File

@@ -137,19 +137,21 @@ 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 {
token, err := reg.IssueChallengeToken(state.PrivateKey(), key, sum, expiry, false) token, err := reg.IssueChallengeToken(state.PrivateKey(), key, sum, expiry, false)
if err != nil { if err != nil {
return challenge.VerifyResultFail return challenge.VerifyResultFail
} }
utils.SetCookie(utils.CookiePrefix+reg.Name, token, expiry, w, r) utils.SetCookie(data.CookiePrefix+reg.Name, token, expiry, w, r)
return challenge.VerifyResultNotOK return challenge.VerifyResultNotOK
} else { } else {
token, err := reg.IssueChallengeToken(state.PrivateKey(), key, sum, expiry, true) token, err := reg.IssueChallengeToken(state.PrivateKey(), key, sum, expiry, true)
if err != nil { if err != nil {
return challenge.VerifyResultFail return challenge.VerifyResultFail
} }
utils.SetCookie(utils.CookiePrefix+reg.Name, token, expiry, w, r) utils.SetCookie(data.CookiePrefix+reg.Name, token, expiry, w, r)
return challenge.VerifyResultOK return challenge.VerifyResultOK
} }
} }

View File

@@ -47,7 +47,8 @@ func GetChallengeKeyForRequest(state StateInterface, reg *Registration, until ti
hasher.Write([]byte("challenge\x00")) hasher.Write([]byte("challenge\x00"))
hasher.Write([]byte(reg.Name)) hasher.Write([]byte(reg.Name))
hasher.Write([]byte{0}) hasher.Write([]byte{0})
hasher.Write(address.To16()) ipBuf := address.Addr().Unmap().As16()
hasher.Write(ipBuf[:])
hasher.Write([]byte{0}) hasher.Write([]byte{0})
// specific headers // specific headers
@@ -72,7 +73,7 @@ func GetChallengeKeyForRequest(state StateInterface, reg *Registration, until ti
sum[0] = 0 sum[0] = 0
if address.To4() != nil { if address.Addr().Unmap().Is4() {
// Is IPv4, mark // Is IPv4, mark
sum.Set(KeyFlagIsIPv4) sum.Set(KeyFlagIsIPv4)
} }

View File

@@ -2,12 +2,11 @@ package challenge
import ( import (
"bytes" "bytes"
http_cel "codeberg.org/gone/http-cel"
"crypto/ed25519" "crypto/ed25519"
"errors" "errors"
"fmt" "fmt"
"git.gammaspectra.live/git/go-away/lib/condition"
"git.gammaspectra.live/git/go-away/lib/policy" "git.gammaspectra.live/git/go-away/lib/policy"
"git.gammaspectra.live/git/go-away/utils"
"github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt" "github.com/go-jose/go-jose/v4/jwt"
"github.com/goccy/go-yaml/ast" "github.com/goccy/go-yaml/ast"
@@ -68,11 +67,11 @@ func (r Register) Create(state StateInterface, name string, pol policy.Challenge
} }
if len(conditions) > 0 { if len(conditions) > 0 {
ast, err := condition.FromStrings(state.ProgramEnv(), condition.OperatorOr, conditions...) ast, err := http_cel.NewAst(state.ProgramEnv(), http_cel.OperatorOr, conditions...)
if err != nil { if err != nil {
return nil, 0, fmt.Errorf("error compiling conditions: %v", err) return nil, 0, fmt.Errorf("error compiling conditions: %v", err)
} }
reg.Condition, err = condition.Program(state.ProgramEnv(), ast) reg.Condition, err = http_cel.ProgramAst(state.ProgramEnv(), ast)
if err != nil { if err != nil {
return nil, 0, fmt.Errorf("error compiling program: %v", err) return nil, 0, fmt.Errorf("error compiling program: %v", err)
} }
@@ -193,7 +192,7 @@ var ErrVerifyVerifyMismatch = errors.New("verify: verification mismatch")
var ErrTokenExpired = errors.New("token: expired") var ErrTokenExpired = errors.New("token: expired")
func (reg Registration) VerifyChallengeToken(publicKey ed25519.PublicKey, expectedKey Key, r *http.Request) (VerifyResult, VerifyState, error) { func (reg Registration) VerifyChallengeToken(publicKey ed25519.PublicKey, expectedKey Key, r *http.Request) (VerifyResult, VerifyState, error) {
cookie, err := r.Cookie(utils.CookiePrefix + reg.Name) cookie, err := r.Cookie(RequestDataFromContext(r.Context()).CookiePrefix + reg.Name)
if err != nil { if err != nil {
return VerifyResultNone, VerifyStateNone, err return VerifyResultNone, VerifyStateNone, err
} }

View File

@@ -33,6 +33,7 @@ func ServeChallengeScript(w http.ResponseWriter, r *http.Request, reg *Registrat
"Random": utils.CacheBust(), "Random": utils.CacheBust(),
"Challenge": reg.Name, "Challenge": reg.Name,
"ChallengeScript": script, "ChallengeScript": script,
"Strings": data.State.Options().Strings,
}) })
if err != nil { if err != nil {
//TODO: log //TODO: log

View File

@@ -14,9 +14,8 @@ const u = (url = "", params = {}) => {
(async () => { (async () => {
const status = document.getElementById('status'); const status = document.getElementById('status');
const title = document.getElementById('title'); const title = document.getElementById('title');
const spinner = document.getElementById('spinner');
status.innerText = 'Starting challenge {{ .Challenge }}...'; status.innerText = '{{ .Strings.Get "status_starting_challenge" }} {{ .Challenge }}...';
try { try {
const info = await setup({ const info = await setup({
@@ -25,15 +24,13 @@ const u = (url = "", params = {}) => {
}); });
if (info != "") { if (info != "") {
status.innerText = 'Calculating... ' + info status.innerText = '{{ .Strings.Get "status_calculating" }} ' + info
} else { } else {
status.innerText = 'Calculating...'; status.innerText = '{{ .Strings.Get "status_calculating" }}';
} }
} catch (err) { } catch (err) {
title.innerHTML = "Oh no!"; title.innerHTML = '{{ .Strings.Get "title_error" }}';
status.innerHTML = `Failed to initialize: ${err.message}`; status.innerHTML = `{{ .Strings.Get "status_error" }} ${err.message}`;
spinner.innerHTML = "";
spinner.style.display = "none";
return return
} }
@@ -44,11 +41,11 @@ const u = (url = "", params = {}) => {
const t1 = Date.now(); const t1 = Date.now();
console.log({ result, info }); console.log({ result, info });
title.innerHTML = "Challenge success!"; title.innerHTML = '{{ .Strings.Get "status_challenge_success" }}';
if (info != "") { if (info != "") {
status.innerHTML = `Done! Took ${t1 - t0}ms, ${info}`; status.innerHTML = `{{ .Strings.Get "status_challenge_done_took" }} ${t1 - t0}ms, ${info}`;
} else { } else {
status.innerHTML = `Done! Took ${t1 - t0}ms`; status.innerHTML = `{{ .Strings.Get "status_challenge_done_took" }} ${t1 - t0}ms`;
} }
setTimeout(() => { setTimeout(() => {
@@ -62,9 +59,7 @@ const u = (url = "", params = {}) => {
}); });
}, 500); }, 500);
} catch (err) { } catch (err) {
title.innerHTML = "Oh no!"; title.innerHTML = '{{ .Strings.Get "title_error" }}';
status.innerHTML = `Failed to challenge: ${err.message}`; status.innerHTML = `{{ .Strings.Get "status_error" }} ${err.message}`;
spinner.innerHTML = "";
spinner.style.display = "none";
} }
})(); })();

View File

@@ -3,6 +3,7 @@ package challenge
import ( import (
"crypto/ed25519" "crypto/ed25519"
"git.gammaspectra.live/git/go-away/lib/policy" "git.gammaspectra.live/git/go-away/lib/policy"
"git.gammaspectra.live/git/go-away/lib/settings"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -96,6 +97,11 @@ type StateInterface interface {
ChallengeFailed(r *http.Request, reg *Registration, err error, redirect string, logger *slog.Logger) ChallengeFailed(r *http.Request, reg *Registration, err error, redirect string, logger *slog.Logger)
ChallengePassed(r *http.Request, reg *Registration, redirect string, logger *slog.Logger) ChallengePassed(r *http.Request, reg *Registration, redirect string, logger *slog.Logger)
ChallengeIssued(r *http.Request, reg *Registration, redirect string, logger *slog.Logger) ChallengeIssued(r *http.Request, reg *Registration, redirect string, logger *slog.Logger)
ChallengeChecked(r *http.Request, reg *Registration, redirect string, logger *slog.Logger)
RuleHit(r *http.Request, name string, logger *slog.Logger)
RuleMiss(r *http.Request, name string, logger *slog.Logger)
ActionHit(r *http.Request, name policy.RuleAction, logger *slog.Logger)
Logger(r *http.Request) *slog.Logger Logger(r *http.Request) *slog.Logger
@@ -106,7 +112,9 @@ type StateInterface interface {
GetChallengeByName(name string) (*Registration, bool) GetChallengeByName(name string) (*Registration, bool)
GetChallenges() Register GetChallenges() Register
Settings() policy.Settings Settings() policy.StateSettings
Options() settings.Settings
GetBackend(host string) http.Handler GetBackend(host string) http.Handler
} }

View File

@@ -1,177 +0,0 @@
package condition
import (
"fmt"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/ext"
"github.com/yl2chen/cidranger"
"log/slog"
"net"
"strings"
)
type Condition struct {
Expression *cel.Ast
}
const (
OperatorOr = "||"
OperatorAnd = "&&"
)
func NewRulesEnvironment(networks map[string]cidranger.Ranger) (*cel.Env, error) {
return cel.NewEnv(
ext.Strings(
ext.StringsLocale("en_US"),
ext.StringsValidateFormatCalls(true),
),
cel.DefaultUTCTimeZone(true),
//TODO: custom type for remoteAddress
cel.Variable("remoteAddress", cel.BytesType),
cel.Variable("host", cel.StringType),
cel.Variable("method", cel.StringType),
cel.Variable("userAgent", cel.StringType),
cel.Variable("path", cel.StringType),
cel.Variable("query", cel.MapType(cel.StringType, cel.StringType)),
cel.Variable("fp", cel.MapType(cel.StringType, cel.StringType)),
// http.Header
cel.Variable("headers", cel.MapType(cel.StringType, cel.StringType)),
//TODO: dynamic type?
cel.Function("inDNSBL",
cel.Overload("inDNSBL_ip",
[]*cel.Type{cel.AnyType},
cel.BoolType,
cel.UnaryBinding(func(val ref.Val) ref.Val {
slog.Error("inDNSBL function has been deprecated, replace with dnsbl challenge")
return types.Bool(false)
}),
),
),
cel.Function("network",
cel.MemberOverload("netIP_network_string",
[]*cel.Type{cel.BytesType, cel.StringType},
cel.BoolType,
cel.BinaryBinding(func(lhs ref.Val, rhs ref.Val) ref.Val {
var ip net.IP
switch v := lhs.Value().(type) {
case []byte:
ip = v
case net.IP:
ip = v
}
if ip == nil {
panic(fmt.Errorf("invalid ip %v", lhs.Value()))
}
val, ok := rhs.Value().(string)
if !ok {
panic(fmt.Errorf("invalid network value %v", rhs.Value()))
}
network, ok := networks[val]
if !ok {
_, ipNet, err := net.ParseCIDR(val)
if err != nil {
panic("network not found")
}
return types.Bool(ipNet.Contains(ip))
} else {
ok, err := network.Contains(ip)
if err != nil {
panic(err)
}
return types.Bool(ok)
}
}),
),
),
cel.Function("inNetwork",
cel.Overload("inNetwork_string_ip",
[]*cel.Type{cel.StringType, cel.BytesType},
cel.BoolType,
cel.BinaryBinding(func(lhs ref.Val, rhs ref.Val) ref.Val {
var ip net.IP
switch v := rhs.Value().(type) {
case []byte:
ip = v
case net.IP:
ip = v
}
if ip == nil {
panic(fmt.Errorf("invalid ip %v", rhs.Value()))
}
val, ok := lhs.Value().(string)
if !ok {
panic(fmt.Errorf("invalid value %v", lhs.Value()))
}
slog.Debug(fmt.Sprintf("inNetwork function has been deprecated and will be removed in a future release, use remoteAddress.network(\"%s\") instead", val))
network, ok := networks[val]
if !ok {
_, ipNet, err := net.ParseCIDR(val)
if err != nil {
panic("network not found")
}
return types.Bool(ipNet.Contains(ip))
} else {
ok, err := network.Contains(ip)
if err != nil {
panic(err)
}
return types.Bool(ok)
}
}),
),
),
)
}
func Program(env *cel.Env, ast *cel.Ast) (cel.Program, error) {
return env.Program(ast,
cel.EvalOptions(cel.OptOptimize),
)
}
func FromStrings(env *cel.Env, operator string, conditions ...string) (*cel.Ast, error) {
var asts []*cel.Ast
for _, c := range conditions {
ast, issues := env.Compile(c)
if issues != nil && issues.Err() != nil {
return nil, fmt.Errorf("condition %s: %s", issues.Err(), c)
}
asts = append(asts, ast)
}
return Merge(env, operator, asts...)
}
func Merge(env *cel.Env, operator string, conditions ...*cel.Ast) (*cel.Ast, error) {
if len(conditions) == 0 {
return nil, nil
} else if len(conditions) == 1 {
return conditions[0], nil
}
var asts []string
for _, c := range conditions {
ast, err := cel.AstToString(c)
if err != nil {
return nil, err
}
asts = append(asts, "("+ast+")")
}
condition := strings.Join(asts, " "+operator+" ")
ast, issues := env.Compile(condition)
if issues != nil && issues.Err() != nil {
return nil, issues.Err()
}
return ast, nil
}

View File

@@ -1,158 +0,0 @@
package condition
import (
"fmt"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"net/textproto"
"reflect"
"strings"
)
type mimeLike struct {
m textproto.MIMEHeader
}
func (a mimeLike) ConvertToNative(typeDesc reflect.Type) (any, error) {
return nil, fmt.Errorf("type conversion error from map to '%v'", typeDesc)
}
func (a mimeLike) ConvertToType(typeVal ref.Type) ref.Val {
switch typeVal {
case types.MapType:
return a
case types.TypeType:
return types.MapType
}
return types.NewErr("type conversion error from '%s' to '%s'", types.MapType, typeVal)
}
func (a mimeLike) Equal(other ref.Val) ref.Val {
return types.Bool(false)
}
func (a mimeLike) Type() ref.Type {
return types.MapType
}
func (a mimeLike) Value() any {
return a.m
}
func (a mimeLike) Contains(key ref.Val) ref.Val {
_, found := a.Find(key)
return types.Bool(found)
}
func (a mimeLike) Get(key ref.Val) ref.Val {
v, found := a.Find(key)
if !found {
return types.ValOrErr(v, "no such key: %v", key)
}
return v
}
func (a mimeLike) Iterator() traits.Iterator {
panic("implement me")
}
func (a mimeLike) IsZeroValue() bool {
return len(a.m) == 0
}
func (a mimeLike) Size() ref.Val {
return types.Int(len(a.m))
}
func (a mimeLike) Find(key ref.Val) (ref.Val, bool) {
k, ok := key.(types.String)
if !ok {
return nil, false
}
return singleVal(a.m.Values(string(k)), true)
}
type valuesLike struct {
m map[string][]string
}
func (a valuesLike) ConvertToNative(typeDesc reflect.Type) (any, error) {
return nil, fmt.Errorf("type conversion error from map to '%v'", typeDesc)
}
func (a valuesLike) ConvertToType(typeVal ref.Type) ref.Val {
switch typeVal {
case types.MapType:
return a
case types.TypeType:
return types.MapType
}
return types.NewErr("type conversion error from '%s' to '%s'", types.MapType, typeVal)
}
func (a valuesLike) Equal(other ref.Val) ref.Val {
return types.Bool(false)
}
func (a valuesLike) Type() ref.Type {
return types.MapType
}
func (a valuesLike) Value() any {
return a.m
}
func (a valuesLike) Contains(key ref.Val) ref.Val {
_, found := a.Find(key)
return types.Bool(found)
}
func (a valuesLike) Get(key ref.Val) ref.Val {
v, found := a.Find(key)
if !found {
return types.ValOrErr(v, "no such key: %v", key)
}
return v
}
func (a valuesLike) Iterator() traits.Iterator {
panic("implement me")
}
func (a valuesLike) IsZeroValue() bool {
return len(a.m) == 0
}
func (a valuesLike) Size() ref.Val {
return types.Int(len(a.m))
}
func (a valuesLike) Find(key ref.Val) (ref.Val, bool) {
k, ok := key.(types.String)
if !ok {
return nil, false
}
val, ok := a.m[string(k)]
return singleVal(val, ok)
}
func singleVal(values []string, ok bool) (ref.Val, bool) {
if len(values) == 0 || !ok {
return nil, false
}
if len(values) > 1 {
return types.String(strings.Join(values, ",")), true
}
return types.String(values[0]), true
}
func NewMIMEMap(m textproto.MIMEHeader) traits.Mapper {
return mimeLike{m: m}
}
func NewValuesMap(m map[string][]string) traits.Mapper {
return mimeLike{m: m}
}

View File

@@ -1,11 +1,111 @@
package lib package lib
import ( import (
"git.gammaspectra.live/git/go-away/lib/condition" http_cel "codeberg.org/gone/http-cel"
"fmt"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"log/slog"
"net"
) )
func (state *State) initConditions() (err error) { func (state *State) initConditions() (err error) {
state.programEnv, err = condition.NewRulesEnvironment(state.networks) state.programEnv, err = http_cel.NewEnvironment(
cel.Variable("fp", cel.MapType(cel.StringType, cel.StringType)),
cel.Function("inDNSBL",
cel.Overload("inDNSBL_ip",
[]*cel.Type{cel.AnyType},
cel.BoolType,
cel.UnaryBinding(func(val ref.Val) ref.Val {
slog.Error("inDNSBL function has been deprecated, replace with dnsbl challenge")
return types.Bool(false)
}),
),
),
cel.Function("network",
cel.MemberOverload("netIP_network_string",
[]*cel.Type{cel.BytesType, cel.StringType},
cel.BoolType,
cel.BinaryBinding(func(lhs ref.Val, rhs ref.Val) ref.Val {
var ip net.IP
switch v := lhs.Value().(type) {
case []byte:
ip = v
case net.IP:
ip = v
}
if ip == nil {
panic(fmt.Errorf("invalid ip %v", lhs.Value()))
}
val, ok := rhs.Value().(string)
if !ok {
panic(fmt.Errorf("invalid network value %v", rhs.Value()))
}
network, ok := state.networks[val]
if !ok {
_, ipNet, err := net.ParseCIDR(val)
if err != nil {
panic("network not found")
}
return types.Bool(ipNet.Contains(ip))
} else {
ok, err := network.Contains(ip)
if err != nil {
panic(err)
}
return types.Bool(ok)
}
}),
),
),
cel.Function("inNetwork",
cel.Overload("inNetwork_string_ip",
[]*cel.Type{cel.StringType, cel.BytesType},
cel.BoolType,
cel.BinaryBinding(func(lhs ref.Val, rhs ref.Val) ref.Val {
var ip net.IP
switch v := rhs.Value().(type) {
case []byte:
ip = v
case net.IP:
ip = v
}
if ip == nil {
panic(fmt.Errorf("invalid ip %v", rhs.Value()))
}
val, ok := lhs.Value().(string)
if !ok {
panic(fmt.Errorf("invalid value %v", lhs.Value()))
}
slog.Debug(fmt.Sprintf("inNetwork function has been deprecated and will be removed in a future release, use remoteAddress.network(\"%s\") instead", val))
network, ok := state.networks[val]
if !ok {
_, ipNet, err := net.ParseCIDR(val)
if err != nil {
panic("network not found")
}
return types.Bool(ipNet.Contains(ip))
} else {
ok, err := network.Contains(ip)
if err != nil {
panic(err)
}
return types.Bool(ok)
}
}),
),
),
)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -6,62 +6,18 @@ import (
"git.gammaspectra.live/git/go-away/embed" "git.gammaspectra.live/git/go-away/embed"
"git.gammaspectra.live/git/go-away/lib/action" "git.gammaspectra.live/git/go-away/lib/action"
"git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/challenge"
"git.gammaspectra.live/git/go-away/lib/policy"
"git.gammaspectra.live/git/go-away/utils" "git.gammaspectra.live/git/go-away/utils"
"html/template"
"log/slog" "log/slog"
"net/http" "net/http"
"net/http/pprof"
"strconv"
"strings" "strings"
"time"
) )
var templates map[string]*template.Template
func init() {
templates = make(map[string]*template.Template)
dir, err := embed.TemplatesFs.ReadDir(".")
if err != nil {
panic(err)
}
for _, e := range dir {
if e.IsDir() {
continue
}
data, err := embed.TemplatesFs.ReadFile(e.Name())
if err != nil {
panic(err)
}
err = initTemplate(e.Name(), string(data))
if err != nil {
panic(err)
}
}
}
func initTemplate(name, data string) error {
tpl := template.New(name)
_, err := tpl.Parse(data)
if err != nil {
return err
}
templates[name] = tpl
return nil
}
func (state *State) addTiming(w http.ResponseWriter, name, desc string, duration time.Duration) {
if state.Settings().Debug {
w.Header().Add("Server-Timing", fmt.Sprintf("%s;desc=%s;dur=%d", name, strconv.Quote(desc), duration.Milliseconds()))
}
}
func GetLoggerForRequest(r *http.Request) *slog.Logger { func GetLoggerForRequest(r *http.Request) *slog.Logger {
data := challenge.RequestDataFromContext(r.Context()) data := challenge.RequestDataFromContext(r.Context())
args := []any{ args := []any{
"request_id", data.Id.String(), "request_id", data.Id.String(),
"remote_address", data.RemoteAddress.String(), "remote_address", data.RemoteAddress.Addr().String(),
"user_agent", r.UserAgent(), "user_agent", r.UserAgent(),
"host", r.Host, "host", r.Host,
"path", r.URL.Path, "path", r.URL.Path,
@@ -96,11 +52,12 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
if fromChallenge { if fromChallenge {
r.Header.Del("Referer") r.Header.Del("Referer")
} }
if ref := r.FormValue(challenge.QueryArgReferer); ref != "" { q := r.URL.Query()
if ref := q.Get(challenge.QueryArgReferer); ref != "" {
r.Header.Set("Referer", ref) r.Header.Set("Referer", ref)
} }
q := r.URL.Query()
// 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 q {
if strings.HasPrefix(k, challenge.QueryArgPrefix) { if strings.HasPrefix(k, challenge.QueryArgPrefix) {
@@ -137,6 +94,9 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
} }
} }
state.RuleHit(r, "DEFAULT", lg)
data.State.ActionHit(r, policy.RuleActionPASS, lg)
// default pass // default pass
_, _ = action.Pass{}.Handle(lg, w, r, func() http.Handler { _, _ = action.Pass{}.Handle(lg, w, r, func() http.Handler {
r.Header.Set("X-Away-Rule", "DEFAULT") r.Header.Set("X-Away-Rule", "DEFAULT")
@@ -151,14 +111,6 @@ func (state *State) setupRoutes() error {
state.Mux.HandleFunc("/", state.handleRequest) state.Mux.HandleFunc("/", state.handleRequest)
if state.Settings().Debug {
//TODO: split this to a different listener, metrics listener
http.HandleFunc(state.urlPath+"/debug/pprof/", pprof.Index)
http.HandleFunc(state.urlPath+"/debug/pprof/profile", pprof.Profile)
http.HandleFunc(state.urlPath+"/debug/pprof/symbol", pprof.Symbol)
http.HandleFunc(state.urlPath+"/debug/pprof/trace", pprof.Trace)
}
state.Mux.Handle("GET "+state.urlPath+"/assets/", http.StripPrefix(state.UrlPath()+"/assets/", gzipped.FileServer(gzipped.FS(embed.AssetsFs)))) state.Mux.Handle("GET "+state.urlPath+"/assets/", http.StripPrefix(state.UrlPath()+"/assets/", gzipped.FileServer(gzipped.FS(embed.AssetsFs))))
for _, reg := range state.challenges { for _, reg := range state.challenges {

View File

@@ -1,14 +1,13 @@
package lib package lib
import ( import (
"bytes"
"crypto/ed25519" "crypto/ed25519"
"git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/challenge"
"git.gammaspectra.live/git/go-away/lib/policy" "git.gammaspectra.live/git/go-away/lib/policy"
"git.gammaspectra.live/git/go-away/lib/settings"
"git.gammaspectra.live/git/go-away/utils" "git.gammaspectra.live/git/go-away/utils"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"log/slog" "log/slog"
"maps"
"net/http" "net/http"
) )
@@ -42,7 +41,7 @@ func (state *State) ChallengeFailed(r *http.Request, reg *challenge.Registration
} }
logger.Warn("challenge failed", "challenge", reg.Name, "err", err, "redirect", redirect) logger.Warn("challenge failed", "challenge", reg.Name, "err", err, "redirect", redirect)
//TODO: metrics metrics.Challenge(reg.Name, "fail")
} }
func (state *State) ChallengePassed(r *http.Request, reg *challenge.Registration, redirect string, logger *slog.Logger) { func (state *State) ChallengePassed(r *http.Request, reg *challenge.Registration, redirect string, logger *slog.Logger) {
@@ -51,7 +50,7 @@ func (state *State) ChallengePassed(r *http.Request, reg *challenge.Registration
} }
logger.Warn("challenge passed", "challenge", reg.Name, "redirect", redirect) logger.Warn("challenge passed", "challenge", reg.Name, "redirect", redirect)
//TODO: metrics metrics.Challenge(reg.Name, "pass")
} }
func (state *State) ChallengeIssued(r *http.Request, reg *challenge.Registration, redirect string, logger *slog.Logger) { func (state *State) ChallengeIssued(r *http.Request, reg *challenge.Registration, redirect string, logger *slog.Logger) {
@@ -60,69 +59,29 @@ func (state *State) ChallengeIssued(r *http.Request, reg *challenge.Registration
} }
logger.Info("challenge issued", "challenge", reg.Name, "redirect", redirect) logger.Info("challenge issued", "challenge", reg.Name, "redirect", redirect)
//TODO: metrics metrics.Challenge(reg.Name, "issue")
}
func (state *State) ChallengeChecked(r *http.Request, reg *challenge.Registration, redirect string, logger *slog.Logger) {
metrics.Challenge(reg.Name, "check")
}
func (state *State) RuleHit(r *http.Request, name string, logger *slog.Logger) {
metrics.Rule(name, "hit")
}
func (state *State) RuleMiss(r *http.Request, name string, logger *slog.Logger) {
metrics.Rule(name, "miss")
}
func (state *State) ActionHit(r *http.Request, name policy.RuleAction, logger *slog.Logger) {
metrics.Action(name)
} }
func (state *State) Logger(r *http.Request) *slog.Logger { func (state *State) Logger(r *http.Request) *slog.Logger {
return GetLoggerForRequest(r) return GetLoggerForRequest(r)
} }
func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status int, reg *challenge.Registration, params map[string]any) {
data := challenge.RequestDataFromContext(r.Context())
input := make(map[string]any)
input["Id"] = data.Id.String()
input["Random"] = utils.CacheBust()
if reg != nil {
input["Challenge"] = reg.Name
input["Path"] = state.UrlPath()
}
input["Theme"] = state.Settings().ChallengeTemplateTheme
maps.Copy(input, params)
if _, ok := input["Title"]; !ok {
input["Title"] = "Checking you are not a bot"
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
buf := bytes.NewBuffer(make([]byte, 0, 8192))
err := templates["challenge-"+state.Settings().ChallengeTemplate+".gohtml"].Execute(buf, input)
if err != nil {
state.ErrorPage(w, r, http.StatusInternalServerError, err, "")
} else {
w.WriteHeader(status)
_, _ = w.Write(buf.Bytes())
}
}
func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int, err error, redirect string) {
data := challenge.RequestDataFromContext(r.Context())
w.Header().Set("Content-Type", "text/html; charset=utf-8")
buf := bytes.NewBuffer(make([]byte, 0, 8192))
err2 := templates["challenge-"+state.Settings().ChallengeTemplate+".gohtml"].Execute(buf, map[string]any{
"Id": data.Id.String(),
"Random": utils.CacheBust(),
"Error": err.Error(),
"Path": state.UrlPath(),
"Theme": state.Settings().ChallengeTemplateTheme,
"Title": "Oh no! " + http.StatusText(status),
"HideSpinner": true,
"Challenge": "",
"Redirect": redirect,
})
if err2 != nil {
// nested errors!
panic(err2)
} else {
w.WriteHeader(status)
_, _ = w.Write(buf.Bytes())
}
}
func (state *State) GetChallenge(id challenge.Id) (*challenge.Registration, bool) { func (state *State) GetChallenge(id challenge.Id) (*challenge.Registration, bool) {
reg, ok := state.challenges.Get(id) reg, ok := state.challenges.Get(id)
return reg, ok return reg, ok
@@ -136,10 +95,14 @@ func (state *State) GetChallengeByName(name string) (*challenge.Registration, bo
reg, _, ok := state.challenges.GetByName(name) reg, _, ok := state.challenges.GetByName(name)
return reg, ok return reg, ok
} }
func (state *State) Settings() policy.Settings { func (state *State) Settings() policy.StateSettings {
return state.settings return state.settings
} }
func (state *State) Options() settings.Settings {
return state.opt
}
func (state *State) GetBackend(host string) http.Handler { func (state *State) GetBackend(host string) http.Handler {
return utils.SelectHTTPHandler(state.Settings().Backends, host) return utils.SelectHTTPHandler(state.Settings().Backends, host)
} }

50
lib/metrics.go Normal file
View File

@@ -0,0 +1,50 @@
package lib
import (
"git.gammaspectra.live/git/go-away/lib/policy"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
type stateMetrics struct {
rules *prometheus.CounterVec
actions *prometheus.CounterVec
challenges *prometheus.CounterVec
}
func newMetrics() *stateMetrics {
return &stateMetrics{
rules: promauto.NewCounterVec(prometheus.CounterOpts{
Name: "go-away_rule_results",
Help: "The number of rule hits or misses",
}, []string{"rule", "result"}),
actions: promauto.NewCounterVec(prometheus.CounterOpts{
Name: "go-away_action_results",
Help: "The number of each action issued",
}, []string{"action"}),
challenges: promauto.NewCounterVec(prometheus.CounterOpts{
Name: "go-away_challenge_results",
Help: "The number of challenges issued, passed or explicitly failed",
}, []string{"challenge", "action"}),
}
}
func (metrics *stateMetrics) Rule(name, result string) {
metrics.rules.With(prometheus.Labels{"rule": name, "result": result}).Inc()
}
func (metrics *stateMetrics) Action(action policy.RuleAction) {
metrics.actions.With(prometheus.Labels{"action": string(action)}).Inc()
}
func (metrics *stateMetrics) Challenge(name, result string) {
metrics.challenges.With(prometheus.Labels{"challenge": name, "action": result}).Inc()
}
func (metrics *stateMetrics) Reset() {
metrics.rules.Reset()
metrics.actions.Reset()
metrics.challenges.Reset()
}
var metrics = newMetrics()

View File

@@ -1,22 +0,0 @@
package policy
import (
"git.gammaspectra.live/git/go-away/utils"
"net/http"
)
type Settings struct {
Cache utils.Cache
Backends map[string]http.Handler
PrivateKeySeed []byte
Debug bool
MainName string
MainVersion string
PackageName string
ChallengeTemplate string
ChallengeTemplateTheme string
ClientIpHeader string
BackendIpHeader string
ChallengeResponseCode int
}

View File

@@ -20,66 +20,77 @@ type Policy struct {
Rules []Rule `yaml:"rules"` Rules []Rule `yaml:"rules"`
} }
func NewPolicy(r io.Reader, snippetsDirectory string) (*Policy, error) { func NewPolicy(r io.Reader, snippetsDirectories ...string) (*Policy, error) {
var p Policy var p Policy
p.Networks = make(map[string][]Network) p.Networks = make(map[string][]Network)
p.Conditions = make(map[string][]string) p.Conditions = make(map[string][]string)
p.Challenges = make(map[string]Challenge) p.Challenges = make(map[string]Challenge)
if snippetsDirectory == "" { if len(snippetsDirectories) == 0 {
err := yaml.NewDecoder(r).Decode(&p) err := yaml.NewDecoder(r).Decode(&p)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else { } else {
err := yaml.NewDecoder(r, yaml.ReferenceDirs(snippetsDirectory)).Decode(&p) var entries []string
for _, dir := range snippetsDirectories {
if dir == "" {
// skip nil directories
continue
}
dirFiles, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, file := range dirFiles {
if file.IsDir() {
continue
}
entries = append(entries, path.Join(dir, file.Name()))
}
}
err := yaml.NewDecoder(r, yaml.ReferenceFiles(entries...)).Decode(&p)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// add specific entries from snippets // add specific entries from snippets
entries, err := os.ReadDir(snippetsDirectory)
if err != nil {
return nil, err
}
for _, entry := range entries { for _, entry := range entries {
var entryPolicy Policy var entryPolicy Policy
if !entry.IsDir() { entryData, err := os.ReadFile(entry)
entryData, err := os.ReadFile(path.Join(snippetsDirectory, entry.Name())) if err != nil {
if err != nil { return nil, err
return nil, err }
} err = yaml.NewDecoder(bytes.NewReader(entryData), yaml.ReferenceFiles(entries...)).Decode(&entryPolicy)
err = yaml.NewDecoder(bytes.NewReader(entryData), yaml.ReferenceDirs(snippetsDirectory)).Decode(&entryPolicy) if err != nil {
if err != nil { return nil, err
return nil, err }
}
// add networks / conditions / challenges definitions if they don't exist already // add networks / conditions / challenges definitions if they don't exist already
for k, v := range entryPolicy.Networks { for k, v := range entryPolicy.Networks {
// add network if policy entry does not exist // add network if policy entry does not exist
_, ok := p.Networks[k] _, ok := p.Networks[k]
if !ok { if !ok {
p.Networks[k] = v p.Networks[k] = v
}
} }
}
for k, v := range entryPolicy.Conditions { for k, v := range entryPolicy.Conditions {
// add condition if policy entry does not exist // add condition if policy entry does not exist
_, ok := p.Conditions[k] _, ok := p.Conditions[k]
if !ok { if !ok {
p.Conditions[k] = v p.Conditions[k] = v
}
} }
}
for k, v := range entryPolicy.Challenges { for k, v := range entryPolicy.Challenges {
// add challenge if policy entry does not exist // add challenge if policy entry does not exist
_, ok := p.Challenges[k] _, ok := p.Challenges[k]
if !ok { if !ok {
p.Challenges[k] = v p.Challenges[k] = v
}
} }
} }
} }
} }

19
lib/policy/state.go Normal file
View File

@@ -0,0 +1,19 @@
package policy
import (
"git.gammaspectra.live/git/go-away/utils"
"net/http"
)
type StateSettings struct {
Cache utils.Cache
Backends map[string]http.Handler
PrivateKeySeed []byte
MainName string
MainVersion string
BasePath string
ClientIpHeader string
BackendIpHeader string
ChallengeResponseCode int
}

View File

@@ -1,12 +1,12 @@
package lib package lib
import ( import (
http_cel "codeberg.org/gone/http-cel"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"git.gammaspectra.live/git/go-away/lib/action" "git.gammaspectra.live/git/go-away/lib/action"
"git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/challenge"
"git.gammaspectra.live/git/go-away/lib/condition"
"git.gammaspectra.live/git/go-away/lib/policy" "git.gammaspectra.live/git/go-away/lib/policy"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types"
@@ -66,12 +66,12 @@ func NewRuleState(state challenge.StateInterface, r policy.Rule, replacer *strin
conditions = append(conditions, cond) conditions = append(conditions, cond)
} }
ast, err := condition.FromStrings(state.ProgramEnv(), condition.OperatorOr, conditions...) ast, err := http_cel.NewAst(state.ProgramEnv(), http_cel.OperatorOr, conditions...)
if err != nil { if err != nil {
return RuleState{}, fmt.Errorf("error compiling conditions: %w", err) return RuleState{}, fmt.Errorf("error compiling conditions: %w", err)
} }
program, err := condition.Program(state.ProgramEnv(), ast) program, err := http_cel.ProgramAst(state.ProgramEnv(), ast)
if err != nil { if err != nil {
return RuleState{}, fmt.Errorf("error compiling program: %w", err) return RuleState{}, fmt.Errorf("error compiling program: %w", err)
} }
@@ -107,6 +107,9 @@ func (rule RuleState) Evaluate(logger *slog.Logger, w http.ResponseWriter, r *ht
return false, fmt.Errorf("error: evaluating administrative rule %s/%s: %w", data.Id.String(), rule.Hash, err) return false, fmt.Errorf("error: evaluating administrative rule %s/%s: %w", data.Id.String(), rule.Hash, err)
} else if out != nil && out.Type() == types.BoolType { } else if out != nil && out.Type() == types.BoolType {
if out.Equal(types.True) == types.True { if out.Equal(types.True) == types.True {
data.State.RuleHit(r, rule.Name, logger)
data.State.ActionHit(r, rule.Action, logger)
next, err = rule.Handler.Handle(lg, w, r, func() http.Handler { next, err = rule.Handler.Handle(lg, w, r, func() http.Handler {
r.Header.Set("X-Away-Rule", rule.Name) r.Header.Set("X-Away-Rule", rule.Name)
r.Header.Set("X-Away-Hash", rule.Hash) r.Header.Set("X-Away-Hash", rule.Hash)
@@ -134,7 +137,13 @@ func (rule RuleState) Evaluate(logger *slog.Logger, w http.ResponseWriter, r *ht
return next, nil return next, nil
} }
} }
} else {
data.State.RuleMiss(r, rule.Name, logger)
} }
} else if out != nil {
err := fmt.Errorf("return type not Bool, got %s", out.Type().TypeName())
lg.Error(err.Error())
return false, fmt.Errorf("error: evaluating administrative rule %s/%s: %w", data.Id.String(), rule.Hash, err)
} }
return true, nil return true, nil

106
lib/settings/backend.go Normal file
View File

@@ -0,0 +1,106 @@
package settings
import (
"git.gammaspectra.live/git/go-away/utils"
"net/http"
"net/http/httputil"
)
type Backend struct {
// URL Target server backend path. Supports http/https/unix protocols.
URL string `yaml:"url"`
// Host Override the Host header and TLS SNI with this value if specified
Host string `yaml:"host"`
//ProxyProtocol uint8 `yaml:"proxy-protocol"`
// HTTP2Enabled Enable HTTP2 to backend
HTTP2Enabled bool `yaml:"http2-enabled"`
// TLSSkipVerify Disable TLS certificate verification, if any
TLSSkipVerify bool `yaml:"tls-skip-verify"`
// IpHeader HTTP header to set containing the IP header. Set - to forcefully ignore global defaults.
IpHeader string `yaml:"ip-header"`
// GoDNS Resolve URL using the Go DNS server
// Only relevant when running with CGO enabled
GoDNS bool `yaml:"go-dns"`
}
func (b Backend) Create() (*httputil.ReverseProxy, error) {
if b.IpHeader == "-" {
b.IpHeader = ""
}
proxy, err := utils.MakeReverseProxy(b.URL, b.GoDNS)
if err != nil {
return nil, err
}
transport := proxy.Transport.(*http.Transport)
if b.HTTP2Enabled {
transport.ForceAttemptHTTP2 = true
}
if b.TLSSkipVerify {
transport.TLSClientConfig.InsecureSkipVerify = true
}
if b.Host != "" {
transport.TLSClientConfig.ServerName = b.Host
}
if b.IpHeader != "" || b.Host != "" {
director := proxy.Director
proxy.Director = func(req *http.Request) {
if b.IpHeader != "" {
if ip := utils.GetRemoteAddress(req.Context()); ip != nil {
req.Header.Set(b.IpHeader, ip.Addr().Unmap().String())
}
}
if b.Host != "" {
req.Host = b.Host
}
director(req)
}
}
/*if b.ProxyProtocol > 0 {
dialContext := transport.DialContext
if dialContext == nil {
dialContext = (&net.Dialer{}).DialContext
}
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := dialContext(ctx, network, addr)
if err != nil {
return nil, err
}
addrPort := utils.GetRemoteAddress(ctx)
if addrPort == nil {
// pass as is
hdr := proxyproto.HeaderProxyFromAddrs(b.ProxyProtocol, conn.LocalAddr(), conn.RemoteAddr())
_, err = hdr.WriteTo(conn)
if err != nil {
conn.Close()
return nil, err
}
} else {
// set proper headers!
hdr := proxyproto.HeaderProxyFromAddrs(b.ProxyProtocol, net.TCPAddrFromAddrPort(*addrPort), conn.RemoteAddr())
_, err = hdr.WriteTo(conn)
if err != nil {
conn.Close()
return nil, err
}
}
return conn, nil
}
}*/
proxy.Transport = transport
return proxy, nil
}

168
lib/settings/bind.go Normal file
View File

@@ -0,0 +1,168 @@
package settings
import (
"context"
"crypto/tls"
"fmt"
"git.gammaspectra.live/git/go-away/utils"
"github.com/pires/go-proxyproto"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"log/slog"
"net"
"net/http"
"os"
"strconv"
"sync/atomic"
)
type Bind struct {
Address string `yaml:"address"`
Network string `yaml:"network"`
SocketMode string `yaml:"socket-mode"`
Proxy bool `yaml:"proxy"`
Passthrough bool `yaml:"passthrough"`
// TLSAcmeAutoCert URL to ACME directory, or letsencrypt
TLSAcmeAutoCert string `yaml:"tls-acme-autocert"`
// TLSCertificate Alternate to TLSAcmeAutoCert
TLSCertificate string `yaml:"tls-certificate"`
// TLSPrivateKey Alternate to TLSAcmeAutoCert
TLSPrivateKey string `yaml:"tls-key"`
}
func (b *Bind) Listener() (net.Listener, string) {
return setupListener(b.Network, b.Address, b.SocketMode, b.Proxy)
}
func (b *Bind) Server(backends map[string]http.Handler, acmeCachePath string) (*http.Server, func(http.Handler), error) {
var tlsConfig *tls.Config
if b.TLSAcmeAutoCert != "" {
switch b.TLSAcmeAutoCert {
case "letsencrypt":
b.TLSAcmeAutoCert = acme.LetsEncryptURL
}
acmeManager := newACMEManager(b.TLSAcmeAutoCert, backends)
if acmeCachePath != "" {
err := os.MkdirAll(acmeCachePath, 0755)
if err != nil {
return nil, nil, fmt.Errorf("failed to create acme cache directory: %w", err)
}
acmeManager.Cache = autocert.DirCache(acmeCachePath)
}
slog.Warn(
"acme-autocert enabled",
"directory", b.TLSAcmeAutoCert,
)
tlsConfig = acmeManager.TLSConfig()
} else if b.TLSCertificate != "" && b.TLSPrivateKey != "" {
tlsConfig = &tls.Config{}
var err error
tlsConfig.Certificates = make([]tls.Certificate, 1)
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(b.TLSCertificate, b.TLSPrivateKey)
if err != nil {
return nil, nil, err
}
slog.Warn(
"TLS enabled",
"certificate", b.TLSCertificate,
)
}
var serverHandler atomic.Pointer[http.Handler]
server := utils.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if handler := serverHandler.Load(); handler == nil {
http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
} else {
(*handler).ServeHTTP(w, r)
}
}), tlsConfig)
swap := func(handler http.Handler) {
serverHandler.Store(&handler)
}
if b.Passthrough {
// setup a passthrough handler temporarily
swap(http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
backend := utils.SelectHTTPHandler(backends, r.Host)
if backend == nil {
http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
} else {
backend.ServeHTTP(w, r)
}
})))
}
return server, swap, nil
}
func setupListener(network, address, socketMode string, proxy bool) (net.Listener, string) {
if network == "proxy" {
network = "tcp"
proxy = true
}
formattedAddress := ""
switch network {
case "unix":
formattedAddress = "unix:" + address
case "tcp":
formattedAddress = "http://localhost" + address
default:
formattedAddress = fmt.Sprintf(`(%s) %s`, network, address)
}
listener, err := net.Listen(network, address)
if err != nil {
panic(fmt.Errorf("failed to bind to %s: %w", formattedAddress, err))
}
// additional permission handling for unix sockets
if network == "unix" {
mode, err := strconv.ParseUint(socketMode, 8, 0)
if err != nil {
listener.Close()
panic(fmt.Errorf("could not parse socket mode %s: %w", socketMode, err))
}
err = os.Chmod(address, os.FileMode(mode))
if err != nil {
listener.Close()
panic(fmt.Errorf("could not change socket mode: %w", err))
}
}
if proxy {
slog.Warn("listener PROXY enabled")
formattedAddress += " +PROXY"
listener = &proxyproto.Listener{
Listener: listener,
}
}
return listener, formattedAddress
}
func newACMEManager(clientDirectory string, backends map[string]http.Handler) *autocert.Manager {
manager := &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostPolicy(func(ctx context.Context, host string) error {
if utils.SelectHTTPHandler(backends, host) != nil {
return nil
}
return fmt.Errorf("acme/autocert: host %s not configured in backends", host)
}),
Client: &acme.Client{
HTTPClient: http.DefaultClient,
DirectoryURL: clientDirectory,
},
}
return manager
}

49
lib/settings/settings.go Normal file
View File

@@ -0,0 +1,49 @@
package settings
import "maps"
type Settings struct {
Bind Bind `yaml:"bind"`
Backends map[string]Backend `yaml:"backends"`
BindDebug string `yaml:"bind-debug"`
BindMetrics string `yaml:"bind-metrics"`
Strings Strings `yaml:"strings"`
// Links to add to challenge/error pages like privacy/impressum.
Links []Link `yaml:"links"`
ChallengeTemplate string `yaml:"challenge-template"`
// ChallengeTemplateOverrides Key/Value overrides for the current chosen template
ChallengeTemplateOverrides map[string]string `yaml:"challenge-template-overrides"`
}
type Link struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
}
var DefaultSettings = Settings{
Strings: DefaultStrings,
ChallengeTemplate: "anubis",
ChallengeTemplateOverrides: func() map[string]string {
m := make(map[string]string)
maps.Copy(m, map[string]string{
"Theme": "",
"Logo": "",
})
return m
}(),
Bind: Bind{
Address: ":8080",
Network: "tcp",
SocketMode: "0770",
Proxy: false,
TLSAcmeAutoCert: "",
},
Backends: make(map[string]Backend),
}

55
lib/settings/strings.go Normal file
View File

@@ -0,0 +1,55 @@
package settings
import (
"html/template"
"maps"
)
type Strings map[string]string
var DefaultStrings = make(Strings).set(map[string]string{
"title_challenge": "Checking you are not a bot",
"title_error": "Oh no!",
"noscript_warning": "<p>Sadly, you may need to enable JavaScript to get past this challenge. This is required because AI companies have changed the social contract around how website hosting works.</p>",
"details_title": "Why am I seeing this?",
"details_text": `
<p>
You are seeing this because the administrator of this website has set up <a href="https://git.gammaspectra.live/git/go-away">go-away</a>
to protect the server against the scourge of <a href="https://thelibre.news/foss-infrastructure-is-under-attack-by-ai-companies/">AI companies aggressively scraping websites</a>.
</p>
<p>
Mass scraping can and does cause downtime for the websites, which makes their resources inaccessible for everyone.
</p>
<p>
Please note that some challenges requires the use of modern JavaScript features and some plugins may disable these.
Disable such plugins for this domain (for example, JShelter) if you encounter any issues.
</p>
`,
"details_contact_admin_with_request_id": "If you have any issues contact the site administrator and provide the following Request Id",
"button_refresh_page": "Refresh page",
"status_loading_challenge": "Loading challenge",
"status_starting_challenge": "Starting challenge",
"status_loading": "Loading...",
"status_calculating": "Calculating...",
"status_challenge_success": "Challenge success!",
"status_challenge_done_took": "Done! Took",
"status_error": "Error:",
})
func (s Strings) set(v map[string]string) Strings {
maps.Copy(s, v)
return s
}
func (s Strings) Get(value string) template.HTML {
v, ok := (s)[value]
if !ok {
// fallback
return template.HTML("string:" + value)
}
return template.HTML(v)
}

View File

@@ -1,13 +1,14 @@
package lib package lib
import ( import (
http_cel "codeberg.org/gone/http-cel"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "crypto/rand"
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/challenge"
"git.gammaspectra.live/git/go-away/lib/condition"
"git.gammaspectra.live/git/go-away/lib/policy" "git.gammaspectra.live/git/go-away/lib/policy"
"git.gammaspectra.live/git/go-away/lib/settings"
"git.gammaspectra.live/git/go-away/utils" "git.gammaspectra.live/git/go-away/utils"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"github.com/yl2chen/cidranger" "github.com/yl2chen/cidranger"
@@ -31,7 +32,8 @@ type State struct {
publicKey ed25519.PublicKey publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey privateKey ed25519.PrivateKey
settings policy.Settings opt settings.Settings
settings policy.StateSettings
networks map[string]cidranger.Ranger networks map[string]cidranger.Ranger
@@ -44,10 +46,12 @@ type State struct {
Mux *http.ServeMux Mux *http.ServeMux
} }
func NewState(p policy.Policy, settings policy.Settings) (handler http.Handler, err error) { func NewState(p policy.Policy, opt settings.Settings, settings policy.StateSettings) (handler http.Handler, err error) {
state := new(State) state := new(State)
state.close = make(chan struct{}) state.close = make(chan struct{})
state.settings = settings state.settings = settings
state.opt = opt
metrics.Reset()
state.client = &http.Client{ state.client = &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
@@ -58,7 +62,7 @@ func NewState(p policy.Policy, settings policy.Settings) (handler http.Handler,
return nil, fmt.Errorf("failed to initialize RADb client: %w", err) return nil, fmt.Errorf("failed to initialize RADb client: %w", err)
} }
state.urlPath = "/.well-known/." + state.Settings().PackageName state.urlPath = state.Settings().BasePath
// set a reasonable configuration for default http proxy if there is none // set a reasonable configuration for default http proxy if there is none
for _, backend := range state.Settings().Backends { for _, backend := range state.Settings().Backends {
@@ -89,22 +93,18 @@ func NewState(p policy.Policy, settings policy.Settings) (handler http.Handler,
} }
} }
if state.Settings().ChallengeTemplate == "" { if templates["challenge-"+state.Options().ChallengeTemplate+".gohtml"] == nil {
state.settings.ChallengeTemplate = "anubis"
}
if templates["challenge-"+state.Settings().ChallengeTemplate+".gohtml"] == nil { if data, err := os.ReadFile(state.Options().ChallengeTemplate); err == nil && len(data) > 0 {
name := path.Base(state.Options().ChallengeTemplate)
if data, err := os.ReadFile(state.Settings().ChallengeTemplate); err == nil && len(data) > 0 {
name := path.Base(state.Settings().ChallengeTemplate)
err := initTemplate(name, string(data)) err := initTemplate(name, string(data))
if err != nil { if err != nil {
return nil, fmt.Errorf("error loading template %s: %w", settings.ChallengeTemplate, err) return nil, fmt.Errorf("error loading template %s: %w", state.Options().ChallengeTemplate, err)
} }
state.settings.ChallengeTemplate = name state.opt.ChallengeTemplate = name
} }
return nil, fmt.Errorf("no template defined for %s", settings.ChallengeTemplate) return nil, fmt.Errorf("no template defined for %s", state.Options().ChallengeTemplate)
} }
state.networks = make(map[string]cidranger.Ranger) state.networks = make(map[string]cidranger.Ranger)
@@ -189,7 +189,7 @@ func NewState(p policy.Policy, settings policy.Settings) (handler http.Handler,
var replacements []string var replacements []string
for k, entries := range p.Conditions { for k, entries := range p.Conditions {
ast, err := condition.FromStrings(state.programEnv, condition.OperatorOr, entries...) ast, err := http_cel.NewAst(state.programEnv, http_cel.OperatorOr, entries...)
if err != nil { if err != nil {
return nil, fmt.Errorf("conditions %s: error compiling conditions: %v", k, err) return nil, fmt.Errorf("conditions %s: error compiling conditions: %v", k, err)
} }
@@ -215,7 +215,6 @@ func NewState(p policy.Policy, settings policy.Settings) (handler http.Handler,
} }
for _, r := range p.Rules { for _, r := range p.Rules {
rule, err := NewRuleState(state, r, conditionReplacer, nil) rule, err := NewRuleState(state, r, conditionReplacer, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("rule %s: %w", r.Name, err) return nil, fmt.Errorf("rule %s: %w", r.Name, err)

114
lib/template.go Normal file
View File

@@ -0,0 +1,114 @@
package lib
import (
"bytes"
"git.gammaspectra.live/git/go-away/embed"
"git.gammaspectra.live/git/go-away/lib/challenge"
"git.gammaspectra.live/git/go-away/utils"
"html/template"
"maps"
"net/http"
)
var templates map[string]*template.Template
func init() {
templates = make(map[string]*template.Template)
dir, err := embed.TemplatesFs.ReadDir(".")
if err != nil {
panic(err)
}
for _, e := range dir {
if e.IsDir() {
continue
}
data, err := embed.TemplatesFs.ReadFile(e.Name())
if err != nil {
panic(err)
}
err = initTemplate(e.Name(), string(data))
if err != nil {
panic(err)
}
}
}
func initTemplate(name, data string) error {
tpl := template.New(name)
_, err := tpl.Parse(data)
if err != nil {
return err
}
templates[name] = tpl
return nil
}
func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status int, reg *challenge.Registration, params map[string]any) {
data := challenge.RequestDataFromContext(r.Context())
input := make(map[string]any)
input["Id"] = data.Id.String()
input["Random"] = utils.CacheBust()
input["Path"] = state.UrlPath()
input["Links"] = state.Options().Links
input["Strings"] = state.Options().Strings
for k, v := range state.Options().ChallengeTemplateOverrides {
input[k] = v
}
if reg != nil {
input["Challenge"] = reg.Name
}
maps.Copy(input, params)
if _, ok := input["Title"]; !ok {
input["Title"] = state.Options().Strings.Get("title_challenge")
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
buf := bytes.NewBuffer(make([]byte, 0, 8192))
err := templates["challenge-"+state.Options().ChallengeTemplate+".gohtml"].Execute(buf, input)
if err != nil {
state.ErrorPage(w, r, http.StatusInternalServerError, err, "")
} else {
w.WriteHeader(status)
_, _ = w.Write(buf.Bytes())
}
}
func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int, err error, redirect string) {
data := challenge.RequestDataFromContext(r.Context())
w.Header().Set("Content-Type", "text/html; charset=utf-8")
buf := bytes.NewBuffer(make([]byte, 0, 8192))
input := map[string]any{
"Id": data.Id.String(),
"Random": utils.CacheBust(),
"Error": err.Error(),
"Path": state.UrlPath(),
"Theme": "",
"Title": template.HTML(string(state.Options().Strings.Get("title_error")) + " " + http.StatusText(status)),
"Challenge": "",
"Redirect": redirect,
"Links": state.Options().Links,
"Strings": state.Options().Strings,
}
for k, v := range state.Options().ChallengeTemplateOverrides {
input[k] = v
}
err2 := templates["challenge-"+state.Options().ChallengeTemplate+".gohtml"].Execute(buf, input)
if err2 != nil {
// nested errors!
panic(err2)
} else {
w.WriteHeader(status)
_, _ = w.Write(buf.Bytes())
}
}

View File

@@ -10,6 +10,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/netip"
"net/url" "net/url"
"strings" "strings"
) )
@@ -68,13 +69,14 @@ func EnsureNoOpenRedirect(redirect string) (string, error) {
return uri.String(), nil return uri.String(), nil
} }
func MakeReverseProxy(target string) (*httputil.ReverseProxy, error) { func MakeReverseProxy(target string, goDns bool) (*httputil.ReverseProxy, error) {
u, err := url.Parse(target) u, err := url.Parse(target)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse target URL: %w", err) return nil, fmt.Errorf("failed to parse target URL: %w", err)
} }
transport := http.DefaultTransport.(*http.Transport).Clone() transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tls.Config{}
// https://github.com/oauth2-proxy/oauth2-proxy/blob/4e2100a2879ef06aea1411790327019c1a09217c/pkg/upstream/http.go#L124 // https://github.com/oauth2-proxy/oauth2-proxy/blob/4e2100a2879ef06aea1411790327019c1a09217c/pkg/upstream/http.go#L124
if u.Scheme == "unix" { if u.Scheme == "unix" {
@@ -88,9 +90,17 @@ func MakeReverseProxy(target string) (*httputil.ReverseProxy, error) {
} }
// tell transport how to handle the unix url scheme // tell transport how to handle the unix url scheme
transport.RegisterProtocol("unix", UnixRoundTripper{Transport: transport}) transport.RegisterProtocol("unix", UnixRoundTripper{Transport: transport})
} else if goDns {
dialer := &net.Dialer{
Resolver: &net.Resolver{
PreferGo: true,
},
}
transport.DialContext = dialer.DialContext
} }
rp := httputil.NewSingleHostReverseProxy(u) rp := httputil.NewSingleHostReverseProxy(u)
rp.Transport = transport rp.Transport = transport
return rp, nil return rp, nil
@@ -108,22 +118,44 @@ func GetRequestScheme(r *http.Request) string {
return "http" return "http"
} }
func GetRequestAddress(r *http.Request, clientHeader string) net.IP { func GetRequestAddress(r *http.Request, clientHeader string) netip.AddrPort {
var ipStr string strVal := r.RemoteAddr
if clientHeader != "" { if clientHeader != "" {
ipStr = r.Header.Get(clientHeader) strVal = r.Header.Get(clientHeader)
} }
if ipStr != "" { if strVal != "" {
// handle X-Forwarded-For // handle X-Forwarded-For
ipStr = strings.Split(ipStr, ",")[0] strVal = strings.Split(strVal, ",")[0]
} }
// fallback // fallback
if ipStr == "" { if strVal == "" {
ipStr, _, _ = net.SplitHostPort(r.RemoteAddr) strVal = r.RemoteAddr
} }
ipStr = strings.Trim(ipStr, "[]")
return net.ParseIP(ipStr) addrPort, err := netip.ParseAddrPort(strVal)
if err != nil {
addr, err2 := netip.ParseAddr(strVal)
if err2 != nil {
return netip.AddrPort{}
}
addrPort = netip.AddrPortFrom(addr, 0)
}
return addrPort
}
type remoteAddress struct{}
func SetRemoteAddress(r *http.Request, addrPort netip.AddrPort) *http.Request {
return r.WithContext(context.WithValue(r.Context(), remoteAddress{}, addrPort))
}
func GetRemoteAddress(ctx context.Context) *netip.AddrPort {
ip, ok := ctx.Value(remoteAddress{}).(netip.AddrPort)
if !ok {
return nil
}
return &ip
} }
func CacheBust() string { func CacheBust() string {