9 Commits
nix ... go1.22

Author SHA1 Message Date
WeebDataHoarder
0ca8e277f9 Patches to bring compatibility to Go 1.22
* Disabled container building
* No TLS Fingerprinting
* HTTP/2 H2C uses golang.org/x/net/http2/h2c
* html DOM walking uses custom function instead of iterator
* Pinned these packages to latest compatible releases:
 * github.com/go-jose/go-jose/v4 v4.0.5
 * golang.org/x/crypto v0.33.0
 * golang.org/x/net v0.37.0
 * golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
 * golang.org/x/text v0.22.0
 * google.golang.org/genproto/googleapis v0.0.0-20240826202546-f6391c0de4c7
2025-05-04 15:43:36 +02:00
WeebDataHoarder
b1f1e9a54f challenge/http: fix setting request headers properly, add method header 2025-05-04 04:03:07 +02:00
WeebDataHoarder
e0c0f8745d readme: add latest release badge 2025-05-04 04:02:38 +02:00
WeebDataHoarder
fb6c5c3eb4 examples/forgejo: remove standard-bots rule, it's redundant 2025-05-03 22:43:09 +02:00
WeebDataHoarder
aebbfa4eaa context: set client network address without original port on backend-ip-header option 2025-05-03 22:32:25 +02:00
WeebDataHoarder
816d0fef90 ci: trigger on tags 2025-05-03 22:14:15 +02:00
WeebDataHoarder
06aca367a1 ci: change push trigger 2025-05-03 22:12:13 +02:00
WeebDataHoarder
44c9114ae5 challenges: add refresh via JavaScript window.location 2025-05-03 21:35:12 +02:00
WeebDataHoarder
4b1878f1ac examples/forgejo: exclude fetchers from suspicious crawler 2025-05-03 21:21:13 +02:00
18 changed files with 449 additions and 742 deletions

View File

@@ -138,25 +138,12 @@ local Publish(mirror, registry, repo, secret, go, alpine, os, arch, trigger, pla
# #
local containerArchitectures = ["linux/amd64", "linux/arm64", "linux/riscv64"]; local containerArchitectures = ["linux/amd64", "linux/arm64", "linux/riscv64"];
local alpineVersion = "3.21"; local alpineVersion = "3.20";
local goVersion = "1.24"; local goVersion = "1.22";
local mirror = "https://mirror.gcr.io"; local mirror = "https://mirror.gcr.io";
[ [
Build(mirror, goVersion, alpineVersion, "linux", "amd64") + {"trigger": {event: ["push"], branch: ["*"], }}, Build(mirror, goVersion, alpineVersion, "linux", "amd64"),
Build(mirror, goVersion, alpineVersion, "linux", "arm64") + {"trigger": {event: ["push"], branch: ["*"], }}, Build(mirror, goVersion, alpineVersion, "linux", "arm64"),
# Test PRs
Build(mirror, goVersion, alpineVersion, "linux", "amd64") + {"name": "test-pr", "trigger": {event: ["pull_request"], }},
# latest
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(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(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
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(mirror, "codeberg.org", "codeberg.org/gone/go-away", "codeberg", 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

@@ -5,7 +5,7 @@ environment:
GOOS: linux GOOS: linux
GOTOOLCHAIN: local GOTOOLCHAIN: local
kind: pipeline kind: pipeline
name: build-1.24-alpine3.21-amd64 name: build-1.22-alpine3.20-amd64
platform: platform:
arch: amd64 arch: amd64
os: linux os: linux
@@ -16,7 +16,7 @@ steps:
- mkdir .bin - mkdir .bin
- go build -v -pgo=auto -v -trimpath -ldflags=-buildid= -o ./.bin/go-away ./cmd/go-away - go build -v -pgo=auto -v -trimpath -ldflags=-buildid= -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.22-alpine3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: build name: build
- commands: - commands:
@@ -24,7 +24,7 @@ steps:
--policy examples/forgejo.yml --policy-snippets examples/snippets/ --policy examples/forgejo.yml --policy-snippets examples/snippets/
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: check-policy-forgejo name: check-policy-forgejo
- commands: - commands:
@@ -32,7 +32,7 @@ steps:
--policy examples/generic.yml --policy-snippets examples/snippets/ --policy examples/generic.yml --policy-snippets examples/snippets/
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: check-policy-generic name: check-policy-generic
- commands: - commands:
@@ -40,7 +40,7 @@ steps:
--policy examples/spa.yml --policy-snippets examples/snippets/ --policy examples/spa.yml --policy-snippets examples/snippets/
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: check-policy-spa name: check-policy-spa
- commands: - commands:
@@ -51,7 +51,7 @@ steps:
0 0
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: test-wasm-success name: test-wasm-success
- commands: - commands:
@@ -62,14 +62,9 @@ steps:
1 1
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: test-wasm-fail name: test-wasm-fail
trigger:
branch:
- '*'
event:
- push
type: docker type: docker
--- ---
environment: environment:
@@ -78,7 +73,7 @@ environment:
GOOS: linux GOOS: linux
GOTOOLCHAIN: local GOTOOLCHAIN: local
kind: pipeline kind: pipeline
name: build-1.24-alpine3.21-arm64 name: build-1.22-alpine3.20-arm64
platform: platform:
arch: arm64 arch: arm64
os: linux os: linux
@@ -89,7 +84,7 @@ steps:
- mkdir .bin - mkdir .bin
- go build -v -pgo=auto -v -trimpath -ldflags=-buildid= -o ./.bin/go-away ./cmd/go-away - go build -v -pgo=auto -v -trimpath -ldflags=-buildid= -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.22-alpine3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: build name: build
- commands: - commands:
@@ -97,7 +92,7 @@ steps:
--policy examples/forgejo.yml --policy-snippets examples/snippets/ --policy examples/forgejo.yml --policy-snippets examples/snippets/
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: check-policy-forgejo name: check-policy-forgejo
- commands: - commands:
@@ -105,7 +100,7 @@ steps:
--policy examples/generic.yml --policy-snippets examples/snippets/ --policy examples/generic.yml --policy-snippets examples/snippets/
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: check-policy-generic name: check-policy-generic
- commands: - commands:
@@ -113,7 +108,7 @@ steps:
--policy examples/spa.yml --policy-snippets examples/snippets/ --policy examples/spa.yml --policy-snippets examples/snippets/
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: check-policy-spa name: check-policy-spa
- commands: - commands:
@@ -124,7 +119,7 @@ steps:
0 0
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: test-wasm-success name: test-wasm-success
- commands: - commands:
@@ -135,376 +130,12 @@ steps:
1 1
depends_on: depends_on:
- build - build
image: alpine:3.21 image: alpine:3.20
mirror: https://mirror.gcr.io mirror: https://mirror.gcr.io
name: test-wasm-fail name: test-wasm-fail
trigger:
branch:
- '*'
event:
- push
type: docker
---
environment:
CGO_ENABLED: "0"
GOARCH: amd64
GOOS: linux
GOTOOLCHAIN: local
kind: pipeline
name: test-pr
platform:
arch: amd64
os: linux
steps:
- commands:
- apk update
- apk add --no-cache git
- mkdir .bin
- 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
image: golang:1.24-alpine3.21
mirror: https://mirror.gcr.io
name: build
- 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/
depends_on:
- build
image: alpine:3.21
mirror: https://mirror.gcr.io
name: check-policy-forgejo
- 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/
depends_on:
- build
image: alpine:3.21
mirror: https://mirror.gcr.io
name: check-policy-generic
- commands:
- ./.bin/go-away --check --slog-level DEBUG --backend example.com=http://127.0.0.1:80
--policy examples/spa.yml --policy-snippets examples/snippets/
depends_on:
- build
image: alpine:3.21
mirror: https://mirror.gcr.io
name: check-policy-spa
- commands:
- ./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm
-make-challenge ./embed/challenge/js-pow-sha256/test/make-challenge.json -make-challenge-out
./embed/challenge/js-pow-sha256/test/make-challenge-out.json -verify-challenge
./embed/challenge/js-pow-sha256/test/verify-challenge.json -verify-challenge-out
0
depends_on:
- build
image: alpine:3.21
mirror: https://mirror.gcr.io
name: test-wasm-success
- commands:
- ./.bin/test-wasm-runtime -wasm ./embed/challenge/js-pow-sha256/runtime/runtime.wasm
-make-challenge ./embed/challenge/js-pow-sha256/test/make-challenge.json -make-challenge-out
./embed/challenge/js-pow-sha256/test/make-challenge-out.json -verify-challenge
./embed/challenge/js-pow-sha256/test/verify-challenge-fail.json -verify-challenge-out
1
depends_on:
- build
image: alpine:3.21
mirror: https://mirror.gcr.io
name: test-wasm-fail
trigger:
event:
- pull_request
type: docker
---
kind: pipeline
name: publish-latest-git
platform:
arch: amd64
os: linux
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:
DOCKER_BUILDKIT: "1"
LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0
TZ: UTC
image: plugins/buildx
name: docker
privileged: true
settings:
auto_tag_suffix: alpine3.21
build_args:
from: alpine:3.21
from_builder: golang:1.24-alpine3.21
compress: true
mirror: https://mirror.gcr.io
password:
from_secret: git_password
platform:
- linux/amd64
- linux/arm64
- linux/riscv64
registry: git.gammaspectra.live
repo: git.gammaspectra.live/git/go-away
tags:
- latest
username:
from_secret: git_username
trigger:
branch:
- master
event:
- push
type: docker
---
kind: pipeline
name: publish-latest-codeberg
platform:
arch: amd64
os: linux
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:
DOCKER_BUILDKIT: "1"
LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0
TZ: UTC
image: plugins/buildx
name: docker
privileged: true
settings:
auto_tag_suffix: alpine3.21
build_args:
from: alpine:3.21
from_builder: golang:1.24-alpine3.21
compress: true
mirror: https://mirror.gcr.io
password:
from_secret: codeberg_password
platform:
- linux/amd64
- linux/arm64
- linux/riscv64
registry: codeberg.org
repo: codeberg.org/gone/go-away
tags:
- latest
username:
from_secret: codeberg_username
trigger:
branch:
- master
event:
- push
type: docker
---
kind: pipeline
name: publish-latest-github
platform:
arch: amd64
os: linux
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:
DOCKER_BUILDKIT: "1"
LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0
TZ: UTC
image: plugins/buildx
name: docker
privileged: true
settings:
auto_tag_suffix: alpine3.21
build_args:
from: alpine:3.21
from_builder: golang:1.24-alpine3.21
compress: true
mirror: https://mirror.gcr.io
password:
from_secret: github_password
platform:
- linux/amd64
- linux/arm64
- linux/riscv64
registry: ghcr.io
repo: ghcr.io/weebdatahoarder/go-away
tags:
- latest
username:
from_secret: github_username
trigger:
branch:
- master
event:
- push
type: docker
---
kind: pipeline
name: publish-1.24-alpine3.21-git
platform:
arch: amd64
os: linux
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:
DOCKER_BUILDKIT: "1"
LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0
TZ: UTC
image: plugins/buildx
name: docker
privileged: true
settings:
auto_tag: true
auto_tag_suffix: alpine3.21
build_args:
from: alpine:3.21
from_builder: golang:1.24-alpine3.21
compress: true
mirror: https://mirror.gcr.io
password:
from_secret: git_password
platform:
- linux/amd64
- linux/arm64
- linux/riscv64
registry: git.gammaspectra.live
repo: git.gammaspectra.live/git/go-away
username:
from_secret: git_username
trigger:
event:
- promote
- tag
target:
- production
type: docker
---
kind: pipeline
name: publish-1.24-alpine3.21-codeberg
platform:
arch: amd64
os: linux
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:
DOCKER_BUILDKIT: "1"
LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0
TZ: UTC
image: plugins/buildx
name: docker
privileged: true
settings:
auto_tag: true
auto_tag_suffix: alpine3.21
build_args:
from: alpine:3.21
from_builder: golang:1.24-alpine3.21
compress: true
mirror: https://mirror.gcr.io
password:
from_secret: codeberg_password
platform:
- linux/amd64
- linux/arm64
- linux/riscv64
registry: codeberg.org
repo: codeberg.org/gone/go-away
username:
from_secret: codeberg_username
trigger:
event:
- promote
- tag
target:
- production
type: docker
---
kind: pipeline
name: publish-1.24-alpine3.21-github
platform:
arch: amd64
os: linux
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:
DOCKER_BUILDKIT: "1"
LC_ALL: C
PLUGIN_BUILDER_CONFIG: buildkitd.toml
PLUGIN_BUILDER_DRIVER: docker-container
SOURCE_DATE_EPOCH: 0
TZ: UTC
image: plugins/buildx
name: docker
privileged: true
settings:
auto_tag: true
auto_tag_suffix: alpine3.21
build_args:
from: alpine:3.21
from_builder: golang:1.24-alpine3.21
compress: true
mirror: https://mirror.gcr.io
password:
from_secret: github_password
platform:
- linux/amd64
- linux/arm64
- linux/riscv64
registry: ghcr.io
repo: ghcr.io/weebdatahoarder/go-away
username:
from_secret: github_username
trigger:
event:
- promote
- tag
target:
- production
type: docker type: docker
--- ---
kind: signature kind: signature
hmac: 5200d5eb519acb0f74a7b62b103399da23d6e994d63c20052b41b10a4654b37a hmac: 00be8252a86e2c760c07c2baa6efa55d75f46fdc22299bb3feb1b199cc54af9e
... ...

View File

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

View File

@@ -50,7 +50,7 @@ conditions:
is-suspicious-crawler: is-suspicious-crawler:
# TLS Fingerprint for specific agent without ALPN # TLS Fingerprint for specific agent without ALPN
- '(userAgent.startsWith("Mozilla/") || userAgent.startsWith("Opera/")) && ("ja4" in fp && fp.ja4.matches("^t[0-9a-z]+00_")) && !(userAgent.contains("facebookexternalhit/") || userAgent.contains("Twitterbot/"))' - '(userAgent.startsWith("Mozilla/") || userAgent.startsWith("Opera/")) && ("ja4" in fp && fp.ja4.matches("^t[0-9a-z]+00_")) && !(userAgent.contains("compatible;") || userAgent.contains("+http") || userAgent.contains("facebookexternalhit/") || userAgent.contains("Twitterbot/"))'
# Old engines # Old engines
- 'userAgent.contains("Presto/") || userAgent.contains("Trident/")' - 'userAgent.contains("Presto/") || userAgent.contains("Trident/")'
# Old IE browsers # Old IE browsers
@@ -147,7 +147,7 @@ rules:
- name: 0 - name: 0
action: check action: check
settings: settings:
challenges: [js-pow-sha256, http-cookie-check] challenges: [js-refresh, http-cookie-check]
- name: 1 - name: 1
action: check action: check
settings: settings:
@@ -173,7 +173,7 @@ rules:
- 'path.matches("^/[^/]+/[^/]+/archive/.*\\.(bundle|zip|tar\\.gz)") && ($is-generic-browser)' - 'path.matches("^/[^/]+/[^/]+/archive/.*\\.(bundle|zip|tar\\.gz)") && ($is-generic-browser)'
action: challenge action: challenge
settings: settings:
challenges: [ js-pow-sha256 ] challenges: [ js-refresh ]
- name: allow-git-operations - name: allow-git-operations
conditions: conditions:
@@ -242,18 +242,11 @@ rules:
- name: 0 - name: 0
action: check action: check
settings: settings:
challenges: [preload-link, header-refresh, js-pow-sha256, http-cookie-check] challenges: [preload-link, header-refresh, js-refresh, http-cookie-check]
- name: 1 - name: 1
action: check action: check
settings: settings:
challenges: [ resource-load, js-pow-sha256, http-cookie-check ] challenges: [ resource-load, js-refresh, http-cookie-check ]
- name: standard-bots
action: check
settings:
challenges: [meta-refresh, resource-load]
conditions:
- '($is-generic-robot-ua)'
# Allow all source downloads not caught in browser above # Allow all source downloads not caught in browser above
# todo: limit this as needed? # todo: limit this as needed?
@@ -274,7 +267,7 @@ rules:
# if DNSBL fails, check additional challenges # if DNSBL fails, check additional challenges
fail: check fail: check
fail-settings: fail-settings:
challenges: [js-pow-sha256, http-cookie-check] challenges: [js-refresh, http-cookie-check]
# Allow PUT/DELETE/PATCH/POST requests in general # Allow PUT/DELETE/PATCH/POST requests in general
- name: non-get-request - name: non-get-request
@@ -321,7 +314,7 @@ rules:
- name: standard-browser - name: standard-browser
action: challenge action: challenge
settings: settings:
challenges: [http-cookie-check, preload-link, meta-refresh, resource-load, js-pow-sha256] challenges: [http-cookie-check, preload-link, meta-refresh, resource-load, js-refresh, js-pow-sha256]
conditions: conditions:
- '($is-generic-browser)' - '($is-generic-browser)'

View File

@@ -98,7 +98,7 @@ rules:
- name: 0 - name: 0
action: check action: check
settings: settings:
challenges: [js-pow-sha256] challenges: [js-refresh]
- name: 1 - name: 1
action: check action: check
settings: settings:
@@ -122,12 +122,12 @@ rules:
# if DNSBL fails, check additional challenges # if DNSBL fails, check additional challenges
fail: check fail: check
fail-settings: fail-settings:
challenges: [js-pow-sha256] challenges: [js-refresh]
- name: suspicious-fetchers - name: suspicious-fetchers
action: check action: check
settings: settings:
challenges: [js-pow-sha256] challenges: [js-refresh]
conditions: conditions:
- 'userAgent.contains("facebookexternalhit/") || userAgent.contains("facebookcatalog/")' - 'userAgent.contains("facebookexternalhit/") || userAgent.contains("facebookcatalog/")'
@@ -170,7 +170,7 @@ rules:
- name: standard-browser - name: standard-browser
action: challenge action: challenge
settings: settings:
challenges: [preload-link, meta-refresh, resource-load, js-pow-sha256] challenges: [preload-link, meta-refresh, resource-load, js-refresh]
conditions: conditions:
- '($is-generic-browser)' - '($is-generic-browser)'

View File

@@ -0,0 +1,6 @@
challenges:
js-refresh:
# Challenges with a redirect via window.location (requires HTML parsing and JavaScript logic)
runtime: "refresh"
parameters:
refresh-via: "javascript"

36
go.mod
View File

@@ -1,14 +1,12 @@
module git.gammaspectra.live/git/go-away module git.gammaspectra.live/git/go-away
go 1.24.0 go 1.22.12
toolchain go1.24.2
require ( require (
codeberg.org/gone/http-cel v1.0.0 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.0.5
github.com/goccy/go-yaml v1.17.1 github.com/goccy/go-yaml v1.17.1
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
@@ -16,7 +14,8 @@ require (
github.com/prometheus/client_golang v1.22.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.33.0
golang.org/x/net v0.37.0
) )
require ( require (
@@ -29,13 +28,26 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/procfs v0.15.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-20250210185358-939b2ce775ac // indirect
golang.org/x/net v0.39.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/text v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.6 // indirect
) )
// Pin latest versions to support Go 1.22 to prevent a package update from changing them
// TODO: remove this when Go 1.22+ is supported by other downstream users
replace (
github.com/go-jose/go-jose/v4 => github.com/go-jose/go-jose/v4 v4.0.5
github.com/prometheus/procfs => github.com/prometheus/procfs v0.15.1
golang.org/x/crypto => golang.org/x/crypto v0.33.0
golang.org/x/exp => golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
golang.org/x/net => golang.org/x/net v0.35.0
golang.org/x/sys => golang.org/x/sys v0.30.0
golang.org/x/text => golang.org/x/text v0.22.0
google.golang.org/genproto/googleapis/api => google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7
google.golang.org/genproto/googleapis/rpc => google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7
)

36
go.sum
View File

@@ -15,8 +15,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
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=
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
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.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
@@ -45,8 +45,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 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 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
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=
@@ -62,20 +62,20 @@ github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZB
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f h1:tjZsroqekhC63+WMqzmWyW5Twj/ZfR5HAlpd5YQ1Vs0= google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ= google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -350,7 +350,7 @@ func (d *RequestData) RequestHeaders(headers http.Header) {
if d.State.Settings().ClientIpHeader != "" { if d.State.Settings().ClientIpHeader != "" {
headers.Del(d.State.Settings().ClientIpHeader) headers.Del(d.State.Settings().ClientIpHeader)
} }
headers.Set(d.State.Settings().BackendIpHeader, d.RemoteAddress.String()) headers.Set(d.State.Settings().BackendIpHeader, d.RemoteAddress.Addr().Unmap().String())
} }
for id, result := range d.ChallengeVerify { for id, result := range d.ChallengeVerify {
@@ -433,7 +433,13 @@ func (d *RequestData) verifyChallengeStateCookie(cookie *http.Cookie) (TokenChal
} }
func (d *RequestData) verifyChallengeState() (state TokenChallengeMap, err error) { func (d *RequestData) verifyChallengeState() (state TokenChallengeMap, err error) {
cookies := d.r.CookiesNamed(d.cookieName) var cookies []*http.Cookie
for _, cookie := range d.r.Cookies() {
if cookie.Name == d.cookieName {
cookies = append(cookies, cookie)
}
}
if len(cookies) == 0 { if len(cookies) == 0 {
return nil, http.ErrNoCookie return nil, http.ErrNoCookie
} }

View File

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

View File

@@ -1,9 +1,12 @@
package refresh package refresh
import ( import (
"encoding/json"
"fmt"
"git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/challenge"
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
"github.com/goccy/go-yaml/ast" "github.com/goccy/go-yaml/ast"
"html/template"
"net/http" "net/http"
"time" "time"
) )
@@ -45,7 +48,17 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio
return challenge.VerifyResultFail return challenge.VerifyResultFail
} }
if params.Mode == "meta" { if params.Mode == "javascript" {
data, err := json.Marshal(uri.String())
if err != nil {
return challenge.VerifyResultFail
}
state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{
"EndTags": []template.HTML{
template.HTML(fmt.Sprintf("<script type=\"text/javascript\">window.location = %s;</script>", string(data))),
},
})
} else if params.Mode == "meta" {
state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{ state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{
"MetaTags": []map[string]string{ "MetaTags": []map[string]string{
{ {

View File

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

View File

@@ -1,48 +1,13 @@
package utils package utils
import ( import (
"context"
"crypto/md5" "crypto/md5"
"crypto/sha256"
"crypto/tls"
"encoding/hex" "encoding/hex"
"fmt"
"net"
"net/http" "net/http"
"slices"
"strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
) )
func applyTLSFingerprinter(server *http.Server) {
if server.TLSConfig == nil {
return
}
server.TLSConfig = server.TLSConfig.Clone()
getConfigForClient := server.TLSConfig.GetConfigForClient
if getConfigForClient == nil {
getConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
return nil, nil
}
}
server.TLSConfig.GetConfigForClient = func(clientHello *tls.ClientHelloInfo) (*tls.Config, error) {
ja3n, ja4 := buildTLSFingerprint(clientHello)
ptr := clientHello.Context().Value(tlsFingerprintKey{})
if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil {
fpPtr.ja3n.Store(&ja3n)
fpPtr.ja4.Store(&ja4)
}
return getConfigForClient(clientHello)
}
server.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
return context.WithValue(ctx, tlsFingerprintKey{}, &TLSFingerprint{})
}
}
type tlsFingerprintKey struct{} type tlsFingerprintKey struct{}
type TLSFingerprint struct { type TLSFingerprint struct {
ja3n atomic.Pointer[TLSFingerprintJA3N] ja3n atomic.Pointer[TLSFingerprintJA3N]
@@ -105,227 +70,6 @@ const (
extensionEncryptedClientHello uint16 = 0xfe0d extensionEncryptedClientHello uint16 = 0xfe0d
) )
func tlsFingerprintJA3(hello *tls.ClientHelloInfo, sortExtensions bool) []byte {
buf := make([]byte, 0, 256)
{
var sslVersion uint16
var hasGrease bool
for _, v := range hello.SupportedVersions {
if v&greaseMask != greaseValue {
if v > sslVersion {
sslVersion = v
}
} else {
hasGrease = true
}
}
// maximum TLS 1.2 as specified on JA3, as TLS 1.3 is put in SupportedVersions
if slices.Contains(hello.Extensions, extensionSupportedVersions) && hasGrease && sslVersion > tls.VersionTLS12 {
sslVersion = tls.VersionTLS12
}
buf = strconv.AppendUint(buf, uint64(sslVersion), 10)
buf = append(buf, ',')
}
n := 0
for _, cipher := range hello.CipherSuites {
//if !slices.Contains(greaseValues[:], cipher) {
if cipher&greaseMask != greaseValue {
buf = strconv.AppendUint(buf, uint64(cipher), 10)
buf = append(buf, '-')
n = 1
}
}
buf = buf[:len(buf)-n]
buf = append(buf, ',')
n = 0
extensions := hello.Extensions
if sortExtensions {
extensions = slices.Clone(extensions)
slices.Sort(extensions)
}
for _, extension := range extensions {
if extension&greaseMask != greaseValue {
buf = strconv.AppendUint(buf, uint64(extension), 10)
buf = append(buf, '-')
n = 1
}
}
buf = buf[:len(buf)-n]
buf = append(buf, ',')
n = 0
for _, curve := range hello.SupportedCurves {
if curve&greaseMask != greaseValue {
buf = strconv.AppendUint(buf, uint64(curve), 10)
buf = append(buf, '-')
n = 1
}
}
buf = buf[:len(buf)-n]
buf = append(buf, ',')
n = 0
for _, point := range hello.SupportedPoints {
buf = strconv.AppendUint(buf, uint64(point), 10)
buf = append(buf, '-')
n = 1
}
buf = buf[:len(buf)-n]
sum := md5.Sum(buf)
return sum[:]
}
func tlsFingerprintJA4(hello *tls.ClientHelloInfo) (ja4 TLSFingerprintJA4) {
buf := make([]byte, 0, 10)
// TODO: t = TLS, q = QUIC
buf = append(buf, 't')
{
var sslVersion uint16
for _, v := range hello.SupportedVersions {
if v&greaseMask != greaseValue {
if v > sslVersion {
sslVersion = v
}
}
}
switch sslVersion {
case tls.VersionSSL30:
buf = append(buf, 's', '3')
case tls.VersionTLS10:
buf = append(buf, '1', '0')
case tls.VersionTLS11:
buf = append(buf, '1', '1')
case tls.VersionTLS12:
buf = append(buf, '1', '2')
case tls.VersionTLS13:
buf = append(buf, '1', '3')
default:
sslVersion -= 0x0201
buf = strconv.AppendUint(buf, uint64(sslVersion>>8), 10)
buf = strconv.AppendUint(buf, uint64(sslVersion&0xff), 10)
}
}
if slices.Contains(hello.Extensions, extensionServerName) && hello.ServerName != "" {
buf = append(buf, 'd')
} else {
buf = append(buf, 'i')
}
ciphers := make([]uint16, 0, len(hello.CipherSuites))
for _, cipher := range hello.CipherSuites {
if cipher&greaseMask != greaseValue {
ciphers = append(ciphers, cipher)
}
}
extensionCount := 0
extensions := make([]uint16, 0, len(hello.Extensions))
for _, extension := range hello.Extensions {
if extension&greaseMask != greaseValue {
extensionCount++
if extension != extensionALPN && extension != extensionServerName {
extensions = append(extensions, extension)
}
}
}
schemes := make([]tls.SignatureScheme, 0, len(hello.SignatureSchemes))
for _, scheme := range hello.SignatureSchemes {
if scheme&greaseMask != greaseValue {
schemes = append(schemes, scheme)
}
}
//TODO: maybe little endian
slices.Sort(ciphers)
slices.Sort(extensions)
//slices.Sort(schemes)
if len(ciphers) < 10 {
buf = append(buf, '0')
buf = strconv.AppendUint(buf, uint64(len(ciphers)), 10)
} else if len(ciphers) > 99 {
buf = append(buf, '9', '9')
} else {
buf = strconv.AppendUint(buf, uint64(len(ciphers)), 10)
}
if extensionCount < 10 {
buf = append(buf, '0')
buf = strconv.AppendUint(buf, uint64(extensionCount), 10)
} else if extensionCount > 99 {
buf = append(buf, '9', '9')
} else {
buf = strconv.AppendUint(buf, uint64(extensionCount), 10)
}
if len(hello.SupportedProtos) > 0 && len(hello.SupportedProtos[0]) > 1 {
buf = append(buf, hello.SupportedProtos[0][0], hello.SupportedProtos[0][len(hello.SupportedProtos[0])-1])
} else {
buf = append(buf, '0', '0')
}
copy(ja4.A[:], buf)
ja4.B = ja4SHA256(uint16SliceToHex(ciphers))
extBuf := uint16SliceToHex(extensions)
if len(schemes) > 0 {
extBuf = append(extBuf, '_')
extBuf = append(extBuf, uint16SliceToHex(schemes)...)
}
ja4.C = ja4SHA256(extBuf)
return ja4
}
func uint16SliceToHex[T ~uint16](in []T) (out []byte) {
if len(in) == 0 {
return out
}
out = slices.Grow(out, hex.EncodedLen(len(in)*2)+len(in))
for _, n := range in {
out = append(out, fmt.Sprintf("%04x", uint16(n))...)
out = append(out, ',')
}
out = out[:len(out)-1]
return out
}
func ja4SHA256(buf []byte) [6]byte {
if len(buf) == 0 {
return [6]byte{0, 0, 0, 0, 0, 0}
}
sum := sha256.Sum256(buf)
return [6]byte(sum[:6])
}
func buildTLSFingerprint(hello *tls.ClientHelloInfo) (ja3n TLSFingerprintJA3N, ja4 TLSFingerprintJA4) {
return TLSFingerprintJA3N(tlsFingerprintJA3(hello, true)), tlsFingerprintJA4(hello)
}
func GetTLSFingerprint(r *http.Request) *TLSFingerprint { func GetTLSFingerprint(r *http.Request) *TLSFingerprint {
ptr := r.Context().Value(tlsFingerprintKey{}) ptr := r.Context().Value(tlsFingerprintKey{})
if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil { if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil {

269
utils/fingerprint_modern.go Normal file
View File

@@ -0,0 +1,269 @@
//go:build !go1.22 && !go1.23
package utils
import (
"context"
"crypto/md5"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"net"
"net/http"
"slices"
"strconv"
)
func applyTLSFingerprinter(server *http.Server) {
server.TLSConfig = server.TLSConfig.Clone()
getCertificate := server.TLSConfig.GetCertificate
if getCertificate == nil {
server.TLSConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
ja3n, ja4 := buildTLSFingerprint(clientHello)
ptr := clientHello.Context().Value(tlsFingerprintKey{})
if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil {
fpPtr.ja3n.Store(&ja3n)
fpPtr.ja4.Store(&ja4)
}
return nil, nil
}
} else {
server.TLSConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
ja3n, ja4 := buildTLSFingerprint(clientHello)
ptr := clientHello.Context().Value(tlsFingerprintKey{})
if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil {
fpPtr.ja3n.Store(&ja3n)
fpPtr.ja4.Store(&ja4)
}
return getCertificate(clientHello)
}
}
server.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
return context.WithValue(ctx, tlsFingerprintKey{}, &TLSFingerprint{})
}
}
func tlsFingerprintJA3(hello *tls.ClientHelloInfo, sortExtensions bool) []byte {
buf := make([]byte, 0, 256)
{
var sslVersion uint16
var hasGrease bool
for _, v := range hello.SupportedVersions {
if v&greaseMask != greaseValue {
if v > sslVersion {
sslVersion = v
}
} else {
hasGrease = true
}
}
// maximum TLS 1.2 as specified on JA3, as TLS 1.3 is put in SupportedVersions
if slices.Contains(hello.Extensions, extensionSupportedVersions) && hasGrease && sslVersion > tls.VersionTLS12 {
sslVersion = tls.VersionTLS12
}
buf = strconv.AppendUint(buf, uint64(sslVersion), 10)
buf = append(buf, ',')
}
n := 0
for _, cipher := range hello.CipherSuites {
//if !slices.Contains(greaseValues[:], cipher) {
if cipher&greaseMask != greaseValue {
buf = strconv.AppendUint(buf, uint64(cipher), 10)
buf = append(buf, '-')
n = 1
}
}
buf = buf[:len(buf)-n]
buf = append(buf, ',')
n = 0
extensions := hello.Extensions
if sortExtensions {
extensions = slices.Clone(extensions)
slices.Sort(extensions)
}
for _, extension := range extensions {
if extension&greaseMask != greaseValue {
buf = strconv.AppendUint(buf, uint64(extension), 10)
buf = append(buf, '-')
n = 1
}
}
buf = buf[:len(buf)-n]
buf = append(buf, ',')
n = 0
for _, curve := range hello.SupportedCurves {
if curve&greaseMask != greaseValue {
buf = strconv.AppendUint(buf, uint64(curve), 10)
buf = append(buf, '-')
n = 1
}
}
buf = buf[:len(buf)-n]
buf = append(buf, ',')
n = 0
for _, point := range hello.SupportedPoints {
buf = strconv.AppendUint(buf, uint64(point), 10)
buf = append(buf, '-')
n = 1
}
buf = buf[:len(buf)-n]
sum := md5.Sum(buf)
return sum[:]
}
func tlsFingerprintJA4(hello *tls.ClientHelloInfo) (ja4 TLSFingerprintJA4) {
buf := make([]byte, 0, 10)
// TODO: t = TLS, q = QUIC
buf = append(buf, 't')
{
var sslVersion uint16
for _, v := range hello.SupportedVersions {
if v&greaseMask != greaseValue {
if v > sslVersion {
sslVersion = v
}
}
}
switch sslVersion {
case tls.VersionSSL30:
buf = append(buf, 's', '3')
case tls.VersionTLS10:
buf = append(buf, '1', '0')
case tls.VersionTLS11:
buf = append(buf, '1', '1')
case tls.VersionTLS12:
buf = append(buf, '1', '2')
case tls.VersionTLS13:
buf = append(buf, '1', '3')
default:
sslVersion -= 0x0201
buf = strconv.AppendUint(buf, uint64(sslVersion>>8), 10)
buf = strconv.AppendUint(buf, uint64(sslVersion&0xff), 10)
}
}
if slices.Contains(hello.Extensions, extensionServerName) && hello.ServerName != "" {
buf = append(buf, 'd')
} else {
buf = append(buf, 'i')
}
ciphers := make([]uint16, 0, len(hello.CipherSuites))
for _, cipher := range hello.CipherSuites {
if cipher&greaseMask != greaseValue {
ciphers = append(ciphers, cipher)
}
}
extensionCount := 0
extensions := make([]uint16, 0, len(hello.Extensions))
for _, extension := range hello.Extensions {
if extension&greaseMask != greaseValue {
extensionCount++
if extension != extensionALPN && extension != extensionServerName {
extensions = append(extensions, extension)
}
}
}
schemes := make([]tls.SignatureScheme, 0, len(hello.SignatureSchemes))
for _, scheme := range hello.SignatureSchemes {
if scheme&greaseMask != greaseValue {
schemes = append(schemes, scheme)
}
}
//TODO: maybe little endian
slices.Sort(ciphers)
slices.Sort(extensions)
//slices.Sort(schemes)
if len(ciphers) < 10 {
buf = append(buf, '0')
buf = strconv.AppendUint(buf, uint64(len(ciphers)), 10)
} else if len(ciphers) > 99 {
buf = append(buf, '9', '9')
} else {
buf = strconv.AppendUint(buf, uint64(len(ciphers)), 10)
}
if extensionCount < 10 {
buf = append(buf, '0')
buf = strconv.AppendUint(buf, uint64(extensionCount), 10)
} else if extensionCount > 99 {
buf = append(buf, '9', '9')
} else {
buf = strconv.AppendUint(buf, uint64(extensionCount), 10)
}
if len(hello.SupportedProtos) > 0 && len(hello.SupportedProtos[0]) > 1 {
buf = append(buf, hello.SupportedProtos[0][0], hello.SupportedProtos[0][len(hello.SupportedProtos[0])-1])
} else {
buf = append(buf, '0', '0')
}
copy(ja4.A[:], buf)
ja4.B = ja4SHA256(uint16SliceToHex(ciphers))
extBuf := uint16SliceToHex(extensions)
if len(schemes) > 0 {
extBuf = append(extBuf, '_')
extBuf = append(extBuf, uint16SliceToHex(schemes)...)
}
ja4.C = ja4SHA256(extBuf)
return ja4
}
func uint16SliceToHex[T ~uint16](in []T) (out []byte) {
if len(in) == 0 {
return out
}
out = slices.Grow(out, hex.EncodedLen(len(in)*2)+len(in))
for _, n := range in {
out = append(out, fmt.Sprintf("%04x", uint16(n))...)
out = append(out, ',')
}
out = out[:len(out)-1]
return out
}
func ja4SHA256(buf []byte) [6]byte {
if len(buf) == 0 {
return [6]byte{0, 0, 0, 0, 0, 0}
}
sum := sha256.Sum256(buf)
return [6]byte(sum[:6])
}
func buildTLSFingerprint(hello *tls.ClientHelloInfo) (ja3n TLSFingerprintJA3N, ja4 TLSFingerprintJA4) {
return TLSFingerprintJA3N(tlsFingerprintJA3(hello, true)), tlsFingerprintJA4(hello)
}

View File

@@ -16,27 +16,6 @@ import (
"time" "time"
) )
func NewServer(handler http.Handler, tlsConfig *tls.Config) *http.Server {
if tlsConfig == nil {
proto := new(http.Protocols)
proto.SetHTTP1(true)
proto.SetUnencryptedHTTP2(true)
h1s := &http.Server{
Handler: handler,
Protocols: proto,
}
return h1s
} else {
server := &http.Server{
TLSConfig: tlsConfig,
Handler: handler,
}
applyTLSFingerprinter(server)
return server
}
}
func SelectHTTPHandler(backends map[string]http.Handler, host string) http.Handler { func SelectHTTPHandler(backends map[string]http.Handler, host string) http.Handler {
backend, ok := backends[host] backend, ok := backends[host]
if !ok { if !ok {

28
utils/http_legacy.go Normal file
View File

@@ -0,0 +1,28 @@
//go:build go1.22 || go1.23
package utils
import (
"crypto/tls"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"net/http"
)
func NewServer(handler http.Handler, tlsConfig *tls.Config) *http.Server {
if tlsConfig == nil {
h2s := &http2.Server{}
h1s := &http.Server{
Handler: h2c.NewHandler(handler, h2s),
}
return h1s
} else {
server := &http.Server{
TLSConfig: tlsConfig,
Handler: handler,
}
return server
}
}

29
utils/http_modern.go Normal file
View File

@@ -0,0 +1,29 @@
//go:build !go1.22 && !go1.23
package utils
import (
"crypto/tls"
"net/http"
)
func NewServer(handler http.Handler, tlsConfig *tls.Config) *http.Server {
if tlsConfig == nil {
proto := new(http.Protocols)
proto.SetHTTP1(true)
proto.SetUnencryptedHTTP2(true)
h1s := &http.Server{
Handler: handler,
Protocols: proto,
}
return h1s
} else {
server := &http.Server{
TLSConfig: tlsConfig,
Handler: handler,
}
applyTLSFingerprinter(server)
return server
}
}

View File

@@ -39,7 +39,7 @@ func FetchTags(backend http.Handler, uri *url.URL, kinds ...string) (result []ht
return nil return nil
} }
for n := range node.Descendants() { descendants(node, func(n *html.Node) {
if n.Type == html.ElementNode && slices.Contains(kinds, n.Data) { if n.Type == html.ElementNode && slices.Contains(kinds, n.Data) {
result = append(result, html.Node{ result = append(result, html.Node{
Type: n.Type, Type: n.Type,
@@ -49,7 +49,14 @@ func FetchTags(backend http.Handler, uri *url.URL, kinds ...string) (result []ht
Attr: n.Attr, Attr: n.Attr,
}) })
} }
} })
return result return result
} }
func descendants(n *html.Node, f func(n *html.Node)) {
f(n)
for c := n.FirstChild; c != nil; c = c.NextSibling {
descendants(c, f)
}
}