Compare commits

..

3 Commits

Author SHA1 Message Date
Xe Iaso
729c52ab7a fix(lib/store/bbolt): gracefully handle the obsolete anubis bucket in cleanup
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-06 01:10:44 +00:00
Xe Iaso
0c49d2ff5b Update metadata
check-spelling run (push) for Xe/optimize-bbolt

Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
on-behalf-of: @check-spelling <check-spelling-bot@check-spelling.dev>
2025-07-06 01:06:17 +00:00
Xe Iaso
64d147fc9b fix(lib/store/bbolt): use a multi-bucket flow instead of a single bucket flow
Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-07-06 00:39:38 +00:00
148 changed files with 1042 additions and 5757 deletions

View File

@@ -2,15 +2,12 @@
// README at: https://github.com/devcontainers/templates/tree/main/src/debian
{
"name": "Dev",
"dockerComposeFile": [
"./docker-compose.yaml"
],
"dockerComposeFile": ["./docker-compose.yaml"],
"service": "workspace",
"workspaceFolder": "/workspace/anubis",
"postStartCommand": "bash ./.devcontainer/poststart.sh",
"postStartCommand": "npm ci && go mod download",
"features": {
"ghcr.io/xe/devcontainer-features/ko:1.1.0": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
"ghcr.io/xe/devcontainer-features/ko:1.1.0": {}
},
"initializeCommand": "mkdir -p ${localEnv:HOME}${localEnv:USERPROFILE}/.local/share/atuin",
"customizations": {
@@ -21,10 +18,8 @@
"golang.go",
"unifiedjs.vscode-mdx",
"a-h.templ",
"redhat.vscode-yaml",
"hashicorp.hcl",
"fredwangwang.vscode-hcl-format"
"redhat.vscode-yaml"
]
}
}
}
}

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env bash
pwd
npm ci &
go mod download &
go install ./utils/cmd/... &
wait

View File

@@ -1,25 +0,0 @@
.env
*.deb
*.rpm
# Additional package locks
pnpm-lock.yaml
yarn.lock
# Go binaries and test artifacts
main
*.test
node_modules
# MacOS
.DS_store
# Intellij
.idea
# how does this get here
doc/VERSION
web/static/js/*
!web/static/js/.gitignore

View File

@@ -3,4 +3,3 @@ https
ssh
ubuntu
workarounds
rjack

View File

@@ -92,4 +92,3 @@ ignore$
robots.txt
^lib/localization/locales/.*\.json$
^lib/localization/.*_test.go$
^test/.*$

View File

@@ -1,10 +1,12 @@
acs
aeacus
Aibrew
alrest
amazonbot
anthro
anubis
anubistest
apk
Applebot
archlinux
asnc
@@ -20,6 +22,7 @@ berr
bingbot
Bitcoin
bitrate
blogging
Bluesky
blueskybot
boi
@@ -59,6 +62,7 @@ connnection
containerbuild
coreutils
Cotoyogi
CRDs
Cromite
crt
Cscript
@@ -67,7 +71,6 @@ DDOS
Debian
debrpm
decaymap
devcontainers
Diffbot
discordapp
discordbot
@@ -117,7 +120,6 @@ goland
gomod
goodbot
googlebot
gopsutil
govulncheck
goyaml
GPG
@@ -129,7 +131,6 @@ Hashcash
hashrate
headermap
healthcheck
healthz
hec
hmc
hostable
@@ -145,7 +146,6 @@ Imagesift
imgproxy
impressum
inp
internets
IPTo
iptoasn
iss
@@ -159,6 +159,7 @@ jshelter
JWTs
kagi
kagibot
keikaku
Keyfunc
keypair
KHTML
@@ -172,13 +173,13 @@ lgbt
licend
licstart
lightpanda
limsa
LIMSA
Linting
linuxbrew
LLU
loadbalancer
lol
lominsa
LOMINSA
maintainership
malware
mcr
@@ -186,13 +187,12 @@ memes
metarefresh
metrix
mimi
Minfilia
minica
mistralai
Mojeek
mojeekbot
mozilla
nbf
nepeat
netsurf
nginx
nicksnyder
@@ -221,7 +221,6 @@ pipefail
pki
podkova
podman
poststart
prebaked
privkey
promauto
@@ -241,7 +240,9 @@ redhat
redir
redirectscheme
refactors
relayd
reputational
reqmeta
risc
ruleset
runlevels
@@ -253,11 +254,11 @@ searchbot
searx
sebest
secretplans
selfsigned
Semrush
Seo
setsebool
shellcheck
shirou
Sidetrade
simprint
sitemap
@@ -295,8 +296,10 @@ uberspace
Unbreak
unbreakdocker
unifiedjs
unixhttpd
unmarshal
unparseable
uuidgen
uvx
UXP
valkey
@@ -305,8 +308,6 @@ Velen
vendored
vhosts
videotest
VKE
Vultr
waitloop
weblate
webmaster
@@ -320,6 +321,7 @@ wordpress
Workaround
workdir
wpbot
xcaddy
Xeact
xeiaso
xeserv
@@ -336,7 +338,6 @@ yeet
yeetfile
yourdomain
yoursite
yyz
Zenos
zizmor
zombocom

View File

@@ -2,7 +2,7 @@ name: Docker image builds (pull requests)
on:
pull_request:
branches: ["main"]
branches: [ "main" ]
env:
DOCKER_METADATA_SET_OUTPUT_ENV: "true"
@@ -11,32 +11,7 @@ permissions:
contents: read
jobs:
buildx-bake:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Build and push
id: build
uses: docker/bake-action@76f9fa3a758507623da19f6092dc4089a7e61592 # v6.6.0
with:
source: .
push: true
sbom: true
cache-from: type=gha
cache-to: type=gha,mode=max
set: |
osiris.tags=ttl.sh/techaro/pr-${{ github.event.number }}/osiris:24h
containerbuild:
build:
runs-on: ubuntu-24.04
steps:
- name: Checkout code

View File

@@ -17,38 +17,7 @@ permissions:
pull-requests: write
jobs:
buildx-bake:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Log into registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/bake-action@76f9fa3a758507623da19f6092dc4089a7e61592 # v6.6.0
with:
source: .
push: true
sbom: true
cache-from: type=gha
cache-to: type=gha,mode=max
set: ""
containerbuild:
build:
runs-on: ubuntu-24.04
steps:
- name: Checkout code

View File

@@ -36,9 +36,6 @@ jobs:
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ghcr.io/techarohq/anubis/docs
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
main
- name: Build and push
id: build
@@ -52,15 +49,15 @@ jobs:
platforms: linux/amd64
push: true
- name: Apply k8s manifests to limsa lominsa
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
- name: Apply k8s manifests to aeacus
uses: actions-hub/kubectl@d50394b7d704525f93faefce1e65a6329ff67271 # v1.33.2
env:
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
with:
args: apply -k docs/manifest
- name: Apply k8s manifests to limsa lominsa
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
- name: Apply k8s manifests to aeacus
uses: actions-hub/kubectl@d50394b7d704525f93faefce1e65a6329ff67271 # v1.33.2
env:
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
with:

View File

@@ -2,7 +2,7 @@ name: Docs test build
on:
pull_request:
branches: ["main"]
branches: [ "main" ]
permissions:
contents: read
@@ -24,10 +24,7 @@ jobs:
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ghcr.io/techarohq/anubis/docs
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
main
images: ghcr.io/${{ github.repository }}/docs
- name: Build and push
id: build

View File

@@ -82,7 +82,7 @@ jobs:
run: npm run test
- name: Lint with staticcheck
uses: dominikh/staticcheck-action@024238d2898c874f26d723e7d0ff4308c35589a2 # v1.4.0
uses: dominikh/staticcheck-action@fe1dd0c3658873b46f8c9bb3291096a617310ca6 # v1.3.1
with:
version: "latest"

View File

@@ -1,9 +1,8 @@
name: Package builds (stable)
on:
workflow_dispatch:
# release:
# types: [published]
release:
types: [published]
permissions:
contents: write
@@ -14,67 +13,67 @@ jobs:
#runs-on: alrest-techarohq
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
fetch-tags: true
fetch-depth: 0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
fetch-tags: true
fetch-depth: 0
- name: build essential
run: |
sudo apt-get update
sudo apt-get install -y build-essential
- name: build essential
run: |
sudo apt-get update
sudo apt-get install -y build-essential
- name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@main
- name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@main
- name: Setup Homebrew cellar cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
/home/linuxbrew/.linuxbrew/Cellar
/home/linuxbrew/.linuxbrew/bin
/home/linuxbrew/.linuxbrew/etc
/home/linuxbrew/.linuxbrew/include
/home/linuxbrew/.linuxbrew/lib
/home/linuxbrew/.linuxbrew/opt
/home/linuxbrew/.linuxbrew/sbin
/home/linuxbrew/.linuxbrew/share
/home/linuxbrew/.linuxbrew/var
key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-go-homebrew-cellar-
- name: Setup Homebrew cellar cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
/home/linuxbrew/.linuxbrew/Cellar
/home/linuxbrew/.linuxbrew/bin
/home/linuxbrew/.linuxbrew/etc
/home/linuxbrew/.linuxbrew/include
/home/linuxbrew/.linuxbrew/lib
/home/linuxbrew/.linuxbrew/opt
/home/linuxbrew/.linuxbrew/sbin
/home/linuxbrew/.linuxbrew/share
/home/linuxbrew/.linuxbrew/var
key: ${{ runner.os }}-go-homebrew-cellar-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-go-homebrew-cellar-
- name: Install Brew dependencies
run: |
brew bundle
- name: Install Brew dependencies
run: |
brew bundle
- name: Setup Golang caches
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-golang-
- name: Setup Golang caches
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-golang-
- name: install node deps
run: |
npm ci
- name: install node deps
run: |
npm ci
- name: Build Packages
run: |
go tool yeet
- name: Build Packages
run: |
go tool yeet
- name: Upload released artifacts
env:
GITHUB_TOKEN: ${{ github.TOKEN }}
RELEASE_VERSION: ${{github.event.release.tag_name}}
shell: bash
run: |
RELEASE="${RELEASE_VERSION}"
cd var
for file in *; do
gh release upload $RELEASE $file
done
- name: Upload released artifacts
env:
GITHUB_TOKEN: ${{ github.TOKEN }}
RELEASE_VERSION: ${{github.event.release.tag_name}}
shell: bash
run: |
RELEASE="${RELEASE_VERSION}"
cd var
for file in *; do
gh release upload $RELEASE $file
done

View File

@@ -1,45 +0,0 @@
name: Smoke tests
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
contents: read
jobs:
smoke-test:
strategy:
matrix:
test:
- git-clone
- git-push
- healthcheck
- i18n
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: latest
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: stable
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
- name: Install utils
run: |
go install ./utils/cmd/...
- name: Run test
run: |
cd test/${{ matrix.test }}
backoff-retry --try-count 10 ./test.sh

View File

@@ -25,19 +25,13 @@ jobs:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Install CI target SSH key
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0
with:
key: ${{ secrets.CI_SSH_KEY }}
name: id_rsa
known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }}
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: stable
- name: Run CI
run: go run ./utils/cmd/backoff-retry bash test/ssh-ci/rigging.sh ${{ matrix.host }}
run: bash test/ssh-ci/rigging.sh ${{ matrix.host }}
env:
GITHUB_RUN_ID: ${{ github.run_id }}

View File

@@ -21,7 +21,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: Run zizmor 🌈
run: uvx zizmor --format sarif . > results.sarif
@@ -29,7 +29,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
uses: github/codeql-action/upload-sarif@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1
with:
sarif_file: results.sarif
category: zizmor

View File

@@ -5,8 +5,6 @@
"golang.go",
"unifiedjs.vscode-mdx",
"a-h.templ",
"redhat.vscode-yaml",
"hashicorp.hcl",
"fredwangwang.vscode-hcl-format"
"redhat.vscode-yaml"
]
}

View File

@@ -41,20 +41,6 @@ Anubis is brought to you by sponsors and donors like:
<a href="https://wildbase.xyz/">
<img src="./docs/static/img/sponsors/wildbase-logo.webp" alt="Wildbase" height="64">
</a>
<a href="https://emma.pet">
<img
src="./docs/static/img/sponsors/nepeat-logo.webp"
alt="Cat eyes over the word Emma in a serif font"
height="64"
/>
</a>
<a href="https://fabulous.systems/">
<img
src="./docs/static/img/sponsors/fabulous-systems.webp"
alt="Cat eyes over the word Emma in a serif font"
height="64"
/>
</a>
## Overview

View File

@@ -1 +1 @@
1.21.0
1.20.0

View File

@@ -38,7 +38,6 @@ import (
"github.com/facebookgo/flagenv"
_ "github.com/joho/godotenv/autoload"
"github.com/prometheus/client_golang/prometheus/promhttp"
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
)
var (
@@ -98,7 +97,7 @@ func keyFromHex(value string) (ed25519.PrivateKey, error) {
}
func doHealthCheck() error {
resp, err := http.Get("http://localhost" + *metricsBind + "/healthz")
resp, err := http.Get("http://localhost" + *metricsBind + anubis.BasePrefix + "/metrics")
if err != nil {
return fmt.Errorf("failed to fetch metrics: %w", err)
}
@@ -232,6 +231,20 @@ func makeReverseProxy(target string, targetSNI string, targetHost string, insecu
return rp, nil
}
func startDecayMapCleanup(ctx context.Context, s *libanubis.Server) {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.CleanupDecayMap()
case <-ctx.Done():
return
}
}
}
func main() {
flagenv.Parse()
flag.Parse()
@@ -242,15 +255,6 @@ func main() {
}
internal.InitSlog(*slogLevel)
internal.SetHealth("anubis", healthv1.HealthCheckResponse_NOT_SERVING)
if *healthcheck {
log.Println("running healthcheck")
if err := doHealthCheck(); err != nil {
log.Fatal(err)
}
return
}
if *extractResources != "" {
if err := extractEmbedFS(data.BotPolicies, ".", *extractResources); err != nil {
@@ -263,17 +267,6 @@ func main() {
return
}
// install signal handler
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
wg := new(sync.WaitGroup)
if *metricsBind != "" {
wg.Add(1)
go metricsServer(ctx, wg.Done)
}
var rp http.Handler
// when using anubis via Systemd and environment variables, then it is not possible to set targe to an empty string but only to space
if strings.TrimSpace(*target) != "" {
@@ -288,6 +281,8 @@ func main() {
log.Fatalf("you can't set COOKIE_DOMAIN and COOKIE_DYNAMIC_DOMAIN at the same time")
}
ctx := context.Background()
// Thoth configuration
switch {
case *thothURL != "" && *thothToken == "":
@@ -417,12 +412,22 @@ func main() {
log.Fatalf("can't construct libanubis.Server: %v", err)
}
wg := new(sync.WaitGroup)
// install signal handler
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if *metricsBind != "" {
wg.Add(1)
go metricsServer(ctx, wg.Done)
}
go startDecayMapCleanup(ctx, s)
var h http.Handler
h = s
h = internal.RemoteXRealIP(*useRemoteAddress, *bindNetwork, h)
h = internal.XForwardedForToXRealIP(h)
h = internal.XForwardedForUpdate(*xffStripPrivate, h)
h = internal.JA4H(h)
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
listener, listenerUrl := setupListener(*bindNetwork, *bind)
@@ -451,8 +456,6 @@ func main() {
}
}()
internal.SetHealth("anubis", healthv1.HealthCheckResponse_SERVING)
if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
@@ -463,30 +466,20 @@ func metricsServer(ctx context.Context, done func()) {
defer done()
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
st, ok := internal.GetHealth("anubis")
if !ok {
slog.Error("health service anubis does not exist, file a bug")
}
switch st {
case healthv1.HealthCheckResponse_NOT_SERVING:
http.Error(w, "NOT OK", http.StatusInternalServerError)
return
case healthv1.HealthCheckResponse_SERVING:
fmt.Fprintln(w, "OK")
return
default:
http.Error(w, "UNKNOWN", http.StatusFailedDependency)
return
}
})
mux.Handle(anubis.BasePrefix+"/metrics", promhttp.Handler())
srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
slog.Debug("listening for metrics", "url", metricsUrl)
if *healthcheck {
log.Println("running healthcheck")
if err := doHealthCheck(); err != nil {
log.Fatal(err)
}
return
}
go func() {
<-ctx.Done()
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)

View File

@@ -1,39 +0,0 @@
package config
import (
"errors"
"fmt"
"net"
)
var (
ErrInvalidHostpost = errors.New("bind: invalid host:port")
)
type Bind struct {
HTTP string `hcl:"http"`
HTTPS string `hcl:"https"`
Metrics string `hcl:"metrics"`
}
func (b *Bind) Valid() error {
var errs []error
if _, _, err := net.SplitHostPort(b.HTTP); err != nil {
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidHostpost, b.HTTP, err))
}
if _, _, err := net.SplitHostPort(b.HTTPS); err != nil {
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidHostpost, b.HTTPS, err))
}
if _, _, err := net.SplitHostPort(b.Metrics); err != nil {
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidHostpost, b.Metrics, err))
}
if len(errs) != 0 {
return errors.Join(errs...)
}
return nil
}

View File

@@ -1,55 +0,0 @@
package config
import (
"errors"
"net"
"testing"
)
func TestBindValid(t *testing.T) {
for _, tt := range []struct {
name string
precondition func(t *testing.T)
bind Bind
err error
}{
{
name: "basic",
precondition: nil,
bind: Bind{
HTTP: ":8081",
HTTPS: ":8082",
Metrics: ":8083",
},
err: nil,
},
{
name: "invalid ports",
precondition: func(t *testing.T) {
ln, err := net.Listen("tcp", ":8081")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ln.Close() })
},
bind: Bind{
HTTP: "",
HTTPS: "",
Metrics: "",
},
err: ErrInvalidHostpost,
},
} {
t.Run(tt.name, func(t *testing.T) {
if tt.precondition != nil {
tt.precondition(t)
}
if err := tt.bind.Valid(); !errors.Is(err, tt.err) {
t.Logf("want: %v", tt.err)
t.Logf("got: %v", err)
t.Error("got wrong error from validation function")
}
})
}
}

View File

@@ -1,31 +0,0 @@
package config
import (
"errors"
"fmt"
)
type Toplevel struct {
Bind Bind `hcl:"bind,block"`
Domains []Domain `hcl:"domain,block"`
}
func (t *Toplevel) Valid() error {
var errs []error
if err := t.Bind.Valid(); err != nil {
errs = append(errs, fmt.Errorf("invalid bind block:\n%w", err))
}
for _, d := range t.Domains {
if err := d.Valid(); err != nil {
errs = append(errs, fmt.Errorf("when parsing domain %s: %w", d.Name, err))
}
}
if len(errs) != 0 {
return fmt.Errorf("invalid configuration file:\n%w", errors.Join(errs...))
}
return nil
}

View File

@@ -1,66 +0,0 @@
package config
import (
"errors"
"fmt"
"net/url"
"golang.org/x/net/idna"
)
var (
ErrInvalidDomainName = errors.New("domain: name is invalid")
ErrInvalidDomainTLSConfig = errors.New("domain: TLS config is invalid")
ErrInvalidURL = errors.New("invalid URL")
ErrInvalidURLScheme = errors.New("URL has invalid scheme")
)
type Domain struct {
Name string `hcl:"name,label"`
TLS TLS `hcl:"tls,block"`
Target string `hcl:"target"`
InsecureSkipVerify bool `hcl:"insecure_skip_verify,optional"`
HealthTarget string `hcl:"health_target"`
}
func (d Domain) Valid() error {
var errs []error
if _, err := idna.Lookup.ToASCII(d.Name); err != nil {
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidDomainName, d.Name, err))
}
if err := d.TLS.Valid(); err != nil {
errs = append(errs, fmt.Errorf("%w: %w", ErrInvalidDomainTLSConfig, err))
}
if err := isURLValid(d.Target); err != nil {
errs = append(errs, fmt.Errorf("target has %w %q: %w", ErrInvalidURL, d.Target, err))
}
if err := isURLValid(d.HealthTarget); err != nil {
errs = append(errs, fmt.Errorf("health_target has %w %q: %w", ErrInvalidURL, d.HealthTarget, err))
}
if len(errs) != 0 {
return errors.Join(errs...)
}
return nil
}
func isURLValid(input string) error {
u, err := url.Parse(input)
if err != nil {
return err
}
switch u.Scheme {
case "http", "https", "h2c", "unix":
// do nothing
default:
return fmt.Errorf("%w %s has scheme %s (want http, https, h2c, unix)", ErrInvalidURLScheme, input, u.Scheme)
}
return nil
}

View File

@@ -1,89 +0,0 @@
package config
import (
"errors"
"testing"
)
func TestDomainValid(t *testing.T) {
for _, tt := range []struct {
name string
input Domain
err error
}{
{
name: "simple happy path",
input: Domain{
Name: "anubis.techaro.lol",
TLS: TLS{
Cert: "./testdata/tls/selfsigned.crt",
Key: "./testdata/tls/selfsigned.key",
},
Target: "http://localhost:3000",
HealthTarget: "http://localhost:9091/healthz",
},
},
{
name: "invalid domain name",
input: Domain{
Name: "\uFFFD.techaro.lol",
TLS: TLS{
Cert: "./testdata/tls/selfsigned.crt",
Key: "./testdata/tls/selfsigned.key",
},
Target: "http://localhost:3000",
HealthTarget: "http://localhost:9091/healthz",
},
err: ErrInvalidDomainName,
},
{
name: "invalid tls config",
input: Domain{
Name: "anubis.techaro.lol",
TLS: TLS{
Cert: "./testdata/tls/invalid.crt",
Key: "./testdata/tls/invalid.key",
},
Target: "http://localhost:3000",
HealthTarget: "http://localhost:9091/healthz",
},
err: ErrInvalidDomainTLSConfig,
},
{
name: "invalid URL",
input: Domain{
Name: "anubis.techaro.lol",
TLS: TLS{
Cert: "./testdata/tls/selfsigned.crt",
Key: "./testdata/tls/selfsigned.key",
},
Target: "file://[::1:3000",
HealthTarget: "file://[::1:9091/healthz",
},
err: ErrInvalidURL,
},
{
name: "wrong URL scheme",
input: Domain{
Name: "anubis.techaro.lol",
TLS: TLS{
Cert: "./testdata/tls/selfsigned.crt",
Key: "./testdata/tls/selfsigned.key",
},
Target: "file://localhost:3000",
HealthTarget: "file://localhost:9091/healthz",
},
err: ErrInvalidURLScheme,
},
} {
t.Run(tt.name, func(t *testing.T) {
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
t.Logf("want: %v", tt.err)
t.Logf("got: %v", err)
t.Error("got wrong error from validation function")
} else {
t.Log(err)
}
})
}
}

View File

@@ -1 +0,0 @@
aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar

View File

@@ -1 +0,0 @@
aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar

View File

@@ -1,11 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIBnzCCAVGgAwIBAgIUAw8funCpiB3ZAAPoWdSCWnzbsFIwBQYDK2VwMEUxCzAJ
BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjUwNzE4MTkwMjM1WhcNMjUwODE3MTkwMjM1
WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAcXDHXV3vgpvjtTaz
s0Oj/73rMr06bhyGGhleYS1MNoWjUzBRMB0GA1UdDgQWBBQwmfKPthucFHB6Wfgz
2Nj5nkMQOjAfBgNVHSMEGDAWgBQwmfKPthucFHB6Wfgz2Nj5nkMQOjAPBgNVHRMB
Af8EBTADAQH/MAUGAytlcANBALBYbULlGwB7Ro0UTgUoQDNxEvayn3qzVFHIt7lC
/2/NzNBkk4yPT+a4mbRuydxLkv+JIvmQbarZxpksYnWlCAM=
-----END CERTIFICATE-----

View File

@@ -1,3 +0,0 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIOHKoX22Mha6SnnpLm34fSSfTUDbRiDCi6N1nOgTOlds
-----END PRIVATE KEY-----

View File

@@ -1,40 +0,0 @@
package config
import (
"crypto/tls"
"errors"
"fmt"
"os"
)
var (
ErrCantReadTLS = errors.New("tls: can't read TLS")
ErrInvalidTLSKeypair = errors.New("tls: can't parse TLS keypair")
)
type TLS struct {
Cert string `hcl:"cert"`
Key string `hcl:"key"`
}
func (t TLS) Valid() error {
var errs []error
if _, err := os.Stat(t.Cert); err != nil {
errs = append(errs, fmt.Errorf("%w certificate %s: %w", ErrCantReadTLS, t.Cert, err))
}
if _, err := os.Stat(t.Key); err != nil {
errs = append(errs, fmt.Errorf("%w key %s: %w", ErrCantReadTLS, t.Key, err))
}
if _, err := tls.LoadX509KeyPair(t.Cert, t.Key); err != nil {
errs = append(errs, fmt.Errorf("%w (%s, %s): %w", ErrInvalidTLSKeypair, t.Cert, t.Key, err))
}
if len(errs) != 0 {
return errors.Join(errs...)
}
return nil
}

View File

@@ -1,48 +0,0 @@
package config
import (
"errors"
"testing"
)
func TestTLSValid(t *testing.T) {
for _, tt := range []struct {
name string
input TLS
err error
}{
{
name: "simple selfsigned",
input: TLS{
Cert: "./testdata/tls/selfsigned.crt",
Key: "./testdata/tls/selfsigned.key",
},
},
{
name: "files don't exist",
input: TLS{
Cert: "./testdata/tls/nonexistent.crt",
Key: "./testdata/tls/nonexistent.key",
},
err: ErrCantReadTLS,
},
{
name: "invalid keypair",
input: TLS{
Cert: "./testdata/tls/invalid.crt",
Key: "./testdata/tls/invalid.key",
},
err: ErrInvalidTLSKeypair,
},
} {
t.Run(tt.name, func(t *testing.T) {
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
t.Logf("want: %v", tt.err)
t.Logf("got: %v", err)
t.Error("got wrong error from validation function")
} else {
t.Log(err)
}
})
}
}

View File

@@ -1,85 +0,0 @@
package entrypoint
import (
"context"
"fmt"
"log/slog"
"net"
"github.com/TecharoHQ/anubis/cmd/osiris/internal/config"
"github.com/TecharoHQ/anubis/internal"
"github.com/hashicorp/hcl/v2/hclsimple"
"golang.org/x/sync/errgroup"
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
)
type Options struct {
ConfigFname string
}
func Main(ctx context.Context, opts Options) error {
internal.SetHealth("osiris", healthv1.HealthCheckResponse_NOT_SERVING)
var cfg config.Toplevel
if err := hclsimple.DecodeFile(opts.ConfigFname, nil, &cfg); err != nil {
return fmt.Errorf("can't read configuration file %s:\n\n%w", opts.ConfigFname, err)
}
if err := cfg.Valid(); err != nil {
return fmt.Errorf("configuration file %s is invalid:\n\n%w", opts.ConfigFname, err)
}
rtr, err := NewRouter(cfg)
if err != nil {
return err
}
rtr.opts = opts
go rtr.backgroundReloadConfig(ctx)
g, gCtx := errgroup.WithContext(ctx)
// HTTP
g.Go(func() error {
ln, err := net.Listen("tcp", cfg.Bind.HTTP)
if err != nil {
return fmt.Errorf("(HTTP) can't bind to tcp %s: %w", cfg.Bind.HTTP, err)
}
defer ln.Close()
go func(ctx context.Context) {
<-ctx.Done()
ln.Close()
}(ctx)
slog.Info("listening", "for", "http", "bind", cfg.Bind.HTTP)
return rtr.HandleHTTP(gCtx, ln)
})
// HTTPS
g.Go(func() error {
ln, err := net.Listen("tcp", cfg.Bind.HTTPS)
if err != nil {
return fmt.Errorf("(https) can't bind to tcp %s: %w", cfg.Bind.HTTPS, err)
}
defer ln.Close()
go func(ctx context.Context) {
<-ctx.Done()
ln.Close()
}(ctx)
slog.Info("listening", "for", "https", "bind", cfg.Bind.HTTPS)
return rtr.HandleHTTPS(gCtx, ln)
})
// Metrics
g.Go(func() error {
return rtr.ListenAndServeMetrics(gCtx, cfg.Bind.Metrics)
})
internal.SetHealth("osiris", healthv1.HealthCheckResponse_SERVING)
return g.Wait()
}

View File

@@ -1,93 +0,0 @@
package entrypoint
import (
"context"
"errors"
"net"
"net/http"
"os"
"path/filepath"
"testing"
"time"
)
func TestMainGoodConfig(t *testing.T) {
files, err := os.ReadDir("./testdata/good")
if err != nil {
t.Fatal(err)
}
for _, st := range files {
t.Run(st.Name(), func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
cfg := loadConfig(t, filepath.Join("testdata", "good", st.Name()))
go func(ctx context.Context) {
if err := Main(ctx, Options{
ConfigFname: filepath.Join("testdata", "good", st.Name()),
}); err != nil {
var netOpErr *net.OpError
switch {
case errors.Is(err, context.Canceled):
// Context was canceled, this is expected
return
case errors.As(err, &netOpErr):
// Network operation error occurred
t.Logf("Network operation error: %v", netOpErr)
return
case errors.Is(err, http.ErrServerClosed):
// Server was closed, this is expected
return
default:
// Other unexpected error
panic(err)
}
}
}(ctx)
wait := 5 * time.Millisecond
for i := range make([]struct{}, 10) {
if i != 0 {
time.Sleep(wait)
wait = wait * 2
}
t.Logf("try %d (wait=%s)", i+1, wait)
resp, err := http.Get("http://localhost" + cfg.Bind.Metrics + "/readyz")
if err != nil {
continue
}
if resp.StatusCode != http.StatusOK {
continue
}
cancel()
return
}
t.Fatal("router initialization did not work")
})
}
}
func TestMainBadConfig(t *testing.T) {
files, err := os.ReadDir("./testdata/bad")
if err != nil {
t.Fatal(err)
}
for _, st := range files {
t.Run(st.Name(), func(t *testing.T) {
if err := Main(t.Context(), Options{
ConfigFname: filepath.Join("testdata", "bad", st.Name()),
}); err == nil {
t.Error("wanted an error but got none")
} else {
t.Log(err)
}
})
}
}

View File

@@ -1,35 +0,0 @@
package entrypoint
import (
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
"net/url"
"golang.org/x/net/http2"
)
func newH2CReverseProxy(target *url.URL) *httputil.ReverseProxy {
target.Scheme = "http"
director := func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.Host = target.Host
}
// Use h2c transport
transport := &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
// Just do plain TCP (h2c)
return net.Dial(network, addr)
},
}
return &httputil.ReverseProxy{
Director: director,
Transport: transport,
}
}

View File

@@ -1,51 +0,0 @@
package entrypoint
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func newH2cServer(t *testing.T, h http.Handler) *httptest.Server {
t.Helper()
h2s := &http2.Server{}
srv := httptest.NewServer(h2c.NewHandler(h, h2s))
t.Cleanup(func() {
srv.Close()
})
return srv
}
func TestH2CReverseProxy(t *testing.T) {
h := &ackHandler{}
srv := newH2cServer(t, h)
u, err := url.Parse(srv.URL)
if err != nil {
t.Fatal(err)
}
rp := httptest.NewServer(newH2CReverseProxy(u))
defer rp.Close()
resp, err := rp.Client().Get(rp.URL)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("wrong status code from reverse proxy: %d", resp.StatusCode)
}
if !h.ack {
t.Error("h2c handler was not executed")
}
}

View File

@@ -1,72 +0,0 @@
package entrypoint
import (
"bytes"
"fmt"
"log/slog"
"net/http"
"sort"
"github.com/TecharoHQ/anubis/internal"
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
)
func healthz(w http.ResponseWriter, r *http.Request) {
services, err := internal.HealthSrv.List(r.Context(), nil)
if err != nil {
slog.Error("can't get list of services", "err", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var keys []string
for k := range services.Statuses {
if k == "" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var msg bytes.Buffer
var healthy bool = true
for _, k := range keys {
st := services.Statuses[k].GetStatus()
fmt.Fprintf(&msg, "%s: %s\n", k, st)
switch st {
case healthv1.HealthCheckResponse_SERVING:
// do nothing
default:
healthy = false
}
}
if !healthy {
w.WriteHeader(http.StatusInternalServerError)
}
w.Write(msg.Bytes())
}
func readyz(w http.ResponseWriter, r *http.Request) {
st, ok := internal.GetHealth("osiris")
if !ok {
slog.Error("health service osiris does not exist, file a bug")
http.Error(w, "health service osiris does not exist", http.StatusExpectationFailed)
}
switch st {
case healthv1.HealthCheckResponse_NOT_SERVING:
http.Error(w, "NOT OK", http.StatusInternalServerError)
return
case healthv1.HealthCheckResponse_SERVING:
fmt.Fprintln(w, "OK")
return
default:
http.Error(w, "UNKNOWN", http.StatusFailedDependency)
return
}
}

View File

@@ -1,66 +0,0 @@
package entrypoint
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/TecharoHQ/anubis/internal"
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
)
func TestHealthz(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(healthz))
internal.SetHealth("osiris", healthv1.HealthCheckResponse_NOT_SERVING)
resp, err := srv.Client().Get(srv.URL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
t.Errorf("wanted not ready but got %d", resp.StatusCode)
}
internal.SetHealth("osiris", healthv1.HealthCheckResponse_SERVING)
resp, err = srv.Client().Get(srv.URL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("wanted ready but got %d", resp.StatusCode)
}
}
func TestReadyz(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(readyz))
internal.SetHealth("osiris", healthv1.HealthCheckResponse_NOT_SERVING)
resp, err := srv.Client().Get(srv.URL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
t.Errorf("wanted not ready but got %d", resp.StatusCode)
}
internal.SetHealth("osiris", healthv1.HealthCheckResponse_SERVING)
resp, err = srv.Client().Get(srv.URL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("wanted ready but got %d", resp.StatusCode)
}
}

View File

@@ -1,320 +0,0 @@
package entrypoint
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/TecharoHQ/anubis/cmd/osiris/internal/config"
"github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/fingerprint"
"github.com/felixge/httpsnoop"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/lum8rjack/go-ja4h"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
ErrTargetInvalid = errors.New("[unexpected] target invalid")
ErrNoHandler = errors.New("[unexpected] no handler for domain")
ErrInvalidTLSKeypair = errors.New("[unexpected] invalid TLS keypair")
ErrNoCert = errors.New("this server does not have a certificate for that domain")
requestsPerDomain = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "techaro",
Subsystem: "osiris",
Name: "request_count",
}, []string{"domain", "method", "response_code"})
responseTime = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "techaro",
Subsystem: "osiris",
Name: "response_time",
}, []string{"domain"})
unresolvedRequests = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "techaro",
Subsystem: "osiris",
Name: "unresolved_requests",
})
)
type Router struct {
lock sync.RWMutex
routes map[string]http.Handler
tlsCerts map[string]*tls.Certificate
opts Options
}
func (rtr *Router) setConfig(c config.Toplevel) error {
var errs []error
newMap := map[string]http.Handler{}
newCerts := map[string]*tls.Certificate{}
for _, d := range c.Domains {
var domainErrs []error
u, err := url.Parse(d.Target)
if err != nil {
domainErrs = append(domainErrs, fmt.Errorf("%w %q: %v", ErrTargetInvalid, d.Target, err))
}
var h http.Handler
if u != nil {
switch u.Scheme {
case "http", "https":
rp := httputil.NewSingleHostReverseProxy(u)
if d.InsecureSkipVerify {
rp.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
h = rp
case "h2c":
h = newH2CReverseProxy(u)
case "unix":
h = &httputil.ReverseProxy{
Director: func(r *http.Request) {
r.URL.Scheme = "http"
r.URL.Host = d.Name
r.Host = d.Name
},
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", strings.TrimPrefix(d.Target, "unix://"))
},
},
}
}
}
if h == nil {
domainErrs = append(domainErrs, ErrNoHandler)
}
newMap[d.Name] = h
cert, err := tls.LoadX509KeyPair(d.TLS.Cert, d.TLS.Key)
if err != nil {
domainErrs = append(domainErrs, fmt.Errorf("%w: %w", ErrInvalidTLSKeypair, err))
}
newCerts[d.Name] = &cert
if len(domainErrs) != 0 {
errs = append(errs, fmt.Errorf("invalid domain %s: %w", d.Name, errors.Join(domainErrs...)))
}
}
if len(errs) != 0 {
return fmt.Errorf("can't compile config to routing map: %w", errors.Join(errs...))
}
rtr.lock.Lock()
rtr.routes = newMap
rtr.tlsCerts = newCerts
rtr.lock.Unlock()
return nil
}
func (rtr *Router) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
rtr.lock.RLock()
cert, ok := rtr.tlsCerts[hello.ServerName]
rtr.lock.RUnlock()
if !ok {
return nil, ErrNoCert
}
return cert, nil
}
func (rtr *Router) loadConfig() error {
slog.Info("reloading config", "fname", rtr.opts.ConfigFname)
var cfg config.Toplevel
if err := hclsimple.DecodeFile(rtr.opts.ConfigFname, nil, &cfg); err != nil {
return err
}
if err := cfg.Valid(); err != nil {
return err
}
if err := rtr.setConfig(cfg); err != nil {
return err
}
slog.Info("done!")
return nil
}
func (rtr *Router) backgroundReloadConfig(ctx context.Context) {
t := time.NewTicker(time.Hour)
defer t.Stop()
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGHUP)
for {
select {
case <-ctx.Done():
return
case <-t.C:
if err := rtr.loadConfig(); err != nil {
slog.Error("can't reload config", "fname", rtr.opts.ConfigFname, "err", err)
}
case <-ch:
if err := rtr.loadConfig(); err != nil {
slog.Error("can't reload config", "fname", rtr.opts.ConfigFname, "err", err)
}
}
}
}
func NewRouter(c config.Toplevel) (*Router, error) {
result := &Router{
routes: map[string]http.Handler{},
}
if err := result.setConfig(c); err != nil {
return nil, err
}
return result, nil
}
func (rtr *Router) HandleHTTP(ctx context.Context, ln net.Listener) error {
srv := http.Server{
Handler: rtr,
ErrorLog: internal.GetFilteredHTTPLogger(),
}
go func(ctx context.Context) {
<-ctx.Done()
srv.Close()
}(ctx)
return srv.Serve(ln)
}
func (rtr *Router) HandleHTTPS(ctx context.Context, ln net.Listener) error {
tc := &tls.Config{
GetCertificate: rtr.GetCertificate,
}
srv := &http.Server{
Handler: rtr,
ErrorLog: internal.GetFilteredHTTPLogger(),
TLSConfig: tc,
}
go func(ctx context.Context) {
<-ctx.Done()
srv.Close()
}(ctx)
fingerprint.ApplyTLSFingerprinter(srv)
return srv.ServeTLS(ln, "", "")
}
func (rtr *Router) ListenAndServeMetrics(ctx context.Context, addr string) error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("(metrics) can't bind to tcp %s: %w", addr, err)
}
defer ln.Close()
go func(ctx context.Context) {
<-ctx.Done()
ln.Close()
}(ctx)
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/readyz", readyz)
mux.HandleFunc("/healthz", healthz)
slog.Info("listening", "for", "metrics", "bind", addr)
srv := http.Server{
Addr: addr,
Handler: mux,
ErrorLog: internal.GetFilteredHTTPLogger(),
}
go func(ctx context.Context) {
<-ctx.Done()
srv.Close()
}(ctx)
return srv.Serve(ln)
}
func (rtr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var host = r.Host
if strings.Contains(host, ":") {
host, _, _ = net.SplitHostPort(host)
}
var h http.Handler
var ok bool
ja4hFP := ja4h.JA4H(r)
slog.Info("got request", "method", r.Method, "host", host, "path", r.URL.Path)
rtr.lock.RLock()
h, ok = rtr.routes[host]
rtr.lock.RUnlock()
if !ok {
unresolvedRequests.Inc()
http.NotFound(w, r) // TODO(Xe): brand this
return
}
r.Header.Set("X-Http-Ja4h-Fingerprint", ja4hFP)
if fp := fingerprint.GetTLSFingerprint(r); fp != nil {
if ja3n := fp.JA3N(); ja3n != nil {
r.Header.Set("X-Tls-Ja3n-Fingerprint", ja3n.String())
}
if ja4 := fp.JA4(); ja4 != nil {
r.Header.Set("X-Tls-Ja4-Fingerprint", ja4.String())
}
}
if tcpFP := fingerprint.GetTCPFingerprint(r); tcpFP != nil {
r.Header.Set("X-Tcp-Ja4t-Fingerprint", tcpFP.String())
}
m := httpsnoop.CaptureMetrics(h, w, r)
requestsPerDomain.WithLabelValues(host, r.Method, fmt.Sprint(m.Code)).Inc()
responseTime.WithLabelValues(host).Observe(float64(m.Duration.Milliseconds()))
slog.Info("request completed", "host", host, "method", r.Method, "response_code", m.Code, "duration_ms", m.Duration.Milliseconds())
}

View File

@@ -1,319 +0,0 @@
package entrypoint
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/TecharoHQ/anubis/cmd/osiris/internal/config"
"github.com/hashicorp/hcl/v2/hclsimple"
)
func loadConfig(t *testing.T, fname string) config.Toplevel {
t.Helper()
var cfg config.Toplevel
if err := hclsimple.DecodeFile(fname, nil, &cfg); err != nil {
t.Fatalf("can't read configuration file %s: %v", fname, err)
}
if err := cfg.Valid(); err != nil {
t.Errorf("configuration file %s is invalid: %v", "./testdata/selfsigned.hcl", err)
}
return cfg
}
func newRouter(t *testing.T, cfg config.Toplevel) *Router {
t.Helper()
rtr, err := NewRouter(cfg)
if err != nil {
t.Fatal(err)
}
return rtr
}
func TestNewRouter(t *testing.T) {
cfg := loadConfig(t, "./testdata/good/selfsigned.hcl")
rtr := newRouter(t, cfg)
srv := httptest.NewServer(rtr)
defer srv.Close()
}
func TestNewRouterFails(t *testing.T) {
cfg := loadConfig(t, "./testdata/good/selfsigned.hcl")
cfg.Domains = append(cfg.Domains, config.Domain{
Name: "test1.internal",
TLS: config.TLS{
Cert: "./testdata/tls/invalid.crt",
Key: "./testdata/tls/invalid.key",
},
Target: cfg.Domains[0].Target,
HealthTarget: cfg.Domains[0].HealthTarget,
})
rtr, err := NewRouter(cfg)
if err == nil {
t.Fatal("wanted an error but got none")
}
srv := httptest.NewServer(rtr)
defer srv.Close()
}
func TestRouterSetConfig(t *testing.T) {
for _, tt := range []struct {
name string
configFname string
mutation func(cfg config.Toplevel) config.Toplevel
err error
}{
{
name: "basic",
configFname: "./testdata/good/selfsigned.hcl",
mutation: func(cfg config.Toplevel) config.Toplevel {
return cfg
},
},
{
name: "all schemes",
configFname: "./testdata/good/selfsigned.hcl",
mutation: func(cfg config.Toplevel) config.Toplevel {
cfg.Domains = append(cfg.Domains, config.Domain{
Name: "http.internal",
TLS: cfg.Domains[0].TLS,
Target: "http://[::1]:3000",
HealthTarget: cfg.Domains[0].HealthTarget,
})
cfg.Domains = append(cfg.Domains, config.Domain{
Name: "https.internal",
TLS: cfg.Domains[0].TLS,
Target: "https://[::1]:3000",
HealthTarget: cfg.Domains[0].HealthTarget,
})
cfg.Domains = append(cfg.Domains, config.Domain{
Name: "h2c.internal",
TLS: cfg.Domains[0].TLS,
Target: "h2c://[::1]:3000",
HealthTarget: cfg.Domains[0].HealthTarget,
})
cfg.Domains = append(cfg.Domains, config.Domain{
Name: "unix.internal",
TLS: cfg.Domains[0].TLS,
Target: "unix://foo.sock",
HealthTarget: cfg.Domains[0].HealthTarget,
})
return cfg
},
},
{
name: "invalid TLS",
configFname: "./testdata/good/selfsigned.hcl",
mutation: func(cfg config.Toplevel) config.Toplevel {
cfg.Domains = append(cfg.Domains, config.Domain{
Name: "test1.internal",
TLS: config.TLS{
Cert: "./testdata/tls/invalid.crt",
Key: "./testdata/tls/invalid.key",
},
Target: cfg.Domains[0].Target,
HealthTarget: cfg.Domains[0].HealthTarget,
})
return cfg
},
err: ErrInvalidTLSKeypair,
},
{
name: "target is not a valid URL",
configFname: "./testdata/good/selfsigned.hcl",
mutation: func(cfg config.Toplevel) config.Toplevel {
cfg.Domains = append(cfg.Domains, config.Domain{
Name: "test1.internal",
TLS: cfg.Domains[0].TLS,
Target: "http://[::1:443",
HealthTarget: cfg.Domains[0].HealthTarget,
})
return cfg
},
err: ErrTargetInvalid,
},
{
name: "invalid target scheme",
configFname: "./testdata/good/selfsigned.hcl",
mutation: func(cfg config.Toplevel) config.Toplevel {
cfg.Domains = append(cfg.Domains, config.Domain{
Name: "test1.internal",
TLS: cfg.Domains[0].TLS,
Target: "foo://",
HealthTarget: cfg.Domains[0].HealthTarget,
})
return cfg
},
err: ErrNoHandler,
},
} {
t.Run(tt.name, func(t *testing.T) {
cfg := loadConfig(t, tt.configFname)
rtr := newRouter(t, cfg)
cfg = tt.mutation(cfg)
if err := rtr.setConfig(cfg); !errors.Is(err, tt.err) {
t.Logf("want: %v", tt.err)
t.Logf("got: %v", err)
t.Error("got wrong error from rtr.setConfig function")
}
})
}
}
type ackHandler struct {
ack bool
}
func (ah *ackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ah.ack = true
fmt.Fprintln(w, "OK")
}
func (ah *ackHandler) Reset() {
ah.ack = false
}
func newUnixServer(t *testing.T, h http.Handler) string {
sockName := filepath.Join(t.TempDir(), "s")
ln, err := net.Listen("unix", sockName)
if err != nil {
t.Fatalf("can't listen on %s: %v", sockName, err)
}
t.Cleanup(func() {
ln.Close()
os.Remove(sockName)
})
go func(ctx context.Context) {
srv := &http.Server{
Handler: h,
}
go func() {
<-ctx.Done()
srv.Close()
}()
srv.Serve(ln)
}(t.Context())
return "unix://" + sockName
}
func TestRouterGetCertificate(t *testing.T) {
cfg := loadConfig(t, "./testdata/good/selfsigned.hcl")
rtr := newRouter(t, cfg)
for _, tt := range []struct {
domainName string
err error
}{
{
domainName: "osiris.local.cetacean.club",
},
{
domainName: "whacky-fun.local",
err: ErrNoCert,
},
} {
t.Run(tt.domainName, func(t *testing.T) {
if _, err := rtr.GetCertificate(&tls.ClientHelloInfo{ServerName: tt.domainName}); !errors.Is(err, tt.err) {
t.Logf("want: %v", tt.err)
t.Logf("got: %v", err)
t.Error("got wrong error from rtr.GetCertificate")
}
})
}
}
func TestRouterServeAllProtocols(t *testing.T) {
cfg := loadConfig(t, "./testdata/good/all_protocols.hcl")
httpAckHandler := &ackHandler{}
httpsAckHandler := &ackHandler{}
h2cAckHandler := &ackHandler{}
unixAckHandler := &ackHandler{}
httpSrv := httptest.NewServer(httpAckHandler)
httpsSrv := httptest.NewTLSServer(httpsAckHandler)
h2cSrv := newH2cServer(t, h2cAckHandler)
unixPath := newUnixServer(t, unixAckHandler)
cfg.Domains[0].Target = httpSrv.URL
cfg.Domains[1].Target = httpsSrv.URL
cfg.Domains[2].Target = strings.ReplaceAll(h2cSrv.URL, "http:", "h2c:")
cfg.Domains[3].Target = unixPath
// enc := json.NewEncoder(os.Stderr)
// enc.SetIndent("", " ")
// enc.Encode(cfg)
rtr := newRouter(t, cfg)
cli := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
t.Run("plain http", func(t *testing.T) {
ln, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
ln.Close()
})
go rtr.HandleHTTP(t.Context(), ln)
serverURL := "http://" + ln.Addr().String()
t.Log(serverURL)
for _, d := range cfg.Domains {
t.Run(d.Name, func(t *testing.T) {
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, serverURL, nil)
if err != nil {
t.Fatal(err)
}
req.Host = d.Name
resp, err := cli.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("wrong status code %d", resp.StatusCode)
}
})
}
})
}

View File

@@ -1,15 +0,0 @@
bind {
http = ":65530"
https = ":65531"
metrics = ":65532"
}
domain "osiris.local.cetacean.club" {
tls {
cert = "./testdata/invalid.crt"
key = "./testdata/invalid.key"
}
target = "http://localhost:3000"
health_target = "http://localhost:9091/healthz"
}

View File

@@ -1,46 +0,0 @@
bind {
http = ":65520"
https = ":65521"
metrics = ":65522"
}
domain "http.internal" {
tls {
cert = "./testdata/selfsigned.crt"
key = "./testdata/selfsigned.key"
}
target = "http://localhost:65510" # XXX(Xe) this is overwritten
health_target = "http://localhost:9091/healthz"
}
domain "https.internal" {
tls {
cert = "./testdata/selfsigned.crt"
key = "./testdata/selfsigned.key"
}
target = "https://localhost:65511" # XXX(Xe) this is overwritten
insecure_skip_verify = true
health_target = "http://localhost:9091/healthz"
}
domain "h2c.internal" {
tls {
cert = "./testdata/selfsigned.crt"
key = "./testdata/selfsigned.key"
}
target = "h2c://localhost:65511" # XXX(Xe) this is overwritten
health_target = "http://localhost:9091/healthz"
}
domain "unix.internal" {
tls {
cert = "./testdata/selfsigned.crt"
key = "./testdata/selfsigned.key"
}
target = "http://localhost:65511" # XXX(Xe) this is overwritten
health_target = "http://localhost:9091/healthz"
}

View File

@@ -1,15 +0,0 @@
bind {
http = ":65530"
https = ":65531"
metrics = ":65532"
}
domain "osiris.local.cetacean.club" {
tls {
cert = "./testdata/selfsigned.crt"
key = "./testdata/selfsigned.key"
}
target = "http://localhost:3000"
health_target = "http://localhost:9091/healthz"
}

View File

@@ -1,11 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIBnzCCAVGgAwIBAgIUOLTjSYOjFk00IemtFTC4oEZs988wBQYDK2VwMEUxCzAJ
BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjUwNzE4MjEyNDIzWhcNMjUwODE3MjEyNDIz
WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAPHphABS15+4VV6R1
vYzBQYIycQmOmlbA8QcfwzuB2VajUzBRMB0GA1UdDgQWBBT2s+MQ4AR6cbK4V0+d
XZnok1orhDAfBgNVHSMEGDAWgBT2s+MQ4AR6cbK4V0+dXZnok1orhDAPBgNVHRMB
Af8EBTADAQH/MAUGAytlcANBAOdoJbRMnHmkEETzVtXP+jkAI9yQNRXujnglApGP
8I5pvIYVgYCgoQrnb4haVWFldHM1T9H698n19e/egfFb+w4=
-----END CERTIFICATE-----

View File

@@ -1,3 +0,0 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIBop42tiZ0yzhaKo9NAc0PlAyBsE8NAE0i9Z7s2lgZuR
-----END PRIVATE KEY-----

View File

@@ -1,43 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/TecharoHQ/anubis"
"github.com/TecharoHQ/anubis/cmd/osiris/internal/entrypoint"
"github.com/TecharoHQ/anubis/internal"
"github.com/facebookgo/flagenv"
)
var (
configFname = flag.String("config", "./osiris.hcl", "Configuration file (HCL), see docs")
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
versionFlag = flag.Bool("version", false, "if true, show version information then quit")
)
func main() {
flagenv.Parse()
flag.Parse()
if *versionFlag {
fmt.Println("Osiris", anubis.Version)
return
}
internal.InitSlog(*slogLevel)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
if err := entrypoint.Main(ctx, entrypoint.Options{
ConfigFname: *configFname,
}); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}

View File

@@ -1,15 +0,0 @@
bind {
http = ":3004"
https = ":3005"
metrics = ":9091"
}
domain "osiris.local.cetacean.club" {
tls {
cert = "./internal/config/testdata/tls/selfsigned.crt"
key = "./internal/config/testdata/tls/selfsigned.key"
}
target = "http://localhost:3000"
health_target = "http://localhost:9091/healthz"
}

View File

@@ -74,25 +74,6 @@ bots:
weight:
adjust: 10
# ## System load based checks.
# # If the system is under high load, add weight.
# - name: high-load-average
# action: WEIGH
# expression: load_1m >= 10.0 # make sure to end the load comparison in a .0
# weight:
# adjust: 20
## If your backend service is running on the same operating system as Anubis,
## you can uncomment this rule to make the challenge easier when the system is
## under low load.
##
## If it is not, remove weight.
# - name: low-load-average
# action: WEIGH
# expression: load_15m <= 4.0 # make sure to end the load comparison in a .0
# weight:
# adjust: -10
# Generic catchall rule
- name: generic-browser
user_agent_regex: >-

View File

@@ -1,223 +0,0 @@
- name: uptime-robot
user_agent_regex: UptimeRobot
action: ALLOW
# https://api.uptimerobot.com/meta/ips
remote_addresses: [
"3.12.251.153/32",
"3.20.63.178/32",
"3.77.67.4/32",
"3.79.134.69/32",
"3.105.133.239/32",
"3.105.190.221/32",
"3.133.226.214/32",
"3.149.57.90/32",
"3.212.128.62/32",
"5.161.61.238/32",
"5.161.73.160/32",
"5.161.75.7/32",
"5.161.113.195/32",
"5.161.117.52/32",
"5.161.177.47/32",
"5.161.194.92/32",
"5.161.215.244/32",
"5.223.43.32/32",
"5.223.53.147/32",
"5.223.57.22/32",
"18.116.205.62/32",
"18.180.208.214/32",
"18.192.166.72/32",
"18.193.252.127/32",
"24.144.78.39/32",
"24.144.78.185/32",
"34.198.201.66/32",
"45.55.123.175/32",
"45.55.127.146/32",
"49.13.24.81/32",
"49.13.130.29/32",
"49.13.134.145/32",
"49.13.164.148/32",
"49.13.167.123/32",
"52.15.147.27/32",
"52.22.236.30/32",
"52.28.162.93/32",
"52.59.43.236/32",
"52.87.72.16/32",
"54.64.67.106/32",
"54.79.28.129/32",
"54.87.112.51/32",
"54.167.223.174/32",
"54.249.170.27/32",
"63.178.84.147/32",
"64.225.81.248/32",
"64.225.82.147/32",
"69.162.124.227/32",
"69.162.124.235/32",
"69.162.124.238/32",
"78.46.190.63/32",
"78.46.215.1/32",
"78.47.98.55/32",
"78.47.173.76/32",
"88.99.80.227/32",
"91.99.101.207/32",
"128.140.41.193/32",
"128.140.106.114/32",
"129.212.132.140/32",
"134.199.240.137/32",
"138.197.53.117/32",
"138.197.53.138/32",
"138.197.54.143/32",
"138.197.54.247/32",
"138.197.63.92/32",
"139.59.50.44/32",
"142.132.180.39/32",
"143.198.249.237/32",
"143.198.250.89/32",
"143.244.196.21/32",
"143.244.196.211/32",
"143.244.221.177/32",
"144.126.251.21/32",
"146.190.9.187/32",
"152.42.149.135/32",
"157.90.155.240/32",
"157.90.156.63/32",
"159.69.158.189/32",
"159.223.243.219/32",
"161.35.247.201/32",
"167.99.18.52/32",
"167.235.143.113/32",
"168.119.53.160/32",
"168.119.96.239/32",
"168.119.123.75/32",
"170.64.250.64/32",
"170.64.250.132/32",
"170.64.250.235/32",
"178.156.181.172/32",
"178.156.184.20/32",
"178.156.185.127/32",
"178.156.185.231/32",
"178.156.187.238/32",
"178.156.189.113/32",
"178.156.189.249/32",
"188.166.201.79/32",
"206.189.241.133/32",
"209.38.49.1/32",
"209.38.49.206/32",
"209.38.49.226/32",
"209.38.51.43/32",
"209.38.53.7/32",
"209.38.124.252/32",
"216.144.248.18/31",
"216.144.248.21/32",
"216.144.248.22/31",
"216.144.248.24/30",
"216.144.248.28/31",
"216.144.248.30/32",
"216.245.221.83/32",
"2400:6180:10:200::56a0:b000/128",
"2400:6180:10:200::56a0:c000/128",
"2400:6180:10:200::56a0:e000/128",
"2400:6180:100:d0::94b6:4001/128",
"2400:6180:100:d0::94b6:5001/128",
"2400:6180:100:d0::94b6:7001/128",
"2406:da14:94d:8601:9d0d:7754:bedf:e4f5/128",
"2406:da14:94d:8601:b325:ff58:2bba:7934/128",
"2406:da14:94d:8601:db4b:c5ac:2cbe:9a79/128",
"2406:da1c:9c8:dc02:7ae1:f2ea:ab91:2fde/128",
"2406:da1c:9c8:dc02:7db9:f38b:7b9f:402e/128",
"2406:da1c:9c8:dc02:82b2:f0fd:ee96:579/128",
"2600:1f16:775:3a00:ac3:c5eb:7081:942e/128",
"2600:1f16:775:3a00:37bf:6026:e54a:f03a/128",
"2600:1f16:775:3a00:3f24:5bb0:95d7:5a6b/128",
"2600:1f16:775:3a00:8c2c:2ba6:778f:5be5/128",
"2600:1f16:775:3a00:91ac:3120:ff38:92b5/128",
"2600:1f16:775:3a00:dbbe:36b0:3c45:da32/128",
"2600:1f18:179:f900:71:af9a:ade7:d772/128",
"2600:1f18:179:f900:2406:9399:4ae6:c5d3/128",
"2600:1f18:179:f900:4696:7729:7bb3:f52f/128",
"2600:1f18:179:f900:4b7d:d1cc:2d10:211/128",
"2600:1f18:179:f900:5c68:91b6:5d75:5d7/128",
"2600:1f18:179:f900:e8dd:eed1:a6c:183b/128",
"2604:a880:800:14:0:1:68ba:d000/128",
"2604:a880:800:14:0:1:68ba:e000/128",
"2604:a880:800:14:0:1:68bb:0/128",
"2604:a880:800:14:0:1:68bb:1000/128",
"2604:a880:800:14:0:1:68bb:3000/128",
"2604:a880:800:14:0:1:68bb:4000/128",
"2604:a880:800:14:0:1:68bb:5000/128",
"2604:a880:800:14:0:1:68bb:6000/128",
"2604:a880:800:14:0:1:68bb:7000/128",
"2604:a880:800:14:0:1:68bb:a000/128",
"2604:a880:800:14:0:1:68bb:b000/128",
"2604:a880:800:14:0:1:68bb:c000/128",
"2604:a880:800:14:0:1:68bb:d000/128",
"2604:a880:800:14:0:1:68bb:e000/128",
"2604:a880:800:14:0:1:68bb:f000/128",
"2607:ff68:107::4/128",
"2607:ff68:107::14/128",
"2607:ff68:107::33/128",
"2607:ff68:107::48/127",
"2607:ff68:107::50/125",
"2607:ff68:107::58/127",
"2607:ff68:107::60/128",
"2a01:4f8:c0c:83fa::1/128",
"2a01:4f8:c17:42e4::1/128",
"2a01:4f8:c2c:9fc6::1/128",
"2a01:4f8:c2c:beae::1/128",
"2a01:4f8:1c1a:3d53::1/128",
"2a01:4f8:1c1b:4ef4::1/128",
"2a01:4f8:1c1b:5b5a::1/128",
"2a01:4f8:1c1b:7ecc::1/128",
"2a01:4f8:1c1c:11aa::1/128",
"2a01:4f8:1c1c:5353::1/128",
"2a01:4f8:1c1c:7240::1/128",
"2a01:4f8:1c1c:a98a::1/128",
"2a01:4f8:c012:c60e::1/128",
"2a01:4f8:c013:c18::1/128",
"2a01:4f8:c013:34c0::1/128",
"2a01:4f8:c013:3b0f::1/128",
"2a01:4f8:c013:3c52::1/128",
"2a01:4f8:c013:3c53::1/128",
"2a01:4f8:c013:3c54::1/128",
"2a01:4f8:c013:3c55::1/128",
"2a01:4f8:c013:3c56::1/128",
"2a01:4ff:f0:bfd::1/128",
"2a01:4ff:f0:2219::1/128",
"2a01:4ff:f0:3e03::1/128",
"2a01:4ff:f0:5f80::1/128",
"2a01:4ff:f0:7fad::1/128",
"2a01:4ff:f0:9c5f::1/128",
"2a01:4ff:f0:b2f2::1/128",
"2a01:4ff:f0:b6f1::1/128",
"2a01:4ff:f0:d283::1/128",
"2a01:4ff:f0:d3cd::1/128",
"2a01:4ff:f0:e516::1/128",
"2a01:4ff:f0:e9cf::1/128",
"2a01:4ff:f0:eccb::1/128",
"2a01:4ff:f0:efd1::1/128",
"2a01:4ff:f0:fdc7::1/128",
"2a01:4ff:2f0:193c::1/128",
"2a01:4ff:2f0:27de::1/128",
"2a01:4ff:2f0:3b3a::1/128",
"2a03:b0c0:2:f0::bd91:f001/128",
"2a03:b0c0:2:f0::bd92:1/128",
"2a03:b0c0:2:f0::bd92:1001/128",
"2a03:b0c0:2:f0::bd92:2001/128",
"2a03:b0c0:2:f0::bd92:4001/128",
"2a03:b0c0:2:f0::bd92:5001/128",
"2a03:b0c0:2:f0::bd92:6001/128",
"2a03:b0c0:2:f0::bd92:7001/128",
"2a03:b0c0:2:f0::bd92:8001/128",
"2a03:b0c0:2:f0::bd92:9001/128",
"2a03:b0c0:2:f0::bd92:a001/128",
"2a03:b0c0:2:f0::bd92:b001/128",
"2a03:b0c0:2:f0::bd92:c001/128",
"2a03:b0c0:2:f0::bd92:e001/128",
"2a03:b0c0:2:f0::bd92:f001/128",
"2a05:d014:1815:3400:6d:9235:c1c0:96ad/128",
"2a05:d014:1815:3400:654f:bd37:724c:212b/128",
"2a05:d014:1815:3400:90b4:4ef9:5631:b170/128",
"2a05:d014:1815:3400:9779:d8e9:100a:9642/128",
"2a05:d014:1815:3400:af29:e95e:64ff:df81/128",
"2a05:d014:1815:3400:c7d6:f7f3:6cc1:30d1/128",
"2a05:d014:1815:3400:d784:e5dd:8e0:67cb/128",
]

View File

@@ -1,32 +0,0 @@
variable "ALPINE_VERSION" { default = "3.22" }
variable "GITHUB_SHA" { default = "devel" }
variable "VERSION" { default = "devel-docker" }
group "default" {
targets = [
"osiris",
]
}
target "osiris" {
args = {
ALPINE_VERSION = "3.22"
VERSION = "${VERSION}"
}
context = "."
dockerfile = "./docker/osiris.Dockerfile"
platforms = [
"linux/amd64",
"linux/arm64",
"linux/arm/v7",
"linux/ppc64le",
"linux/riscv64",
]
pull = true
sbom = true
provenance = true
tags = [
"ghcr.io/techarohq/anubis/osiris:${VERSION}",
"ghcr.io/techarohq/anubis/osiris:main"
]
}

View File

@@ -1,30 +0,0 @@
ARG ALPINE_VERSION=edge
FROM --platform=${BUILDPLATFORM} alpine:${ALPINE_VERSION} AS build
RUN apk -U add go nodejs git build-base git npm bash zstd brotli gzip
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/root/.cache --mount=type=cache,target=/root/go go mod download
COPY package.json package-lock.json ./
RUN npm ci
ARG TARGETOS
ARG TARGETARCH
ARG VERSION=devel-docker
COPY . .
RUN --mount=type=cache,target=/root/.cache --mount=type=cache,target=/root/go GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 GOARM=7 go build -gcflags "all=-N -l" -o /app/bin/osiris -ldflags "-s -w -extldflags -static -X github.com/TecharoHQ/anubis.Version=${VERSION}" ./cmd/osiris
FROM alpine:${ALPINE_VERSION} AS run
WORKDIR /app
RUN apk -U add ca-certificates mailcap
COPY --from=build /app/bin/osiris /app/bin/osiris
CMD ["/app/bin/osiris"]
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"

View File

@@ -19,3 +19,5 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Kubernetes manifests
/manifest

View File

@@ -5,7 +5,6 @@ COPY . .
RUN npm ci && npm run build
FROM ghcr.io/xe/nginx-micro
COPY --from=build /app/build /www
COPY ./manifest/cfg/nginx/nginx.conf /conf
FROM docker.io/library/nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"

View File

@@ -1,105 +0,0 @@
---
slug: incident/TI-20250709-0001
title: "TI-20250709-0001: IPv4 traffic failures for Techaro services"
authors: [xe]
tags: [incident]
image: ./window-portal.jpg
---
![](./window-portal.jpg)
Techaro services were down for IPv4 traffic on July 9th, 2025. This blogpost is a report of what happened, what actions were taken to resolve the situation, and what actions are being done in the near future to prevent this problem. Enjoy this incident report!
{/* truncate */}
:::note
In other companies, this kind of documentation would be kept internal. At Techaro, we believe that you deserve radical candor and the truth. As such, we are proving our lofty words with actions by publishing details about how things go wrong publicly.
Everything past this point follows my standard incident root cause meeting template.
:::
This incident report will focus on the services affected, timeline of what happened at which stage of the incident, where we got lucky, the root cause analysis, and what action items are being planned or taken to prevent this from happening in the future.
## Timeline
All events take place on July 9th, 2025.
| Time (UTC) | Description |
| :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 12:32 | Uptime Kuma reports that another unrelated website on the same cluster was timing out. |
| 12:33 | Uptime Kuma reports that Thoth's production endpoint is failing gRPC health checks. |
| 12:35 | Investigation begins, [announcement made on Xe's Bluesky](https://bsky.app/profile/xeiaso.net/post/3ltjtdczpwc2x) due to the impact including their personal blog. |
| 12:39 | `nginx-ingress` logs on the production cluster show IPv6 traffic but an abrupt cutoff in IPv4 traffic around 12:32 UTC. Ticket is opened with the hosting provider. |
| 12:41 | IPv4 traffic resumes long enough for Uptime Kuma to report uptime, but then immediately fails again. |
| 12:46 | IPv4 traffic resumes long enough for Uptime Kuma to report uptime, but then immediately fails again. (repeat instances of this have been scrubbed, but it happened about every 5-10 minutes) |
| 12:48 | First reply from the hosting provider. |
| 12:57 | Reply to hosting provider, ask to reboot the load balancer. |
| 13:00 | Incident responder because busy due to a meeting under the belief that the downtime was out of their control and that uptime monitoring software would let them know if it came back up. |
| 13:20 | Incident responder ended meeting and went back to monitoring downtime and preparing this document. |
| 13:34 | IPv4 traffic starts to show up in the `ingress-nginx` logs. |
| 13:35 | All services start to report healthy. Incident status changes to monitoring. |
| 13:48 | Incident closed. |
| 14:07 | Incident re-opened. Issues seem to be manifesting as BGP issues in the upstream provider. |
| 14:10 | IPv4 traffic resumes and then stops. |
| 14:18 | IPv4 traffic resumes again. Incident status changes to monitoring. |
| 14:40 | Incident closed. |
## Services affected
| Service name | User impact |
| :-------------------------------------------------- | :----------------- |
| [Anubis Docs](https://anubis.techaro.lol) (IPv4) | Connection timeout |
| [Anubis Docs](https://anubis.techaro.lol) (IPv6) | None |
| [Thoth](/docs/admin/thoth/) (IPv4) | Connection timeout |
| [Thoth](/docs/admin/thoth/) (IPv6) | None |
| Other websites colocated on the same cluster (IPv4) | Connection timeout |
| Other websites colocated on the same cluster (IPv6) | None |
## Root cause analysis
In simplify server management, Techaro runs a [Kubernetes](https://kubernetes.io/) cluster on [Vultr VKE](https://www.vultr.com/kubernetes/) (Vultr Kubernetes Engine). When you do this, Vultr needs to provision a [load balancer](https://docs.vultr.com/how-to-use-a-vultr-load-balancer-with-vke) to bridge the gap between the outside world and the Kubernetes world, kinda like this:
```mermaid
---
title: Overall architecture
---
flowchart LR
UT(User Traffic)
subgraph Provider Infrastructure
LB[Load Balancer]
end
subgraph Kubernetes
IN(ingress-nginx)
TH(Thoth)
AN(Anubis Docs)
OS(Other sites)
IN --> TH
IN --> AN
IN --> OS
end
UT --> LB --> IN
```
Techaro controls everything inside the Kubernetes side of that diagram. Anything else is out of our control. That load balancer is routed to the public internet via [Border Gateway Protocol (BGP)](https://en.wikipedia.org/wiki/Border_Gateway_Protocol).
If there is an interruption with the BGP sessions in the upstream provider, this can manifest as things either not working or inconsistently working. This is made more difficult by the fact that the IPv4 and IPv6 internets are technically separate networks. With this in mind, it's very possible to have IPv4 traffic fail but not IPv6 traffic.
The root cause is that the hosting provider we use for production services had flapping IPv4 BGP sessions in its Toronto region. When this happens all we can do is open a ticket and wait for it to come back up.
## Where we got lucky
The Uptime Kuma instance that caught this incident runs on an IPv4-only network. If it was dual stack, this would not have been caught as quickly.
The `ingress-nginx` logs print IP addresses of remote clients to the log feed. If this was not the case, it would be much more difficult to find this error.
## Action items
- A single instance of downtime like this is not enough reason to move providers. Moving providers because of this is thus out of scope.
- Techaro needs a status page hosted on a different cloud provider than is used for the production cluster (`TecharoHQ/TODO#6`).
- Health checks for IPv4 and IPv6 traffic need to be created (`TecharoHQ/TODO#7`).
- Remove the requirement for [Anubis to pass Thoth health checks before it can start if Thoth is enabled](https://github.com/TecharoHQ/anubis/pull/794).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -13,129 +13,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- This changes the project to: -->
- Expired records are now properly removed from bbolt databases ([#848](https://github.com/TecharoHQ/anubis/pull/848)).
- Fix hanging on service restart ([#853](https://github.com/TecharoHQ/anubis/issues/853))
### Added
Anubis now supports these new languages:
- [Czech](https://github.com/TecharoHQ/anubis/pull/849)
Anubis now supports the [`missingHeader`](./admin/configuration/expressions.mdx#missingHeader) to assert the absence of headers in requests.
## v1.21.0: Minfilia Warde
> Please, be at ease. You are among friends here.
In this release, Anubis becomes internationalized, gains the ability to use system load as input to issuing challenges, finally fixes the "invalid response" after "success" bug, and more! Please read these notes before upgrading as the changes are big enough that administrators should take action to ensure that the upgrade goes smoothly.
### Big ticket changes
The biggest change is that the ["invalid response" after "success" bug](https://github.com/TecharoHQ/anubis/issues/564) is now finally fixed for good by totally rewriting how Anubis' challenge issuance flow works. Instead of generating challenge strings from request metadata (under the assumption that the values being compared against are stable), Anubis now generates random data for each challenge. This data is stored in the active [storage backend](./admin/policies.mdx#storage-backends) for up to 30 minutes. This also fixes [#746](https://github.com/TecharoHQ/anubis/issues/746) and other similar instances of this issue.
In order to reduce confusion, the "Success" interstitial that shows up when you pass a proof of work challenge has been removed.
#### Storage
Anubis now is able to store things persistently [in memory](./admin/policies.mdx#memory), [on the disk](./admin/policies.mdx#bbolt), or [in Valkey](./admin/policies.mdx#valkey) (this includes other compatible software). By default Anubis uses the in-memory backend. If you have an environment with mutable storage (even if it is temporary), be sure to configure the [`bbolt`](./admin/policies.mdx#bbolt) storage backend.
#### Localization
Anubis now supports localized responses. Locales can be added in [lib/localization/locales/](https://github.com/TecharoHQ/anubis/tree/main/lib/localization/locales). This release includes support for the following languages:
- [Brazilian Portugese](https://github.com/TecharoHQ/anubis/pull/726)
- [Chinese (Simplified)](https://github.com/TecharoHQ/anubis/pull/774)
- [Chinese (Traditional)](https://github.com/TecharoHQ/anubis/pull/759)
- English
- [Estonian](https://github.com/TecharoHQ/anubis/pull/783)
- [Filipino](https://github.com/TecharoHQ/anubis/pull/775)
- [French](https://github.com/TecharoHQ/anubis/pull/716)
- [German](https://github.com/TecharoHQ/anubis/pull/741)
- [Icelandic](https://github.com/TecharoHQ/anubis/pull/780)
- [Italian](https://github.com/TecharoHQ/anubis/pull/778)
- [Japanese](https://github.com/TecharoHQ/anubis/pull/772)
- [Spanish](https://github.com/TecharoHQ/anubis/pull/716)
- [Turkish](https://github.com/TecharoHQ/anubis/pull/751)
If facts or local regulations demand, you can set Anubis default language with the `FORCED_LANGUAGE` environment variable or the `--forced-language` command line argument:
```sh
FORCED_LANGUAGE=de
```
#### Load average
Anubis can dynamically take action [based on the system load average](./admin/configuration/expressions.mdx#using-the-system-load-average), allowing you to write rules like this:
```yaml
## System load based checks.
# If the system is under high load for the last minute, add weight.
- name: high-load-average
action: WEIGH
expression: load_1m >= 10.0 # make sure to end the load comparison in a .0
weight:
adjust: 20
# If it is not for the last 15 minutes, remove weight.
- name: low-load-average
action: WEIGH
expression: load_15m <= 4.0 # make sure to end the load comparison in a .0
weight:
adjust: -10
```
Something to keep in mind about system load average is that it is not aware of the number of cores the system has. If you have a 16 core system that has 16 processes running but none of them is hogging the CPU, then you will get a load average below 16. If you are in doubt, make your "high load" metric at least two times the number of CPU cores and your "low load" metric at least half of the number of CPU cores. For example:
| Kind | Core count | Load threshold |
| --------: | :--------- | :------------- |
| high load | 4 | `8.0` |
| low load | 4 | `2.0` |
| high load | 16 | `32.0` |
| low load | 16 | `8` |
Also keep in mind that this does not account for other kinds of latency like I/O latency. A system can have its web applications unresponsive due to high latency from a MySQL server but still have that web application server report a load near or at zero.
### Other features and fixes
There are a bunch of other assorted features and fixes too:
- Add `COOKIE_SECURE` option to set the cookie [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies)
- Sets cookie defaults to use [SameSite: None](https://web.dev/articles/samesite-cookies-explained)
- Determine the `BIND_NETWORK`/`--bind-network` value from the bind address ([#677](https://github.com/TecharoHQ/anubis/issues/677)).
- Implement localization system. Find locale files in lib/localization/locales/.
- Implement a [development container](https://containers.dev/) manifest to make contributions easier.
- Fix dynamic cookie domains functionality ([#731](https://github.com/TecharoHQ/anubis/pull/731))
- Add option for custom cookie prefix ([#732](https://github.com/TecharoHQ/anubis/pull/732))
- Make the [Open Graph](./admin/configuration/open-graph.mdx) subsystem and DNSBL subsystem use [storage backends](./admin/policies.mdx#storage-backends) instead of storing everything in memory by default.
- Add translation for German language ([#741](https://github.com/TecharoHQ/anubis/pull/741))
- Remove the "Success" interstitial after a proof of work challenge is concluded.
- Anubis now has the concept of [storage backends](./admin/policies.mdx#storage-backends). These allow you to change how Anubis stores temporary data (in memory, on the disk, or in Valkey). If you run Anubis in an environment where you have a low amount of memory available for Anubis (eg: less than 64 megabytes), be sure to configure the [`bbolt`](./admin/policies.mdx#bbolt) storage backend.
- The challenge issuance and validation process has been rewritten from scratch. Instead of generating challenge strings from request metadata (under the assumption that the values being compared against are stable), Anubis now generates random data for each challenge. This data is stored in the active [storage backend](./admin/policies.mdx#storage-backends) for up to 30 minutes. Fixes [#564](https://github.com/TecharoHQ/anubis/issues/564), [#746](https://github.com/TecharoHQ/anubis/issues/746), and other similar instances of this issue.
- Add option for forcing a specific language ([#742](https://github.com/TecharoHQ/anubis/pull/742))
- Add translation for Turkish language ([#751](https://github.com/TecharoHQ/anubis/pull/751))
- Allow [Common Crawl](https://commoncrawl.org/) by default so scrapers have less incentive to scrape
- The [bbolt storage backend](./admin/policies.mdx#bbolt) now runs its cleanup every hour instead of every five minutes.
- Don't block Anubis starting up if [Thoth](./admin/thoth.mdx) health checks fail.
- A race condition involving [opening two challenge pages at once in different tabs](https://github.com/TecharoHQ/anubis/issues/832) causing one of them to fail has been fixed.
- The "Try again" button on the error page has been fixed. Previously it meant "try the solution again" instead of "try the challenge again".
- In certain cases, a user could be stuck with a test cookie that is invalid, locking them out of the service for up to half an hour. This has been fixed with better validation of this case and clearing the cookie.
- Start exposing JA4H fingerprints for later use in CEL expressions.
- Add `/healthz` route for use in platform-based health checks.
### Potentially breaking changes
We try to introduce breaking changes as much as possible, but these are the changes that may be relevant for you as an administrator:
#### Challenge format change
Previously Anubis did no accounting for challenges that it issued. This means that if Anubis restarted during a client, the client would be able to proceed once Anubis came back online.
During the upgrade to v1.21.0 and when v1.21.0 (or later) restarts with the [in-memory storage backend](./admin/policies.mdx#memory), you may see a higher rate of failed challenges than normal. If this persists beyond a few minutes, [open an issue](https://github.com/TecharoHQ/anubis/issues/new).
If you are using the in-memory storage backend, please consider using [a different storage backend](./admin/policies.mdx#storage-backends).
#### Systemd service changes
The following potentially breaking change applies to native installs with systemd only:
Each instance of systemd service template now has a unique `RuntimeDirectory`, as opposed to each instance of the service sharing a `RuntimeDirectory`. This change was made to avoid [the `RuntimeDirectory` getting nuked any time one of the Anubis instances restarts](https://github.com/TecharoHQ/anubis/issues/748).
If you configured Anubis' unix sockets to listen on `/run/anubis/foo.sock` for instance `anubis@foo`, you will need to configure Anubis to listen on `/run/anubis/foo/foo.sock` and additionally configure your HTTP load balancer as appropriate.
If you configured Anubis' unix sockets to listen on `/run/anubis/foo.sock` for instance `anubis@foo`, you will need to configure Anubis to listen on `/run/anubis/foo/sock` and additionally configure your HTTP load balancer as appropriate.
If you need the legacy behaviour, install this [systemd unit dropin](https://www.flatcar.org/docs/latest/setup/systemd/drop-in-units/):
@@ -145,8 +44,6 @@ If you need the legacy behaviour, install this [systemd unit dropin](https://www
RuntimeDirectory=anubis
```
Just keep in mind that this will cause problems when Anubis restarts.
## v1.20.0: Thancred Waters
The big ticket items are as follows:

View File

@@ -77,7 +77,7 @@ For example, consider this rule:
For this rule, if a request comes in from `8.8.8.8` or `1.1.1.1`, Anubis will deny the request and return an error page.
### `all` blocks
#### `all` blocks
An `all` block that contains a list of expressions. If all expressions in the list return `true`, then the action specified in the rule will be taken. If any of the expressions in the list returns `false`, Anubis will move on to the next rule.
@@ -99,18 +99,15 @@ For this rule, if a request comes in matching [the signature of the `go get` com
Anubis exposes the following variables to expressions:
| Name | Type | Explanation | Example |
| :-------------- | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- |
| `headers` | `map[string, string]` | The [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) of the request being processed. | `{"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"}` |
| `host` | `string` | The [HTTP hostname](https://web.dev/articles/url-parts#host) the request is targeted to. | `anubis.techaro.lol` |
| `load_1m` | `double` | The current system load average over the last one minute. This is useful for making [load-based checks](#using-the-system-load-average). |
| `load_5m` | `double` | The current system load average over the last five minutes. This is useful for making [load-based checks](#using-the-system-load-average). |
| `load_15m` | `double` | The current system load average over the last fifteen minutes. This is useful for making [load-based checks](#using-the-system-load-average). |
| `method` | `string` | The [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods) in the request being processed. | `GET`, `POST`, `DELETE`, etc. |
| `path` | `string` | The [path](https://web.dev/articles/url-parts#pathname) of the request being processed. | `/`, `/api/memes/create` |
| `query` | `map[string, string]` | The [query parameters](https://web.dev/articles/url-parts#query) of the request being processed. | `?foo=bar` -> `{"foo": "bar"}` |
| `remoteAddress` | `string` | The IP address of the client. | `1.1.1.1` |
| `userAgent` | `string` | The [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent) string in the request being processed. | `Mozilla/5.0 Gecko/20100101 Firefox/137.0` |
| Name | Type | Explanation | Example |
| :-------------- | :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- |
| `headers` | `map[string, string]` | The [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) of the request being processed. | `{"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"}` |
| `host` | `string` | The [HTTP hostname](https://web.dev/articles/url-parts#host) the request is targeted to. | `anubis.techaro.lol` |
| `method` | `string` | The [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods) in the request being processed. | `GET`, `POST`, `DELETE`, etc. |
| `path` | `string` | The [path](https://web.dev/articles/url-parts#pathname) of the request being processed. | `/`, `/api/memes/create` |
| `query` | `map[string, string]` | The [query parameters](https://web.dev/articles/url-parts#query) of the request being processed. | `?foo=bar` -> `{"foo": "bar"}` |
| `remoteAddress` | `string` | The IP address of the client. | `1.1.1.1` |
| `userAgent` | `string` | The [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent) string in the request being processed. | `Mozilla/5.0 Gecko/20100101 Firefox/137.0` |
Of note: in many languages when you look up a key in a map and there is nothing there, the language will return some "falsy" value like `undefined` in JavaScript, `None` in Python, or the zero value of the type in Go. In CEL, if you try to look up a value that does not exist, execution of the expression will fail and Anubis will return an error.
@@ -123,7 +120,7 @@ In order to avoid this, make sure the header or query parameter you are testing
- 'path == "/index.php"'
- '"title" in query'
- '"action" in query'
- 'query["action"] == "history"'
- 'query["action"] == "history"
```
This rule throws a challenge if and only if all of the following conditions are true:
@@ -144,74 +141,12 @@ X-Real-Ip: 8.8.8.8
Anubis would return a challenge because all of those conditions are true.
### Using the system load average
In Unix-like systems (such as Linux), every process on the system has to wait its turn to be able to run. This means that as more processes on the system are running, they need to wait longer to be able to execute. The [load average](<https://en.wikipedia.org/wiki/Load_(computing)>) represents the number of processes that want to be able to run but can't run yet. This metric isn't the most reliable to identify a cause, but is great at helping to identify symptoms.
Anubis lets you use the system load average as an input to expressions so that you can make dynamic rules like "when the system is under a low amount of load, dial back the protection, but when it's under a lot of load, crank it up to the mix". This lets you get all of the blocking features of Anubis in the background but only really expose Anubis to users when the system is actively being attacked.
This is best combined with the [weight](../policies.mdx#request-weight) and [threshold](./thresholds.mdx) systems so that you can have Anubis dynamically respond to attacks. Consider these rules in the default configuration file:
```yaml
## System load based checks.
# If the system is under high load for the last minute, add weight.
- name: high-load-average
action: WEIGH
expression: load_1m >= 10.0 # make sure to end the load comparison in a .0
weight:
adjust: 20
# If it is not for the last 15 minutes, remove weight.
- name: low-load-average
action: WEIGH
expression: load_15m <= 4.0 # make sure to end the load comparison in a .0
weight:
adjust: -10
```
This combination of rules makes Anubis dynamically react to the system load and only kick in when the system is under attack.
Something to keep in mind about system load average is that it is not aware of the number of cores the system has. If you have a 16 core system that has 16 processes running but none of them is hogging the CPU, then you will get a load average below 16. If you are in doubt, make your "high load" metric at least two times the number of CPU cores and your "low load" metric at least half of the number of CPU cores. For example:
| Kind | Core count | Load threshold |
| --------: | :--------- | :------------- |
| high load | 4 | `8.0` |
| low load | 4 | `2.0` |
| high load | 16 | `32.0` |
| low load | 16 | `8` |
Also keep in mind that this does not account for other kinds of latency like I/O latency. A system can have its web applications unresponsive due to high latency from a MySQL server but still have that web application server report a load near or at zero.
## Functions exposed to Anubis expressions
Anubis expressions can be augmented with the following functions:
### `missingHeader`
Available in `bot` expressions.
```ts
function missingHeader(headers: Record<string, string>, key: string) bool
```
`missingHeader` returns `true` if the request does not contain a header. This is useful when you are trying to assert behavior such as:
```yaml
# Adds weight to old versions of Chrome
- name: old-chrome
action: WEIGH
weight:
adjust: 10
expression:
all:
- userAgent.matches("Chrome/[1-9][0-9]?\\.0\\.0\\.0")
- missingHeader(headers, "Sec-Ch-Ua")
```
### `randInt`
Available in all expressions.
```ts
function randInt(n: int): int;
```

View File

@@ -30,7 +30,7 @@ Effectively you have one trip through Apache to do TLS termination, a detour thr
:::note
These examples assume that you are using a setup where your Apache configuration is made up of a bunch of files in `/etc/httpd/conf.d/*.conf`. This is not true for all deployments of Apache. If you are not in such an environment, append these snippets to your `/etc/httpd/conf/httpd.conf` file.
These examples assume that you are using a setup where your nginx configuration is made up of a bunch of files in `/etc/httpd/conf.d/*.conf`. This is not true for all deployments of Apache. If you are not in such an environment, append these snippets to your `/etc/httpd/conf/httpd.conf` file.
:::
@@ -56,7 +56,6 @@ Assuming you are protecting `anubistest.techaro.lol`, you need the following ser
</VirtualHost>
# HTTPS listener that forwards to Anubis
<IfModule mod_proxy.c>
<VirtualHost *:443>
ServerAdmin your@email.here
ServerName anubistest.techaro.lol

View File

@@ -4,7 +4,7 @@ Docker compose is typically used in concert with other load balancers such as [A
```yaml
services:
anubis:
anubis-nginx:
image: ghcr.io/techarohq/anubis:latest
environment:
BIND: ":8080"
@@ -15,17 +15,10 @@ services:
POLICY_FNAME: "/data/cfg/botPolicy.yaml"
OG_PASSTHROUGH: "true"
OG_EXPIRY_TIME: "24h"
healthcheck:
test: ["CMD", "anubis", "--healthcheck"]
interval: 5s
timeout: 30s
retries: 5
start_period: 500ms
ports:
- 8080:8080
volumes:
- "./botPolicy.yaml:/data/cfg/botPolicy.yaml:ro"
nginx:
image: nginx
volumes:

View File

@@ -69,7 +69,7 @@ Anubis uses these environment variables for configuration:
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
| `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false |
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. |
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. See below for details. |
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. |
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
| `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. |

View File

@@ -137,7 +137,7 @@ Test to make sure it's running with `curl`:
curl http://localhost:8240/metrics
```
Then set up your reverse proxy (Nginx, Caddy, etc.) to point to the Anubis port. Anubis will then reverse proxy all requests that meet the policies in `/etc/anubis/gitea.botPolicies.yaml` to the target service.
Then set up your reverse proxy (Nginx, Caddy, etc.) to point to the Anubis port. Anubis will then reverse proxy all requests that meet the policies in `/etc/anubis/gitea.botPolicies.json` to the target service.
For more details on particular reverse proxies, see here:

View File

@@ -268,12 +268,6 @@ The memory backend is an in-memory cache. This backend works best if you don't u
The biggest downside is that there is not currently a limit to how much data can be stored in memory. This will be addressed at a later time.
:::warning
The in-memory backend exists mostly for validation, testing, and to ensure that the default configuration of Anubis works as expected. Do not use this persistently in production.
:::
#### Configuration
The memory backend does not require any configuration to use.

View File

@@ -58,20 +58,6 @@ Anubis is brought to you by sponsors and donors like:
<a href="https://wildbase.xyz/">
<img src="/img/sponsors/wildbase-logo.webp" alt="Wildbase" height="64" />
</a>
<a href="https://emma.pet">
<img
src="/img/sponsors/nepeat-logo.webp"
alt="Cat eyes over the word Emma in a serif font"
height="64"
/>
</a>
<a href="https://fabulous.systems/">
<img
src="/img/sponsors/fabulous-systems.webp"
alt="Cat eyes over the word Emma in a serif font"
height="64"
/>
</a>
## Overview

View File

@@ -50,14 +50,6 @@ This page contains a non-exhaustive list with all websites using Anubis.
- https://coinhoards.org/
- https://pluralpedia.org/
- https://git.aya.so/
- https://marginalia-search.com/
- https://repositorio.ufrn.br/home/
- https://mozillazine.org/
- https://clew.se/
- https://tumfatig.net/
- https://rpmfusion.org/
- https://wiki.freepascal.org/
- https://azurlane.koumakan.jp/
- <details>
<summary>FreeCAD</summary>
- https://forum.freecad.org/
@@ -102,14 +94,3 @@ This page contains a non-exhaustive list with all websites using Anubis.
- https://find.library.duke.edu/
- https://nicholas.duke.edu/
</details>
- <details>
<summary>Forschungszentrum Jülich</summary>
- https://juser.fz-juelich.de/
</details>
- <details>
<summary>archlinux32.org</summary>
- https://www.archlinux32.org/packages/
- https://bbs.archlinux32.org/
- https://bugs.archlinux32.org/
</details>

View File

@@ -6,7 +6,7 @@ import type * as Preset from '@docusaurus/preset-classic';
const config: Config = {
title: 'Anubis',
tagline: 'Weigh the soul of incoming HTTP requests to protect your website!',
tagline: 'Weigh the soul of incoming HTTP requests using proof-of-work to stop AI crawlers',
favicon: 'img/favicon.ico',
// Set the production url of your site here
@@ -40,20 +40,27 @@ const config: Config = {
[
'classic',
{
docs: {
sidebarPath: './sidebars.ts',
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/TecharoHQ/anubis/tree/main/docs/',
},
blog: {
showReadingTime: true,
feedOptions: {
type: ['rss', 'atom', "json"],
xslt: true,
},
editUrl: 'https://github.com/TecharoHQ/anubis/tree/main/docs/',
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
// Useful options to enforce blogging best practices
onInlineTags: 'warn',
onInlineAuthors: 'warn',
onUntruncatedBlogPosts: 'throw',
},
docs: {
sidebarPath: './sidebars.ts',
editUrl: 'https://github.com/TecharoHQ/anubis/tree/main/docs/',
onUntruncatedBlogPosts: 'warn',
},
theme: {
customCss: './src/css/custom.css',
@@ -67,7 +74,7 @@ const config: Config = {
respectPrefersColorScheme: true,
},
// Replace with your project's social card
image: 'img/social-card.jpg',
image: 'img/docusaurus-social-card.jpg',
navbar: {
title: 'Anubis',
logo: {
@@ -75,28 +82,23 @@ const config: Config = {
src: 'img/favicon.webp',
},
items: [
{ to: '/blog', label: 'Blog', position: 'left' },
{
type: 'docSidebar',
sidebarId: 'tutorialSidebar',
position: 'left',
label: 'Docs',
},
{ to: '/blog', label: 'Blog', position: 'left' },
{
to: '/docs/admin/botstopper',
label: "Unbranded Version",
position: "left"
href: 'https://github.com/sponsors/Xe',
label: "Sponsorship",
position: 'left'
},
{
href: 'https://github.com/TecharoHQ/anubis',
label: 'GitHub',
position: 'right',
},
{
href: 'https://github.com/sponsors/Xe',
label: "Sponsor the Project",
position: 'right'
},
],
},
footer: {
@@ -139,10 +141,6 @@ const config: Config = {
label: 'GitHub',
href: 'https://github.com/TecharoHQ/anubis',
},
{
label: 'Status',
href: 'https://techarohq.github.io/status/'
},
],
},
],

View File

@@ -1,19 +0,0 @@
app = 'anubis-docs'
primary_region = 'yyz'
[build]
image = "ghcr.io/techarohq/anubis/docs:main"
[http_service]
internal_port = 80
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ['app']
[[vm]]
cpu_kind = 'shared'
cpus = 1
memory_mb = 256

View File

@@ -1,99 +0,0 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/avif avif;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/wasm wasm;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@@ -1,31 +0,0 @@
user nginx;
worker_processes 2;
error_log /dev/stdout warn;
pid /nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
sendfile on;
keepalive_timeout 65;
server {
listen 80 default_server;
server_name _;
error_page 404 /404.html;
root /www;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
}

View File

@@ -15,9 +15,6 @@ spec:
- name: anubis
configMap:
name: anubis-cfg
- name: nginx
configMap:
name: nginx-cfg
- name: temporary-data
emptyDir: {}
containers:
@@ -31,23 +28,8 @@ spec:
requests:
cpu: 250m
memory: 128Mi
volumeMounts:
- name: nginx
mountPath: /conf
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 1
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 20
- name: anubis
image: ghcr.io/techarohq/anubis:main
imagePullPolicy: Always
@@ -93,15 +75,3 @@ spec:
envFrom:
- secretRef:
name: anubis-docs-thoth
readinessProbe:
httpGet:
path: /healthz
port: 9090
initialDelaySeconds: 1
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 9090
initialDelaySeconds: 10
periodSeconds: 20

View File

@@ -11,8 +11,3 @@ configMapGenerator:
behavior: create
files:
- ./cfg/anubis/botPolicies.yaml
- name: nginx-cfg
behavior: create
files:
- ./cfg/nginx/mime.types
- ./cfg/nginx/nginx.conf

View File

@@ -5,50 +5,49 @@ import styles from "./styles.module.css";
type FeatureItem = {
title: string;
imageURL: string;
Svg: React.ComponentType<React.ComponentProps<"svg">>;
description: ReactNode;
};
const FeatureList: FeatureItem[] = [
{
title: "Easy to Use",
imageURL: require("@site/static/img/anubis/happy.webp").default,
Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default,
description: (
<>
Anubis sits in the background and weighs the risk of incoming requests.
If it asks a client to complete a challenge, no user interaction is
required.
Anubis is easy to set up, lightweight, and helps get rid of the lowest
hanging fruit so you can sleep at night.
</>
),
},
{
title: "Lightweight",
imageURL: require("@site/static/img/anubis/pensive.webp").default,
Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default,
description: (
<>
Anubis is so lightweight you'll forget it's there until you look at your
hosting bill. On average it uses less than 128 MB of ram.
Anubis is efficient and as lightweight as possible, blocking the worst
of the bots on the internet and makes it easy to protect what you host
online.
</>
),
},
{
title: "Block the scrapers",
imageURL: require("@site/static/img/anubis/reject.webp").default,
title: "Multi-threaded",
Svg: require("@site/static/img/undraw_docusaurus_react.svg").default,
description: (
<>
Anubis uses a combination of heuristics to identify and block bots
before they take your website down. You can customize the rules with{" "}
<a href="/docs/admin/policies">your own policies</a>.
Anubis uses a multi-threaded proof of work check to ensure that users
browsers are up to date and support modern standards.
</>
),
},
];
function Feature({ title, description, imageURL }: FeatureItem) {
function Feature({ title, Svg, description }: FeatureItem) {
return (
<div className={clsx("col col--4")}>
<div className="text--center">
<img src={imageURL} className={styles.featureSvg} role="img" />
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<Heading as="h3">{title}</Heading>

View File

@@ -31,12 +31,19 @@ export default function Home(): ReactNode {
const { siteConfig } = useDocusaurusContext();
return (
<Layout
title={`Anubis: Web AI Firewall Utility`}
description="Weigh the soul of incoming HTTP requests to protect your website!"
title={`Anubis: self hostable scraper defense software`}
description="Weigh the soul of incoming HTTP requests using proof-of-work to stop AI crawlers"
>
<HomepageHeader />
<main>
<HomepageFeatures />
<center>
<p>
This is all placeholder text. It will be fixed. Give me time. I am
one person and my project has unexpectedly gone viral.
</p>
</center>
</main>
</Layout>
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

61
go.mod
View File

@@ -4,37 +4,34 @@ go 1.24.2
require (
github.com/TecharoHQ/thoth-proto v0.4.0
github.com/a-h/templ v0.3.920
github.com/a-h/templ v0.3.906
github.com/cespare/xxhash/v2 v2.3.0
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
github.com/gaissmai/bart v0.22.0
github.com/golang-jwt/jwt/v5 v5.2.3
github.com/google/cel-go v0.26.0
github.com/gaissmai/bart v0.20.4
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/cel-go v0.25.0
github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2
github.com/joho/godotenv v1.5.1
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be
github.com/nicksnyder/go-i18n/v2 v2.6.0
github.com/playwright-community/playwright-go v0.5200.0
github.com/prometheus/client_golang v1.22.0
github.com/redis/go-redis/v9 v9.11.0
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
github.com/shirou/gopsutil/v4 v4.25.6
github.com/testcontainers/testcontainers-go v0.38.0
go.etcd.io/bbolt v1.4.2
golang.org/x/net v0.42.0
golang.org/x/text v0.27.0
golang.org/x/net v0.41.0
golang.org/x/text v0.26.0
google.golang.org/grpc v1.73.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.33.3
k8s.io/apimachinery v0.33.2
sigs.k8s.io/yaml v1.5.0
)
require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 // indirect
cel.dev/expr v0.24.0 // indirect
cel.dev/expr v0.23.1 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
@@ -47,10 +44,8 @@ require (
github.com/Songmu/gitconfig v0.2.0 // indirect
github.com/TecharoHQ/yeet v0.6.0 // indirect
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/cavaliergopher/cpio v1.0.1 // indirect
@@ -58,8 +53,6 @@ require (
github.com/cli/browser v1.3.0 // indirect
github.com/cli/go-gh v0.1.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
@@ -70,11 +63,11 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/docker/docker v28.2.2+incompatible // indirect
github.com/docker/docker v28.0.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
@@ -86,7 +79,7 @@ require (
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.14.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
@@ -95,7 +88,6 @@ require (
github.com/goccy/go-yaml v1.12.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-github/v70 v70.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
@@ -104,7 +96,6 @@ require (
github.com/goreleaser/fileglob v1.3.0 // indirect
github.com/goreleaser/nfpm/v2 v2.42.1 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/hcl/v2 v2.24.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
@@ -115,13 +106,11 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
@@ -138,6 +127,7 @@ require (
github.com/prometheus/procfs v0.15.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
@@ -148,6 +138,7 @@ require (
github.com/suzuki-shunsuke/logrus-error v0.1.4 // indirect
github.com/suzuki-shunsuke/pinact v1.6.0 // indirect
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 // indirect
github.com/testcontainers/testcontainers-go v0.37.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
@@ -155,32 +146,28 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zclconf/go-cty v1.16.3 // indirect
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.3 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/tools v0.34.0 // indirect
golang.org/x/vuln v1.1.4 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
honnef.co/go/tools v0.6.1 // indirect

136
go.sum
View File

@@ -2,12 +2,10 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeX
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 h1:YhMSc48s25kr7kv31Z8vf7sPUIq5YJva9z1mn/hAt0M=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
@@ -40,18 +38,14 @@ github.com/TecharoHQ/yeet v0.6.0 h1:RCBAjr7wIlllsgy0tpvWpLX7jsZgu2tiuBY3RrprcR0=
github.com/TecharoHQ/yeet v0.6.0/go.mod h1:bj2V4Fg8qKQXoiuPZa3HuawrE8g+LsOQv/9q2WyGSsA=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/a-h/templ v0.3.920 h1:IQjjTu4KGrYreHo/ewzSeS8uefecisPayIIc9VflLSE=
github.com/a-h/templ v0.3.920/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
github.com/a-h/templ v0.3.906 h1:ZUThc8Q9n04UATaCwaG60pB1AqbulLmYEAMnWV63svg=
github.com/a-h/templ v0.3.906/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -81,10 +75,6 @@ github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5
github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
@@ -109,16 +99,16 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg=
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -141,8 +131,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gaissmai/bart v0.22.0 h1:+yR2mCpZx8H8GlqA+Icqi7/Iwx2/OUbO4bVbsORK0ns=
github.com/gaissmai/bart v0.22.0/go.mod h1:RpLtt3lWq1BoRz3AAyDAJ7jhLWBkYhVCfi+ximB2t68=
github.com/gaissmai/bart v0.20.4 h1:Ik47r1fy3jRVU+1eYzKSW3ho2UgBVTVnUS8O993584U=
github.com/gaissmai/bart v0.20.4/go.mod h1:cEed+ge8dalcbpi8wtS9x9m2hn/fNJH5suhdGQOHnYk=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -156,8 +146,8 @@ github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
@@ -183,14 +173,14 @@ github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
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/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -224,13 +214,9 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
@@ -263,8 +249,6 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be h1:dVIND0nXGXPQnFZYrMXT6CxHhBYhTPMm0GFqcmfaIC4=
github.com/lum8rjack/go-ja4h v0.0.0-20250606032308-3a989c6635be/go.mod h1:q68TUR45WDa2r3yU4aO6WgxfCc0Vj1qtRaKaRE3yMLM=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
@@ -284,22 +268,16 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
@@ -351,8 +329,8 @@ github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a h1:iLcLb5Fwwz7g/DLK89F+
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -373,8 +351,6 @@ github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -389,8 +365,8 @@ github.com/suzuki-shunsuke/pinact v1.6.0 h1:2QvSzREOquwLwKXhF9Hj0AInE/Rl63SZz9dK
github.com/suzuki-shunsuke/pinact v1.6.0/go.mod h1:FDUMck0mmL0mcnNZ23Vjh/aOR5cIdZhF1IIpGksT4dQ=
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 h1:YGHgrVjGTYHY98II6zijXUHP+OyvrzSCvd8m9iUcaK8=
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4/go.mod h1:sSi6xaUaHfaqu32ECLeyE7NTMv+ZM5dW0JikhllaalY=
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
@@ -411,8 +387,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8=
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
@@ -421,22 +395,16 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
@@ -447,8 +415,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ=
@@ -468,8 +436,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -477,8 +445,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -505,8 +473,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0=
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -514,8 +482,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -524,10 +492,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -545,10 +511,10 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
@@ -567,12 +533,10 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA=
k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=

View File

@@ -1,97 +0,0 @@
package fingerprint
import (
"crypto/md5"
"crypto/tls"
"encoding/hex"
"slices"
"strconv"
)
// TLSFingerprintJA3N represents a JA3N fingerprint
type TLSFingerprintJA3N [md5.Size]byte
func (f TLSFingerprintJA3N) String() string {
return hex.EncodeToString(f[:])
}
func buildJA3N(hello *tls.ClientHelloInfo, sortExtensions bool) TLSFingerprintJA3N {
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 TLSFingerprintJA3N(sum[:])
}

View File

@@ -1,176 +0,0 @@
package fingerprint
import (
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"slices"
"strconv"
"strings"
)
// TLSFingerprintJA4 represents a JA4 fingerprint
type TLSFingerprintJA4 struct {
A [10]byte
B [6]byte
C [6]byte
}
func (f *TLSFingerprintJA4) String() string {
if f == nil {
return ""
}
return strings.Join([]string{
string(f.A[:]),
hex.EncodeToString(f.B[:]),
hex.EncodeToString(f.C[:]),
}, "_")
}
func buildJA4(hello *tls.ClientHelloInfo) (ja4 TLSFingerprintJA4) {
buf := make([]byte, 0, 36)
hasQuic := false
for _, ext := range hello.Extensions {
if ext == extensionQUICTransportParameters {
hasQuic = true
}
}
switch hasQuic {
case true:
buf = append(buf, 'q')
case false:
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.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])
}

View File

@@ -1,46 +0,0 @@
package fingerprint
import (
"fmt"
"net/http"
"strings"
)
// JA4T represents a TCP fingerprint
type JA4T struct {
Window uint32
Options []uint8
MSS uint16
WindowScale uint8
}
func (j JA4T) String() string {
var sb strings.Builder
// Start with the window size
fmt.Fprintf(&sb, "%d", j.Window)
// Append each option
for i, opt := range j.Options {
if i == 0 {
fmt.Fprint(&sb, "_")
} else {
fmt.Fprint(&sb, "-")
}
fmt.Fprintf(&sb, "%d", opt)
}
// Append MSS and WindowScale
fmt.Fprintf(&sb, "_%d_%d", j.MSS, j.WindowScale)
return sb.String()
}
// GetTCPFingerprint extracts TCP fingerprint from HTTP request context
func GetTCPFingerprint(r *http.Request) *JA4T {
ptr := r.Context().Value(tcpFingerprintKey{})
if fpPtr, ok := ptr.(*JA4T); ok && ptr != nil && fpPtr != nil {
return fpPtr
}
return nil
}

View File

@@ -1,106 +0,0 @@
//go:build freebsd
package fingerprint
import (
"fmt"
"net"
"syscall"
"unsafe"
)
type tcpInfo struct {
State uint8
Options uint8
SndScale uint8
RcvScale uint8
__pad [4]byte
Rto uint32
Ato uint32
SndMss uint32
RcvMss uint32
Unacked uint32
Sacked uint32
Lost uint32
Retrans uint32
Fackets uint32
Last_data_sent uint32
Last_ack_sent uint32
Last_data_recv uint32
Last_ack_recv uint32
Pmtu uint32
Rcv_ssthresh uint32
RTT uint32
RTTvar uint32
Snd_ssthresh uint32
Snd_cwnd uint32
Advmss uint32
Reordering uint32
Rcv_rtt uint32
Rcv_space uint32
Total_retrans uint32
Snd_wnd uint32
// Truncated for brevity — add more fields if needed
}
// AssignTCPFingerprint extracts TCP fingerprint information from a connection
func AssignTCPFingerprint(conn net.Conn) (*JA4T, error) {
tcpConn, ok := conn.(*net.TCPConn)
if !ok {
return nil, fmt.Errorf("not a TCPConn")
}
rawConn, err := tcpConn.SyscallConn()
if err != nil {
return nil, fmt.Errorf("SyscallConn failed: %w", err)
}
var info tcpInfo
var sysErr error
err = rawConn.Control(func(fd uintptr) {
size := uint32(unsafe.Sizeof(info))
_, _, errno := syscall.Syscall6(
syscall.SYS_GETSOCKOPT,
fd,
uintptr(syscall.IPPROTO_TCP),
uintptr(syscall.TCP_INFO),
uintptr(unsafe.Pointer(&info)),
uintptr(unsafe.Pointer(&size)),
0,
)
if errno != 0 {
sysErr = errno
}
})
if err != nil {
return nil, fmt.Errorf("SyscallConn.Control: %w", err)
}
if sysErr != nil {
return nil, fmt.Errorf("getsockopt TCP_INFO: %w", sysErr)
}
fp := &JA4T{
Window: info.Snd_wnd,
MSS: uint16(info.SndMss),
WindowScale: info.SndScale,
}
const (
TCPI_OPT_TIMESTAMPS = 1 << 0
TCPI_OPT_SACK = 1 << 1
TCPI_OPT_WSCALE = 1 << 2
)
if info.Options&TCPI_OPT_SACK != 0 {
fp.Options = append(fp.Options, 4, 1)
}
if info.Options&TCPI_OPT_TIMESTAMPS != 0 {
fp.Options = append(fp.Options, 8, 1)
}
if info.Options&TCPI_OPT_WSCALE != 0 {
fp.Options = append(fp.Options, 3)
}
return fp, nil
}

View File

@@ -1,132 +0,0 @@
//go:build linux
package fingerprint
import (
"fmt"
"net"
"syscall"
"unsafe"
)
type tcpInfo struct {
State uint8
Ca_state uint8
Retransmits uint8
Probes uint8
Backoff uint8
Options uint8
Wnd_scale uint8
Delivery_rate_app_limited uint8
Rto uint32
Ato uint32
SndMss uint32
RcvMss uint32
Unacked uint32
Sacked uint32
Lost uint32
Retrans uint32
Fackets uint32
Last_data_sent uint32
Last_ack_sent uint32
Last_data_recv uint32
Last_ack_recv uint32
PMTU uint32
Rcv_ssthresh uint32
RTT uint32
RTTvar uint32
Snd_ssthresh uint32
Snd_cwnd uint32
Advmss uint32
Reordering uint32
Rcv_rtt uint32
Rcv_space uint32
Total_retrans uint32
Pacing_rate uint64
Max_pacing_rate uint64
Bytes_acked uint64
Bytes_received uint64
Segs_out uint32
Segs_in uint32
Notsent_bytes uint32
Min_rtt uint32
Data_segs_in uint32
Data_segs_out uint32
Delivery_rate uint64
Busy_time uint64
Rwnd_limited uint64
Sndbuf_limited uint64
Delivered uint32
Delivered_ce uint32
Bytes_sent uint64
Bytes_retrans uint64
DSACK_dups uint32
Reord_seen uint32
Rcv_ooopack uint32
Snd_wnd uint32
}
// AssignTCPFingerprint extracts TCP fingerprint information from a connection
func AssignTCPFingerprint(conn net.Conn) (*JA4T, error) {
tcpConn, ok := conn.(*net.TCPConn)
if !ok {
return nil, fmt.Errorf("not a TCPConn")
}
rawConn, err := tcpConn.SyscallConn()
if err != nil {
return nil, fmt.Errorf("SyscallConn failed: %w", err)
}
var info tcpInfo
var sysErr error
err = rawConn.Control(func(fd uintptr) {
size := uint32(unsafe.Sizeof(info))
_, _, errno := syscall.Syscall6(
syscall.SYS_GETSOCKOPT,
fd,
uintptr(syscall.IPPROTO_TCP),
uintptr(syscall.TCP_INFO),
uintptr(unsafe.Pointer(&info)),
uintptr(unsafe.Pointer(&size)),
0,
)
if errno != 0 {
sysErr = errno
}
})
if err != nil {
return nil, fmt.Errorf("SyscallConn.Control: %w", err)
}
if sysErr != nil {
return nil, fmt.Errorf("getsockopt TCP_INFO: %w", sysErr)
}
fp := &JA4T{
Window: info.Snd_wnd,
MSS: uint16(info.SndMss),
}
const (
TCPI_OPT_TIMESTAMPS = 1 << 0
TCPI_OPT_SACK = 1 << 1
TCPI_OPT_WSCALE = 1 << 2
)
if info.Options&TCPI_OPT_SACK != 0 {
fp.Options = append(fp.Options, 4, 1)
}
if info.Options&TCPI_OPT_TIMESTAMPS != 0 {
fp.Options = append(fp.Options, 8, 1)
}
if info.Options&TCPI_OPT_WSCALE != 0 {
fp.Options = append(fp.Options, 3)
fp.WindowScale = info.Wnd_scale
}
return fp, nil
}

View File

@@ -1,11 +0,0 @@
//go:build !linux && !freebsd
package fingerprint
import "net"
// AssignTCPFingerprint is not supported on this platform
func AssignTCPFingerprint(conn net.Conn) (*JA4T, error) {
// Not supported on macOS and other platforms
return &JA4T{}, nil
}

View File

@@ -1,110 +0,0 @@
package fingerprint
import (
"context"
"crypto/tls"
"log/slog"
"net"
"net/http"
"sync/atomic"
)
// ApplyTLSFingerprinter configures a TLS server to capture TLS fingerprints
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 {
ctx = context.WithValue(ctx, tlsFingerprintKey{}, &TLSFingerprint{})
if tc, ok := c.(*tls.Conn); ok {
tcpFP, err := AssignTCPFingerprint(tc.NetConn())
if err == nil {
ctx = context.WithValue(ctx, tcpFingerprintKey{}, tcpFP)
} else {
slog.Debug("ja4t error", "err", err)
}
}
return ctx
}
}
type tcpFingerprintKey struct{}
type tlsFingerprintKey struct{}
// TLSFingerprint represents TLS fingerprint data
type TLSFingerprint struct {
ja3n atomic.Pointer[TLSFingerprintJA3N]
ja4 atomic.Pointer[TLSFingerprintJA4]
}
// JA3N returns the JA3N fingerprint
func (f *TLSFingerprint) JA3N() *TLSFingerprintJA3N {
return f.ja3n.Load()
}
// JA4 returns the JA4 fingerprint
func (f *TLSFingerprint) JA4() *TLSFingerprintJA4 {
return f.ja4.Load()
}
const greaseMask = 0x0F0F
const greaseValue = 0x0a0a
// TLS extension numbers
const (
extensionServerName uint16 = 0
extensionStatusRequest uint16 = 5
extensionSupportedCurves uint16 = 10 // supported_groups in TLS 1.3, see RFC 8446, Section 4.2.7
extensionSupportedPoints uint16 = 11
extensionSignatureAlgorithms uint16 = 13
extensionALPN uint16 = 16
extensionSCT uint16 = 18
extensionExtendedMasterSecret uint16 = 23
extensionSessionTicket uint16 = 35
extensionPreSharedKey uint16 = 41
extensionEarlyData uint16 = 42
extensionSupportedVersions uint16 = 43
extensionCookie uint16 = 44
extensionPSKModes uint16 = 45
extensionCertificateAuthorities uint16 = 47
extensionSignatureAlgorithmsCert uint16 = 50
extensionKeyShare uint16 = 51
extensionQUICTransportParameters uint16 = 57
extensionRenegotiationInfo uint16 = 0xff01
extensionECHOuterExtensions uint16 = 0xfd00
extensionEncryptedClientHello uint16 = 0xfe0d
)
func buildTLSFingerprint(hello *tls.ClientHelloInfo) (TLSFingerprintJA3N, TLSFingerprintJA4) {
return TLSFingerprintJA3N(buildJA3N(hello, true)), buildJA4(hello)
}
// GetTLSFingerprint extracts TLS fingerprint from HTTP request context
func GetTLSFingerprint(r *http.Request) *TLSFingerprint {
ptr := r.Context().Value(tlsFingerprintKey{})
if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil {
return fpPtr
}
return nil
}

View File

@@ -1,25 +0,0 @@
package internal
import (
"context"
"google.golang.org/grpc/health"
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
)
var HealthSrv = health.NewServer()
func SetHealth(svc string, status healthv1.HealthCheckResponse_ServingStatus) {
HealthSrv.SetServingStatus(svc, status)
}
func GetHealth(svc string) (healthv1.HealthCheckResponse_ServingStatus, bool) {
st, err := HealthSrv.Check(context.Background(), &healthv1.HealthCheckRequest{
Service: svc,
})
if err != nil {
return healthv1.HealthCheckResponse_UNKNOWN, false
}
return st.GetStatus(), true
}

View File

@@ -1,14 +0,0 @@
package internal
import (
"net/http"
"github.com/lum8rjack/go-ja4h"
)
func JA4H(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Add("X-Http-Fingerprint-JA4H", ja4h.JA4H(r))
next.ServeHTTP(w, r)
})
}

View File

@@ -1,7 +1,6 @@
package ogtags
import (
"context"
"errors"
"log/slog"
"net/url"
@@ -9,7 +8,7 @@ import (
)
// GetOGTags is the main function that retrieves Open Graph tags for a URL
func (c *OGTagCache) GetOGTags(ctx context.Context, url *url.URL, originalHost string) (map[string]string, error) {
func (c *OGTagCache) GetOGTags(url *url.URL, originalHost string) (map[string]string, error) {
if url == nil {
return nil, errors.New("nil URL provided, cannot fetch OG tags")
}
@@ -22,12 +21,12 @@ func (c *OGTagCache) GetOGTags(ctx context.Context, url *url.URL, originalHost s
cacheKey := c.generateCacheKey(target, originalHost)
// Check cache first
if cachedTags := c.checkCache(ctx, cacheKey); cachedTags != nil {
if cachedTags := c.checkCache(cacheKey); cachedTags != nil {
return cachedTags, nil
}
// Fetch HTML content, passing the original host
doc, err := c.fetchHTMLDocumentWithCache(ctx, target, originalHost, cacheKey)
doc, err := c.fetchHTMLDocumentWithCache(target, originalHost, cacheKey)
if errors.Is(err, syscall.ECONNREFUSED) {
slog.Debug("Connection refused, returning empty tags")
return nil, nil
@@ -43,7 +42,7 @@ func (c *OGTagCache) GetOGTags(ctx context.Context, url *url.URL, originalHost s
ogTags := c.extractOGTags(doc)
// Store in cache
c.cache.Set(ctx, cacheKey, ogTags, c.ogTimeToLive)
c.cache.Set(cacheKey, ogTags, c.ogTimeToLive)
return ogTags, nil
}
@@ -60,8 +59,8 @@ func (c *OGTagCache) generateCacheKey(target string, originalHost string) string
}
// checkCache checks if we have the tags cached and returns them if so
func (c *OGTagCache) checkCache(ctx context.Context, cacheKey string) map[string]string {
if cachedTags, err := c.cache.Get(ctx, cacheKey); err == nil {
func (c *OGTagCache) checkCache(cacheKey string) map[string]string {
if cachedTags, ok := c.cache.Get(cacheKey); ok {
slog.Debug("cache hit", "tags", cachedTags)
return cachedTags
}

View File

@@ -9,7 +9,6 @@ import (
"time"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
)
func TestCacheReturnsDefault(t *testing.T) {
@@ -22,14 +21,14 @@ func TestCacheReturnsDefault(t *testing.T) {
TimeToLive: time.Minute,
ConsiderHost: false,
Override: want,
}, memory.New(t.Context()))
})
u, err := url.Parse("https://anubis.techaro.lol")
if err != nil {
t.Fatal(err)
}
result, err := cache.GetOGTags(t.Context(), u, "anubis.techaro.lol")
result, err := cache.GetOGTags(u, "anubis.techaro.lol")
if err != nil {
t.Fatal(err)
}
@@ -50,7 +49,7 @@ func TestCheckCache(t *testing.T) {
Enabled: true,
TimeToLive: time.Minute,
ConsiderHost: false,
}, memory.New(t.Context()))
})
// Set up test data
urlStr := "http://example.com/page"
@@ -61,16 +60,16 @@ func TestCheckCache(t *testing.T) {
cacheKey := cache.generateCacheKey(urlStr, "example.com")
// Test cache miss
tags := cache.checkCache(t.Context(), cacheKey)
tags := cache.checkCache(cacheKey)
if tags != nil {
t.Errorf("expected nil tags on cache miss, got %v", tags)
}
// Manually add to cache
cache.cache.Set(t.Context(), cacheKey, expectedTags, time.Minute)
cache.cache.Set(cacheKey, expectedTags, time.Minute)
// Test cache hit
tags = cache.checkCache(t.Context(), cacheKey)
tags = cache.checkCache(cacheKey)
if tags == nil {
t.Fatal("expected non-nil tags on cache hit, got nil")
}
@@ -113,7 +112,7 @@ func TestGetOGTags(t *testing.T) {
Enabled: true,
TimeToLive: time.Minute,
ConsiderHost: false,
}, memory.New(t.Context()))
})
// Parse the test server URL
parsedURL, err := url.Parse(ts.URL)
@@ -123,7 +122,7 @@ func TestGetOGTags(t *testing.T) {
// Test fetching OG tags from the test server
// Pass the host from the parsed test server URL
ogTags, err := cache.GetOGTags(t.Context(), parsedURL, parsedURL.Host)
ogTags, err := cache.GetOGTags(parsedURL, parsedURL.Host)
if err != nil {
t.Fatalf("failed to get OG tags: %v", err)
}
@@ -143,14 +142,14 @@ func TestGetOGTags(t *testing.T) {
// Test fetching OG tags from the cache
// Pass the host from the parsed test server URL
ogTags, err = cache.GetOGTags(t.Context(), parsedURL, parsedURL.Host)
ogTags, err = cache.GetOGTags(parsedURL, parsedURL.Host)
if err != nil {
t.Fatalf("failed to get OG tags from cache: %v", err)
}
// Test fetching OG tags from the cache (3rd time)
// Pass the host from the parsed test server URL
newOgTags, err := cache.GetOGTags(t.Context(), parsedURL, parsedURL.Host)
newOgTags, err := cache.GetOGTags(parsedURL, parsedURL.Host)
if err != nil {
t.Fatalf("failed to get OG tags from cache: %v", err)
}
@@ -264,10 +263,10 @@ func TestGetOGTagsWithHostConsideration(t *testing.T) {
Enabled: true,
TimeToLive: time.Minute,
ConsiderHost: tc.ogCacheConsiderHost,
}, memory.New(t.Context()))
})
for i, req := range tc.requests {
ogTags, err := cache.GetOGTags(t.Context(), parsedURL, req.host)
ogTags, err := cache.GetOGTags(parsedURL, req.host)
if err != nil {
t.Errorf("Request %d (host: %s): unexpected error: %v", i+1, req.host, err)
continue // Skip further checks for this request if error occurred

View File

@@ -20,8 +20,8 @@ var (
// fetchHTMLDocumentWithCache fetches the HTML document from the given URL string,
// preserving the original host header.
func (c *OGTagCache) fetchHTMLDocumentWithCache(ctx context.Context, urlStr string, originalHost string, cacheKey string) (*html.Node, error) {
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
func (c *OGTagCache) fetchHTMLDocumentWithCache(urlStr string, originalHost string, cacheKey string) (*html.Node, error) {
req, err := http.NewRequestWithContext(context.Background(), "GET", urlStr, nil)
if err != nil {
return nil, fmt.Errorf("failed to create http request: %w", err)
}
@@ -41,7 +41,7 @@ func (c *OGTagCache) fetchHTMLDocumentWithCache(ctx context.Context, urlStr stri
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
slog.Debug("og: request timed out", "url", urlStr)
c.cache.Set(ctx, cacheKey, emptyMap, c.ogTimeToLive/2) // Cache empty result for half the TTL to not spam the server
c.cache.Set(cacheKey, emptyMap, c.ogTimeToLive/2) // Cache empty result for half the TTL to not spam the server
}
return nil, fmt.Errorf("http get failed: %w", err)
}
@@ -56,7 +56,7 @@ func (c *OGTagCache) fetchHTMLDocumentWithCache(ctx context.Context, urlStr stri
if resp.StatusCode != http.StatusOK {
slog.Debug("og: received non-OK status code", "url", urlStr, "status", resp.StatusCode)
c.cache.Set(ctx, cacheKey, emptyMap, c.ogTimeToLive) // Cache empty result for non-successful status codes
c.cache.Set(cacheKey, emptyMap, c.ogTimeToLive) // Cache empty result for non-successful status codes
return nil, fmt.Errorf("%w: page not found", ErrOgHandled)
}

View File

@@ -1,7 +1,6 @@
package ogtags
import (
"context"
"fmt"
"io"
"net/http"
@@ -12,7 +11,6 @@ import (
"time"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html"
)
@@ -87,8 +85,8 @@ func TestFetchHTMLDocument(t *testing.T) {
Enabled: true,
TimeToLive: time.Minute,
ConsiderHost: false,
}, memory.New(t.Context()))
doc, err := cache.fetchHTMLDocument(t.Context(), ts.URL, "anything")
})
doc, err := cache.fetchHTMLDocument(ts.URL, "anything")
if tt.expectError {
if err == nil {
@@ -118,9 +116,9 @@ func TestFetchHTMLDocumentInvalidURL(t *testing.T) {
Enabled: true,
TimeToLive: time.Minute,
ConsiderHost: false,
}, memory.New(t.Context()))
})
doc, err := cache.fetchHTMLDocument(t.Context(), "http://invalid.url.that.doesnt.exist.example", "anything")
doc, err := cache.fetchHTMLDocument("http://invalid.url.that.doesnt.exist.example", "anything")
if err == nil {
t.Error("expected error for invalid URL, got nil")
@@ -132,7 +130,7 @@ func TestFetchHTMLDocumentInvalidURL(t *testing.T) {
}
// fetchHTMLDocument allows you to call fetchHTMLDocumentWithCache without a duplicate generateCacheKey call
func (c *OGTagCache) fetchHTMLDocument(ctx context.Context, urlStr string, originalHost string) (*html.Node, error) {
func (c *OGTagCache) fetchHTMLDocument(urlStr string, originalHost string) (*html.Node, error) {
cacheKey := c.generateCacheKey(urlStr, originalHost)
return c.fetchHTMLDocumentWithCache(ctx, urlStr, originalHost, cacheKey)
return c.fetchHTMLDocumentWithCache(urlStr, originalHost, cacheKey)
}

View File

@@ -8,7 +8,6 @@ import (
"time"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
)
func TestIntegrationGetOGTags(t *testing.T) {
@@ -111,7 +110,7 @@ func TestIntegrationGetOGTags(t *testing.T) {
Enabled: true,
TimeToLive: time.Minute,
ConsiderHost: false,
}, memory.New(t.Context()))
})
// Create URL for test
testURL, _ := url.Parse(ts.URL)
@@ -120,7 +119,7 @@ func TestIntegrationGetOGTags(t *testing.T) {
// Get OG tags
// Pass the host from the test URL
ogTags, err := cache.GetOGTags(t.Context(), testURL, testURL.Host)
ogTags, err := cache.GetOGTags(testURL, testURL.Host)
// Check error expectation
if tc.expectError {
@@ -148,7 +147,7 @@ func TestIntegrationGetOGTags(t *testing.T) {
// Test cache retrieval
// Pass the host from the test URL
cachedOGTags, err := cache.GetOGTags(t.Context(), testURL, testURL.Host)
cachedOGTags, err := cache.GetOGTags(testURL, testURL.Host)
if err != nil {
t.Fatalf("failed to get OG tags from cache: %v", err)
}

View File

@@ -7,7 +7,6 @@ import (
"testing"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html"
)
@@ -31,7 +30,7 @@ func BenchmarkGetTarget(b *testing.B) {
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
cache := NewOGTagCache(tt.target, config.OpenGraph{}, memory.New(b.Context()))
cache := NewOGTagCache(tt.target, config.OpenGraph{})
urls := make([]*url.URL, len(tt.paths))
for i, path := range tt.paths {
u, _ := url.Parse(path)
@@ -67,7 +66,7 @@ func BenchmarkExtractOGTags(b *testing.B) {
</head><body><div><p>Content</p></div></body></html>`,
}
cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(b.Context()))
cache := NewOGTagCache("http://example.com", config.OpenGraph{})
docs := make([]*html.Node, len(htmlSamples))
for i, sample := range htmlSamples {
@@ -85,7 +84,7 @@ func BenchmarkExtractOGTags(b *testing.B) {
// Memory usage test
func TestMemoryUsage(t *testing.T) {
cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(t.Context()))
cache := NewOGTagCache("http://example.com", config.OpenGraph{})
// Force GC and wait for it to complete
runtime.GC()

View File

@@ -9,8 +9,8 @@ import (
"strings"
"time"
"github.com/TecharoHQ/anubis/decaymap"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store"
)
const (
@@ -22,7 +22,7 @@ const (
)
type OGTagCache struct {
cache store.JSON[map[string]string]
cache *decaymap.Impl[string, map[string]string]
targetURL *url.URL
client *http.Client
@@ -36,7 +36,7 @@ type OGTagCache struct {
ogOverride map[string]string
}
func NewOGTagCache(target string, conf config.OpenGraph, backend store.Interface) *OGTagCache {
func NewOGTagCache(target string, conf config.OpenGraph) *OGTagCache {
// Predefined approved tags and prefixes
defaultApprovedTags := []string{"description", "keywords", "author"}
defaultApprovedPrefixes := []string{"og:", "twitter:", "fediverse:"}
@@ -77,10 +77,7 @@ func NewOGTagCache(target string, conf config.OpenGraph, backend store.Interface
}
return &OGTagCache{
cache: store.JSON[map[string]string]{
Underlying: backend,
Prefix: "ogtags:",
},
cache: decaymap.New[string, map[string]string](),
targetURL: parsedTargetURL,
ogPassthrough: conf.Enabled,
ogTimeToLive: conf.TimeToLive,
@@ -127,3 +124,9 @@ func (c *OGTagCache) getTarget(u *url.URL) string {
return sb.String()
}
func (c *OGTagCache) Cleanup() {
if c.cache != nil {
c.cache.Cleanup()
}
}

View File

@@ -1,14 +1,12 @@
package ogtags
import (
"context"
"net/url"
"strings"
"testing"
"unicode/utf8"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html"
)
@@ -48,7 +46,7 @@ func FuzzGetTarget(f *testing.F) {
}
// Create cache - should not panic
cache := NewOGTagCache(target, config.OpenGraph{}, memory.New(context.Background()))
cache := NewOGTagCache(target, config.OpenGraph{})
// Create URL
u := &url.URL{
@@ -132,7 +130,7 @@ func FuzzExtractOGTags(f *testing.F) {
return
}
cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(context.Background()))
cache := NewOGTagCache("http://example.com", config.OpenGraph{})
// Should not panic
tags := cache.extractOGTags(doc)
@@ -188,7 +186,7 @@ func FuzzGetTargetRoundTrip(f *testing.F) {
t.Skip()
}
cache := NewOGTagCache(target, config.OpenGraph{}, memory.New(context.Background()))
cache := NewOGTagCache(target, config.OpenGraph{})
u := &url.URL{Path: path, RawQuery: query}
result := cache.getTarget(u)
@@ -245,7 +243,7 @@ func FuzzExtractMetaTagInfo(f *testing.F) {
},
}
cache := NewOGTagCache("http://example.com", config.OpenGraph{}, memory.New(context.Background()))
cache := NewOGTagCache("http://example.com", config.OpenGraph{})
// Should not panic
property, content := cache.extractMetaTagInfo(node)
@@ -298,7 +296,7 @@ func BenchmarkFuzzedGetTarget(b *testing.B) {
for _, input := range inputs {
b.Run(input.name, func(b *testing.B) {
cache := NewOGTagCache(input.target, config.OpenGraph{}, memory.New(context.Background()))
cache := NewOGTagCache(input.target, config.OpenGraph{})
u := &url.URL{Path: input.path, RawQuery: input.query}
b.ResetTimer()

View File

@@ -15,7 +15,6 @@ import (
"time"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
)
func TestNewOGTagCache(t *testing.T) {
@@ -45,7 +44,7 @@ func TestNewOGTagCache(t *testing.T) {
Enabled: tt.ogPassthrough,
TimeToLive: tt.ogTimeToLive,
ConsiderHost: false,
}, memory.New(t.Context()))
})
if cache == nil {
t.Fatal("expected non-nil cache, got nil")
@@ -85,7 +84,7 @@ func TestNewOGTagCache_UnixSocket(t *testing.T) {
Enabled: true,
TimeToLive: 5 * time.Minute,
ConsiderHost: false,
}, memory.New(t.Context()))
})
if cache == nil {
t.Fatal("expected non-nil cache, got nil")
@@ -170,7 +169,7 @@ func TestGetTarget(t *testing.T) {
Enabled: true,
TimeToLive: time.Minute,
ConsiderHost: false,
}, memory.New(t.Context()))
})
u := &url.URL{
Path: tt.path,
@@ -243,14 +242,14 @@ func TestIntegrationGetOGTags_UnixSocket(t *testing.T) {
Enabled: true,
TimeToLive: time.Minute,
ConsiderHost: false,
}, memory.New(t.Context()))
})
// Create a dummy URL for the request (path and query matter)
testReqURL, _ := url.Parse("/some/page?query=1")
// Get OG tags
// Pass an empty string for host, as it's irrelevant for unix sockets
ogTags, err := cache.GetOGTags(t.Context(), testReqURL, "")
ogTags, err := cache.GetOGTags(testReqURL, "")
if err != nil {
t.Fatalf("GetOGTags failed for unix socket: %v", err)
@@ -266,7 +265,7 @@ func TestIntegrationGetOGTags_UnixSocket(t *testing.T) {
// Test cache retrieval (should hit cache)
// Pass an empty string for host
cachedTags, err := cache.GetOGTags(t.Context(), testReqURL, "")
cachedTags, err := cache.GetOGTags(testReqURL, "")
if err != nil {
t.Fatalf("GetOGTags (cache hit) failed for unix socket: %v", err)
}

View File

@@ -7,7 +7,6 @@ import (
"time"
"github.com/TecharoHQ/anubis/lib/policy/config"
"github.com/TecharoHQ/anubis/lib/store/memory"
"golang.org/x/net/html"
)
@@ -18,7 +17,7 @@ func TestExtractOGTags(t *testing.T) {
Enabled: false,
ConsiderHost: false,
TimeToLive: time.Minute,
}, memory.New(t.Context()))
})
// Manually set approved tags/prefixes based on the user request for clarity
testCache.approvedTags = []string{"description"}
testCache.approvedPrefixes = []string{"og:"}
@@ -199,7 +198,7 @@ func TestExtractMetaTagInfo(t *testing.T) {
Enabled: false,
ConsiderHost: false,
TimeToLive: time.Minute,
}, memory.New(t.Context()))
})
testCache.approvedTags = []string{"description"}
testCache.approvedPrefixes = []string{"og:"}

View File

@@ -60,6 +60,15 @@ func New(ctx context.Context, thothURL, apiToken string, plaintext bool) (*Clien
hc := healthv1.NewHealthClient(conn)
resp, err := hc.Check(ctx, &healthv1.HealthCheckRequest{})
if err != nil {
return nil, fmt.Errorf("can't verify thoth health at %s: %w", thothURL, err)
}
if resp.Status != healthv1.HealthCheckResponse_SERVING {
return nil, fmt.Errorf("thoth is not healthy, wanted %s but got %s", healthv1.HealthCheckResponse_SERVING, resp.Status)
}
return &Client{
conn: conn,
health: hc,

Some files were not shown because too many files have changed in this diff Show More