mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-06-10 22:37:59 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8480175eac | |||
| c082cd89dc | |||
| 03bf695dff | |||
| 51ae340a7b | |||
| 430e262c84 | |||
| a47efe31b0 | |||
| 763c896b63 | |||
| a426230698 | |||
| 6c3fc188fb | |||
| a0589d3c7a | |||
| b57508afcd | |||
| 276b537776 | |||
| 9f479f578a | |||
| c184028d42 | |||
| 0491f1fac2 | |||
| d3a00da448 | |||
| 7e037b65e8 | |||
| ebf9a30878 | |||
| f8605bcd3c | |||
| 1d700a0370 | |||
| 681c2cc2ed | |||
| 8f8ae76d56 | |||
| f21706eb12 | |||
| d5ccf9c670 | |||
| 3a6e368179 | |||
| 98afcf8c64 | |||
| 982394ca91 | |||
| 66b7b27aef | |||
| cc07be8a9e | |||
| 04f8b6b148 | |||
| dbd64e0f4f | |||
| 3acf8ee387 | |||
| 57260a562a |
@@ -34,3 +34,10 @@ de
|
|||||||
resourced
|
resourced
|
||||||
envoyproxy
|
envoyproxy
|
||||||
unipromos
|
unipromos
|
||||||
|
Samsung
|
||||||
|
wenet
|
||||||
|
qwertiko
|
||||||
|
setuplistener
|
||||||
|
mba
|
||||||
|
xfu
|
||||||
|
xou
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ cachediptoasn
|
|||||||
Caddyfile
|
Caddyfile
|
||||||
caninetools
|
caninetools
|
||||||
Cardyb
|
Cardyb
|
||||||
|
CAs
|
||||||
celchecker
|
celchecker
|
||||||
celphase
|
celphase
|
||||||
cerr
|
cerr
|
||||||
@@ -80,6 +81,7 @@ databento
|
|||||||
dayjob
|
dayjob
|
||||||
dco
|
dco
|
||||||
DDOS
|
DDOS
|
||||||
|
ddwrt
|
||||||
Debian
|
Debian
|
||||||
debrpm
|
debrpm
|
||||||
decaymap
|
decaymap
|
||||||
@@ -102,6 +104,7 @@ duckduckbot
|
|||||||
eerror
|
eerror
|
||||||
ellenjoe
|
ellenjoe
|
||||||
emacs
|
emacs
|
||||||
|
embe
|
||||||
enbyware
|
enbyware
|
||||||
etld
|
etld
|
||||||
everyones
|
everyones
|
||||||
@@ -117,9 +120,9 @@ fahedouch
|
|||||||
fastcgi
|
fastcgi
|
||||||
FCr
|
FCr
|
||||||
fcrdns
|
fcrdns
|
||||||
|
fcvg
|
||||||
fediverse
|
fediverse
|
||||||
ffprobe
|
ffprobe
|
||||||
FFXIV
|
|
||||||
fhdr
|
fhdr
|
||||||
financials
|
financials
|
||||||
finfos
|
finfos
|
||||||
@@ -202,8 +205,10 @@ kagi
|
|||||||
kagibot
|
kagibot
|
||||||
Keyfunc
|
Keyfunc
|
||||||
keypair
|
keypair
|
||||||
|
keypairreloader
|
||||||
KHTML
|
KHTML
|
||||||
kinda
|
kinda
|
||||||
|
kpr
|
||||||
KUBECONFIG
|
KUBECONFIG
|
||||||
lcj
|
lcj
|
||||||
ldflags
|
ldflags
|
||||||
@@ -221,7 +226,6 @@ LLU
|
|||||||
loadbalancer
|
loadbalancer
|
||||||
lol
|
lol
|
||||||
lominsa
|
lominsa
|
||||||
maintainership
|
|
||||||
malware
|
malware
|
||||||
mcr
|
mcr
|
||||||
memes
|
memes
|
||||||
@@ -229,17 +233,18 @@ metarefresh
|
|||||||
metrix
|
metrix
|
||||||
mimi
|
mimi
|
||||||
Minfilia
|
Minfilia
|
||||||
|
minica
|
||||||
mistralai
|
mistralai
|
||||||
mnt
|
mnt
|
||||||
Mojeek
|
Mojeek
|
||||||
mojeekbot
|
mojeekbot
|
||||||
mozilla
|
mozilla
|
||||||
|
mqvh
|
||||||
myclient
|
myclient
|
||||||
mymaster
|
mymaster
|
||||||
mypass
|
mypass
|
||||||
myuser
|
myuser
|
||||||
nbf
|
nbf
|
||||||
Necron
|
|
||||||
nepeat
|
nepeat
|
||||||
netsurf
|
netsurf
|
||||||
nginx
|
nginx
|
||||||
@@ -314,6 +319,7 @@ searchbot
|
|||||||
searx
|
searx
|
||||||
sebest
|
sebest
|
||||||
secretplans
|
secretplans
|
||||||
|
selfsigned
|
||||||
Semrush
|
Semrush
|
||||||
Seo
|
Seo
|
||||||
setsebool
|
setsebool
|
||||||
@@ -334,7 +340,6 @@ spyderbot
|
|||||||
srcip
|
srcip
|
||||||
srv
|
srv
|
||||||
stackoverflow
|
stackoverflow
|
||||||
Stargate
|
|
||||||
startprecmd
|
startprecmd
|
||||||
stoppostcmd
|
stoppostcmd
|
||||||
storetest
|
storetest
|
||||||
@@ -384,6 +389,7 @@ vnd
|
|||||||
VPS
|
VPS
|
||||||
Vultr
|
Vultr
|
||||||
WAIFU
|
WAIFU
|
||||||
|
wcg
|
||||||
weblate
|
weblate
|
||||||
webmaster
|
webmaster
|
||||||
webpage
|
webpage
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: "24.11.0"
|
node-version: "24.11.0"
|
||||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: "1.25.7"
|
go-version: "1.25.7"
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: "24.11.0"
|
node-version: "24.11.0"
|
||||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
|
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ jobs:
|
|||||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: "24.11.0"
|
node-version: "24.11.0"
|
||||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
|
|
||||||
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
|
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
|
||||||
|
|
||||||
- name: Log into registry
|
- name: Log into registry
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Log into registry
|
- name: Log into registry
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: techarohq
|
username: techarohq
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ jobs:
|
|||||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: "24.11.0"
|
node-version: "24.11.0"
|
||||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: "24.11.0"
|
node-version: "24.11.0"
|
||||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: "24.11.0"
|
node-version: "24.11.0"
|
||||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ jobs:
|
|||||||
- palemoon/amd64
|
- palemoon/amd64
|
||||||
#- palemoon/i386
|
#- palemoon/i386
|
||||||
- robots_txt
|
- robots_txt
|
||||||
|
- traefik
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -37,7 +38,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: "24.11.0"
|
node-version: "24.11.0"
|
||||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: check-spelling
|
- name: check-spelling
|
||||||
id: spelling
|
id: spelling
|
||||||
uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25
|
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
|
||||||
with:
|
with:
|
||||||
suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }}
|
suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }}
|
||||||
checkout: true
|
checkout: true
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Log into registry
|
- name: Log into registry
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install CI target SSH key
|
- name: Install CI target SSH key
|
||||||
uses: shimataro/ssh-key-action@6b84f2e793b32fa0b03a379cadadec75cc539391 # v2.8.0
|
uses: shimataro/ssh-key-action@87a8f067114a8ce263df83e9ed5c849953548bc3 # v2.8.1
|
||||||
with:
|
with:
|
||||||
key: ${{ secrets.CI_SSH_KEY }}
|
key: ${{ secrets.CI_SSH_KEY }}
|
||||||
name: id_rsa
|
name: id_rsa
|
||||||
known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }}
|
known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }}
|
||||||
|
|
||||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "stable"
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||||
|
|
||||||
- name: Run zizmor 🌈
|
- name: Run zizmor 🌈
|
||||||
run: uvx zizmor --format sarif . > results.sarif
|
run: uvx zizmor --format sarif . > results.sarif
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload SARIF file
|
- name: Upload SARIF file
|
||||||
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
category: zizmor
|
category: zizmor
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
apiVersion: tekton.dev/v1
|
||||||
|
kind: PipelineRun
|
||||||
|
metadata:
|
||||||
|
generateName: anubis-m-
|
||||||
|
namespace: ci
|
||||||
|
|
||||||
|
spec:
|
||||||
|
params:
|
||||||
|
- name: commit
|
||||||
|
value: "Xe/tekton"
|
||||||
|
- name: branch
|
||||||
|
value: main
|
||||||
|
pipelineRef:
|
||||||
|
name: anubis-build-test
|
||||||
|
taskRunTemplate:
|
||||||
|
serviceAccountName: anubis-k3k
|
||||||
|
timeouts:
|
||||||
|
pipeline: 1h0m0s
|
||||||
|
workspaces:
|
||||||
|
- name: repo
|
||||||
|
volumeClaimTemplate:
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 4Gi
|
||||||
|
- name: go-mod-cache
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: go-mod-cache
|
||||||
|
- name: dockerconfig-atcr
|
||||||
|
secret:
|
||||||
|
secretName: atcr
|
||||||
|
- name: dockerconfig-ghcr
|
||||||
|
secret:
|
||||||
|
secretName: ghcr
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
apiVersion: tekton.dev/v1beta1
|
||||||
|
kind: Pipeline
|
||||||
|
metadata:
|
||||||
|
name: anubis-build-test
|
||||||
|
namespace: ci
|
||||||
|
|
||||||
|
spec:
|
||||||
|
description: |
|
||||||
|
The CI/CD pipeline for Anubis
|
||||||
|
params:
|
||||||
|
- name: repo-url
|
||||||
|
type: string
|
||||||
|
description: "Git repo to clone"
|
||||||
|
default: "https://github.com/TecharoHQ/anubis"
|
||||||
|
- name: "branch"
|
||||||
|
type: string
|
||||||
|
description: "Git branch to operate against"
|
||||||
|
- name: "commit"
|
||||||
|
type: string
|
||||||
|
description: "Git revision to check out"
|
||||||
|
- name: "actor"
|
||||||
|
type: string
|
||||||
|
description: "Tangled actor"
|
||||||
|
default: "did:web:anubis.techaro.lol"
|
||||||
|
- name: docker-image-base
|
||||||
|
type: string
|
||||||
|
description: string prefix for production docker images
|
||||||
|
default: "registry.int.xeserv.us/techarohq"
|
||||||
|
- name: docker-cache
|
||||||
|
type: string
|
||||||
|
description: docker repo to store cache files
|
||||||
|
default: "registry.int.xeserv.us/techarohq/anubis/cache"
|
||||||
|
- name: go-version
|
||||||
|
type: string
|
||||||
|
description: "Go version to use"
|
||||||
|
default: "1.26.3"
|
||||||
|
workspaces:
|
||||||
|
- name: repo
|
||||||
|
description: |
|
||||||
|
Cloned repo files.
|
||||||
|
- name: dockerconfig-atcr
|
||||||
|
description: |
|
||||||
|
Docker config for pushing images to atcr
|
||||||
|
- name: dockerconfig-ghcr
|
||||||
|
description: |
|
||||||
|
Docker config for pushing images to ghcr
|
||||||
|
tasks:
|
||||||
|
- name: fix-permissions
|
||||||
|
taskRef:
|
||||||
|
name: fix-permissions
|
||||||
|
workspaces:
|
||||||
|
- name: dir
|
||||||
|
workspace: repo
|
||||||
|
- name: clone-repo
|
||||||
|
runAfter: ["fix-permissions"]
|
||||||
|
taskRef:
|
||||||
|
name: git-clone-naive
|
||||||
|
workspaces:
|
||||||
|
- name: output
|
||||||
|
workspace: repo
|
||||||
|
params:
|
||||||
|
- name: url
|
||||||
|
value: $(params.repo-url)
|
||||||
|
- name: revision
|
||||||
|
value: $(params.commit)
|
||||||
|
- name: docker-build-ci
|
||||||
|
runAfter: ["clone-repo"]
|
||||||
|
workspaces:
|
||||||
|
- name: source
|
||||||
|
workspace: repo
|
||||||
|
taskRef:
|
||||||
|
name: kaniko
|
||||||
|
params:
|
||||||
|
- name: IMAGE
|
||||||
|
value: $(params.docker-image-base)/anubis/ci:$(tasks.clone-repo.results.version)
|
||||||
|
- name: DOCKERFILE
|
||||||
|
value: ./test/ssh-ci/Dockerfile
|
||||||
|
- name: EXTRA_ARGS
|
||||||
|
value:
|
||||||
|
[
|
||||||
|
"--build-arg=GO_VERSION=$(params.go-version)",
|
||||||
|
"--cache",
|
||||||
|
"--cache-copy-layers",
|
||||||
|
"--cache-run-layers",
|
||||||
|
"--cache-repo=$(params.docker-cache)",
|
||||||
|
"--label=org.tangled.actor=$(params.actor)",
|
||||||
|
"--snapshot-mode=redo",
|
||||||
|
"--use-new-run",
|
||||||
|
]
|
||||||
|
- name: provision-test-cluster
|
||||||
|
runAfter: ["docker-build-ci"]
|
||||||
|
taskSpec:
|
||||||
|
workspaces:
|
||||||
|
- name: repo
|
||||||
|
mountPath: /src
|
||||||
|
results:
|
||||||
|
- name: cluster-name
|
||||||
|
description: "k3k cluster name object in k8s"
|
||||||
|
steps:
|
||||||
|
- name: create-cluster
|
||||||
|
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
|
||||||
|
workingDir: $(workspaces.repo.path)/repo
|
||||||
|
env:
|
||||||
|
- name: NAMESPACE
|
||||||
|
value: $(context.pipelineRun.namespace)
|
||||||
|
- name: PIPELINE_NAME
|
||||||
|
value: $(context.pipeline.name)
|
||||||
|
- name: PIPELINERUN_NAME
|
||||||
|
value: $(context.pipelineRun.name)
|
||||||
|
- name: PIPELINERUN_UID
|
||||||
|
value: $(context.pipelineRun.uid)
|
||||||
|
- name: KUBECONFIG_OUT
|
||||||
|
value: $(workspaces.repo.path)/kube/config
|
||||||
|
script: |
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
./test/k3k/create-cluster.sh > "$(results.cluster-name.path)"
|
||||||
|
- name: build-assets
|
||||||
|
runAfter: ["docker-build-ci"]
|
||||||
|
taskSpec:
|
||||||
|
workspaces:
|
||||||
|
- name: repo
|
||||||
|
mountPath: /src
|
||||||
|
steps:
|
||||||
|
- name: test
|
||||||
|
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
|
||||||
|
workingDir: $(workspaces.repo.path)/repo
|
||||||
|
script: |
|
||||||
|
npm ci
|
||||||
|
npm run assets
|
||||||
|
workspaces:
|
||||||
|
- name: repo
|
||||||
|
workspace: repo
|
||||||
|
- name: go-test
|
||||||
|
runAfter: ["build-assets"]
|
||||||
|
taskSpec:
|
||||||
|
workspaces:
|
||||||
|
- name: repo
|
||||||
|
mountPath: /src
|
||||||
|
steps:
|
||||||
|
- name: test
|
||||||
|
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
|
||||||
|
workingDir: $(workspaces.repo.path)/repo
|
||||||
|
script: |
|
||||||
|
SKIP_INTEGRATION=1 go test ./...
|
||||||
|
workspaces:
|
||||||
|
- name: repo
|
||||||
|
workspace: repo
|
||||||
|
- name: test-anubis
|
||||||
|
runAfter: ["build-assets"]
|
||||||
|
taskRef:
|
||||||
|
name: ko
|
||||||
|
workspaces:
|
||||||
|
- name: source
|
||||||
|
workspace: repo
|
||||||
|
params:
|
||||||
|
- name: VERSION
|
||||||
|
value: $(tasks.clone-repo.results.version)
|
||||||
|
- name: SOURCE_DATE_EPOCH
|
||||||
|
value: $(tasks.clone-repo.results.source-date-epoch)
|
||||||
|
- name: KO_DOCKER_REPO
|
||||||
|
value: $(params.docker-image-base)
|
||||||
|
- name: extra-args
|
||||||
|
value:
|
||||||
|
[
|
||||||
|
"--platform=all",
|
||||||
|
"--base-import-paths",
|
||||||
|
"--tags=$(tasks.clone-repo.results.version)",
|
||||||
|
"--image-label=org.tangled.actor=$(params.actor)",
|
||||||
|
]
|
||||||
|
- name: packages
|
||||||
|
value:
|
||||||
|
- ./cmd/anubis
|
||||||
|
- name: integration
|
||||||
|
runAfter:
|
||||||
|
- "provision-test-cluster"
|
||||||
|
- "build-assets"
|
||||||
|
- "test-anubis"
|
||||||
|
matrix:
|
||||||
|
params:
|
||||||
|
- name: test-case
|
||||||
|
value:
|
||||||
|
- default-config-macro
|
||||||
|
- i18n
|
||||||
|
- robots_txt
|
||||||
|
taskSpec:
|
||||||
|
params:
|
||||||
|
- name: test-case
|
||||||
|
type: string
|
||||||
|
workspaces:
|
||||||
|
- name: repo
|
||||||
|
mountPath: /src
|
||||||
|
steps:
|
||||||
|
- name: exec
|
||||||
|
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
|
||||||
|
workingDir: $(workspaces.repo.path)/repo/test/$(params.test-case)
|
||||||
|
script: |
|
||||||
|
./tekton.sh
|
||||||
|
env:
|
||||||
|
- name: KUBECONFIG
|
||||||
|
value: "$(workspaces.repo.path)/kube/config"
|
||||||
|
finally:
|
||||||
|
- name: teardown-cluster
|
||||||
|
when:
|
||||||
|
- input: "$(tasks.provision-test-cluster.status)"
|
||||||
|
operator: in
|
||||||
|
values: ["Succeeded"]
|
||||||
|
taskSpec:
|
||||||
|
workspaces:
|
||||||
|
- name: repo
|
||||||
|
mountPath: /src
|
||||||
|
steps:
|
||||||
|
- name: delete
|
||||||
|
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
|
||||||
|
workingDir: $(workspaces.repo.path)/repo
|
||||||
|
script: |
|
||||||
|
kubectl delete --ignore-not-found -n $(context.pipelineRun.namespace) clusters.k3k.io/"$(tasks.provision-test-cluster.results.cluster-name)"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace: ci
|
||||||
|
resources:
|
||||||
|
- anubis-test.yaml
|
||||||
|
- rbac.yaml
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: anubis-k3k
|
||||||
|
namespace: ci
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: anubis-k3k
|
||||||
|
namespace: ci
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["k3k.io"]
|
||||||
|
resources: ["clusters"]
|
||||||
|
verbs: ["*"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: anubis-k3k
|
||||||
|
namespace: ci
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: anubis-k3k
|
||||||
|
namespace: ci
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: anubis-k3k
|
||||||
@@ -73,6 +73,15 @@ Anubis is brought to you by sponsors and donors like:
|
|||||||
<a href="https://www.anexia.com/">
|
<a href="https://www.anexia.com/">
|
||||||
<img src="./docs/static/img/sponsors/anexia-cloudsolutions-logo.webp" alt="ANEXIA Cloud Solutions" height="64">
|
<img src="./docs/static/img/sponsors/anexia-cloudsolutions-logo.webp" alt="ANEXIA Cloud Solutions" height="64">
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://dd-wrt.com/">
|
||||||
|
<img src="./docs/static/img/sponsors/ddwrt-logo.webp" alt="embeDD GmbH" height="64">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.qwertiko.de?utm_campaign=github&utm_medium=referral&utm_content=anubis">
|
||||||
|
<img src="./docs/static/img/sponsors/qwertiko-logo.webp" alt="Qwertiko" height="64">
|
||||||
|
</a>
|
||||||
|
<a href="https://wenet.pl/?utm_campaign=github&utm_medium=referral&utm_content=anubis">
|
||||||
|
<img src="./docs/static/img/sponsors/wenet-logo.webp" alt="Wenet" height="64">
|
||||||
|
</a>
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
|||||||
+27
-131
@@ -17,12 +17,10 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/http/pprof"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -33,12 +31,12 @@ import (
|
|||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
libanubis "github.com/TecharoHQ/anubis/lib"
|
libanubis "github.com/TecharoHQ/anubis/lib"
|
||||||
"github.com/TecharoHQ/anubis/lib/config"
|
"github.com/TecharoHQ/anubis/lib/config"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/metrics"
|
||||||
botPolicy "github.com/TecharoHQ/anubis/lib/policy"
|
botPolicy "github.com/TecharoHQ/anubis/lib/policy"
|
||||||
"github.com/TecharoHQ/anubis/lib/thoth"
|
"github.com/TecharoHQ/anubis/lib/thoth"
|
||||||
"github.com/TecharoHQ/anubis/web"
|
"github.com/TecharoHQ/anubis/web"
|
||||||
"github.com/facebookgo/flagenv"
|
"github.com/facebookgo/flagenv"
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -119,33 +117,6 @@ func doHealthCheck() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseBindNetFromAddr determine bind network and address based on the given network and address.
|
|
||||||
func parseBindNetFromAddr(address string) (string, string) {
|
|
||||||
defaultScheme := "http://"
|
|
||||||
if !strings.Contains(address, "://") {
|
|
||||||
if strings.HasPrefix(address, ":") {
|
|
||||||
address = defaultScheme + "localhost" + address
|
|
||||||
} else {
|
|
||||||
address = defaultScheme + address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bindUri, err := url.Parse(address)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(fmt.Errorf("failed to parse bind URL: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
switch bindUri.Scheme {
|
|
||||||
case "unix":
|
|
||||||
return "unix", bindUri.Path
|
|
||||||
case "tcp", "http", "https":
|
|
||||||
return "tcp", bindUri.Host
|
|
||||||
default:
|
|
||||||
log.Fatal(fmt.Errorf("unsupported network scheme %s in address %s", bindUri.Scheme, address))
|
|
||||||
}
|
|
||||||
return "", address
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSameSite(s string) http.SameSite {
|
func parseSameSite(s string) http.SameSite {
|
||||||
switch strings.ToLower(s) {
|
switch strings.ToLower(s) {
|
||||||
case "none":
|
case "none":
|
||||||
@@ -162,53 +133,6 @@ func parseSameSite(s string) http.SameSite {
|
|||||||
return http.SameSiteDefaultMode
|
return http.SameSiteDefaultMode
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupListener(network string, address string) (net.Listener, string) {
|
|
||||||
formattedAddress := ""
|
|
||||||
|
|
||||||
if network == "" {
|
|
||||||
// keep compatibility
|
|
||||||
network, address = parseBindNetFromAddr(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch network {
|
|
||||||
case "unix":
|
|
||||||
formattedAddress = "unix:" + address
|
|
||||||
case "tcp":
|
|
||||||
if strings.HasPrefix(address, ":") { // assume it's just a port e.g. :4259
|
|
||||||
formattedAddress = "http://localhost" + address
|
|
||||||
} else {
|
|
||||||
formattedAddress = "http://" + address
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
formattedAddress = fmt.Sprintf(`(%s) %s`, network, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
listener, err := net.Listen(network, address)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(fmt.Errorf("failed to bind to %s: %w", formattedAddress, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// additional permission handling for unix sockets
|
|
||||||
if network == "unix" {
|
|
||||||
mode, err := strconv.ParseUint(*socketMode, 8, 0)
|
|
||||||
if err != nil {
|
|
||||||
listener.Close()
|
|
||||||
log.Fatal(fmt.Errorf("could not parse socket mode %s: %w", *socketMode, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Chmod(address, os.FileMode(mode))
|
|
||||||
if err != nil {
|
|
||||||
err := listener.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to close listener: %v", err)
|
|
||||||
}
|
|
||||||
log.Fatal(fmt.Errorf("could not change socket mode: %w", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return listener, formattedAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeReverseProxy(target string, targetSNI string, targetHost string, insecureSkipVerify bool, targetDisableKeepAlive bool) (http.Handler, error) {
|
func makeReverseProxy(target string, targetSNI string, targetHost string, insecureSkipVerify bool, targetDisableKeepAlive bool) (http.Handler, error) {
|
||||||
targetUri, err := url.Parse(target)
|
targetUri, err := url.Parse(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -304,11 +228,6 @@ func main() {
|
|||||||
|
|
||||||
wg := new(sync.WaitGroup)
|
wg := new(sync.WaitGroup)
|
||||||
|
|
||||||
if *metricsBind != "" {
|
|
||||||
wg.Add(1)
|
|
||||||
go metricsServer(ctx, *lg.With("subsystem", "metrics"), wg.Done)
|
|
||||||
}
|
|
||||||
|
|
||||||
var rp http.Handler
|
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
|
// 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) != "" {
|
if strings.TrimSpace(*target) != "" {
|
||||||
@@ -340,7 +259,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lg.Info("loading policy file", "fname", *policyFname)
|
lg.Info("loading policy file", "fname", *policyFname)
|
||||||
policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty, *slogLevel)
|
policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty, *slogLevel, strings.TrimSpace(*target) == "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("can't parse policy file: %v", err)
|
log.Fatalf("can't parse policy file: %v", err)
|
||||||
}
|
}
|
||||||
@@ -348,6 +267,26 @@ func main() {
|
|||||||
lg.Debug("swapped to new logger")
|
lg.Debug("swapped to new logger")
|
||||||
slog.SetDefault(lg)
|
slog.SetDefault(lg)
|
||||||
|
|
||||||
|
if *metricsBind != "" || policy.Metrics != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
ms := &metrics.Server{
|
||||||
|
Config: policy.Metrics,
|
||||||
|
Log: lg,
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy.Metrics == nil {
|
||||||
|
lg.Debug("migrating flags to metrics config", "bind", *metricsBind, "network", *metricsBindNetwork, "socket-mode", *socketMode)
|
||||||
|
ms.Config = &config.Metrics{
|
||||||
|
Bind: *metricsBind,
|
||||||
|
Network: *metricsBindNetwork,
|
||||||
|
SocketMode: *socketMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go ms.Run(ctx, wg.Done)
|
||||||
|
}
|
||||||
|
|
||||||
// Warn if persistent storage is used without a configured signing key
|
// Warn if persistent storage is used without a configured signing key
|
||||||
if policy.Store.IsPersistent() {
|
if policy.Store.IsPersistent() {
|
||||||
if *hs512Secret == "" && *ed25519PrivateKeyHex == "" && *ed25519PrivateKeyHexFile == "" {
|
if *hs512Secret == "" && *ed25519PrivateKeyHex == "" && *ed25519PrivateKeyHexFile == "" {
|
||||||
@@ -484,7 +423,11 @@ func main() {
|
|||||||
h = internal.JA4H(h)
|
h = internal.JA4H(h)
|
||||||
|
|
||||||
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
|
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
|
||||||
listener, listenerUrl := setupListener(*bindNetwork, *bind)
|
listener, listenerUrl, err := internal.SetupListener(*bindNetwork, *bind, *socketMode)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("SetupListener(%q, %q, %q): %v", *bindNetwork, *bind, *socketMode, err)
|
||||||
|
}
|
||||||
|
|
||||||
lg.Info(
|
lg.Info(
|
||||||
"listening",
|
"listening",
|
||||||
"url", listenerUrl,
|
"url", listenerUrl,
|
||||||
@@ -519,53 +462,6 @@ func main() {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func metricsServer(ctx context.Context, lg slog.Logger, done func()) {
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("GET /debug/pprof/", pprof.Index)
|
|
||||||
mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
|
|
||||||
mux.HandleFunc("GET /debug/pprof/profile", pprof.Profile)
|
|
||||||
mux.HandleFunc("GET /debug/pprof/symbol", pprof.Symbol)
|
|
||||||
mux.HandleFunc("GET /debug/pprof/trace", pprof.Trace)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
|
|
||||||
listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
|
|
||||||
lg.Debug("listening for metrics", "url", metricsUrl)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if err := srv.Shutdown(c); err != nil {
|
|
||||||
log.Printf("cannot shut down: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractEmbedFS(fsys embed.FS, root string, destDir string) error {
|
func extractEmbedFS(fsys embed.FS, root string, destDir string) error {
|
||||||
return fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
|
return fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -166,6 +166,36 @@ status_codes:
|
|||||||
CHALLENGE: 200
|
CHALLENGE: 200
|
||||||
DENY: 200
|
DENY: 200
|
||||||
|
|
||||||
|
# # Configuration for the metrics server. See the docs for more information:
|
||||||
|
# #
|
||||||
|
# # https://anubis.techaro.lol/docs/admin/policies#metrics-server
|
||||||
|
# #
|
||||||
|
# # This is commented out by default so that command line flags take precedence.
|
||||||
|
# metrics:
|
||||||
|
# bind: ":9090"
|
||||||
|
# network: "tcp"
|
||||||
|
#
|
||||||
|
# # To protect your metrics server with basic auth, set credentials below:
|
||||||
|
# #
|
||||||
|
# # https://anubis.techaro.lol/docs/admin/policies#http-basic-authentication
|
||||||
|
# basicAuth:
|
||||||
|
# username: ""
|
||||||
|
# password: ""
|
||||||
|
#
|
||||||
|
# # To serve metrics over TLS, set the path to the right TLS certificate and key
|
||||||
|
# # here. When the files change on disk, they will automatically be reloaded.
|
||||||
|
# #
|
||||||
|
# # https://anubis.techaro.lol/docs/admin/policies#tls
|
||||||
|
# tls:
|
||||||
|
# certificate: /path/to/tls.crt
|
||||||
|
# key: /path/to/tls.key
|
||||||
|
#
|
||||||
|
# # If you want to secure your metrics endpoint using mutual TLS (mTLS), set
|
||||||
|
# # the path to a certificate authority public certificate here.
|
||||||
|
# #
|
||||||
|
# # https://anubis.techaro.lol/docs/admin/policies#mtls
|
||||||
|
# ca: /path/to/ca.crt
|
||||||
|
|
||||||
# Anubis can store temporary data in one of a few backends. See the storage
|
# Anubis can store temporary data in one of a few backends. See the storage
|
||||||
# backends section of the docs for more information:
|
# backends section of the docs for more information:
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
# - Claude-SearchBot: No published IP allowlist
|
# - Claude-SearchBot: No published IP allowlist
|
||||||
- name: "ai-crawlers-search"
|
- name: "ai-crawlers-search"
|
||||||
user_agent_regex: >-
|
user_agent_regex: >-
|
||||||
OAI-SearchBot|Claude-SearchBot|PerplexityBot
|
OAI-SearchBot|Claude-SearchBot|PerplexityBot|meta-webindexer
|
||||||
action: DENY
|
action: DENY
|
||||||
|
|||||||
+12
-2
@@ -11,15 +11,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
<!-- This changes the project to: -->
|
||||||
|
|
||||||
|
- Patch [GHSA-6wcg-mqvh-fcvg](https://github.com/TecharoHQ/anubis/security/advisories/GHSA-6wcg-mqvh-fcvg) by containing subrequest logic to Anubis instances in subrequest mode.
|
||||||
|
- Implement robot9001 style delays on the honeypot feature so that the first hit takes 1 millisecond, the second takes 2, etc.
|
||||||
|
- Move metrics server configuration to [the policy file](./admin/policies.mdx#metrics-server).
|
||||||
- Expose [pprof endpoints](https://pkg.go.dev/net/http/pprof) on the metrics listener to enable profiling Anubis in production.
|
- Expose [pprof endpoints](https://pkg.go.dev/net/http/pprof) on the metrics listener to enable profiling Anubis in production.
|
||||||
- fix: prevent nil pointer panic in challenge validation when threshold rules match during PassChallenge (#1463)
|
- fix: prevent nil pointer panic in challenge validation when threshold rules match during PassChallenge (#1463)
|
||||||
- Instruct reverse proxies to not cache error pages.
|
- Instruct reverse proxies to not cache error pages.
|
||||||
- Fixed mixed tab/space indentation in Caddy documentation code block
|
- Fixed mixed tab/space indentation in Caddy documentation code block
|
||||||
- Improve error messages and fix broken REDIRECT_DOMAINS link in docs ([#1193](https://github.com/TecharoHQ/anubis/issues/1193))
|
- Improve error messages and fix broken REDIRECT_DOMAINS link in docs ([#1193](https://github.com/TecharoHQ/anubis/issues/1193))
|
||||||
- Add Bulgarian locale ([#1394](https://github.com/TecharoHQ/anubis/pull/1394))
|
- Add Bulgarian locale ([#1394](https://github.com/TecharoHQ/anubis/pull/1394))
|
||||||
|
- Fixed case-sensitivity mismatch in geoipchecker.go
|
||||||
<!-- This changes the project to: -->
|
|
||||||
- Fix CEL internal errors when iterating `headers`/`query` map wrappers by implementing map iterators for `HTTPHeaders` and `URLValues` ([#1465](https://github.com/TecharoHQ/anubis/pull/1465)).
|
- Fix CEL internal errors when iterating `headers`/`query` map wrappers by implementing map iterators for `HTTPHeaders` and `URLValues` ([#1465](https://github.com/TecharoHQ/anubis/pull/1465)).
|
||||||
|
- Enable [metrics serving via TLS](./admin/policies.mdx#tls), including [mutual TLS (mTLS)](./admin/policies.mdx#mtls).
|
||||||
|
- Enable [HTTP basic auth](./admin/policies.mdx#http-basic-authentication) for the metrics server.
|
||||||
|
- Fix a bug in the dataset poisoning maze that could allow denial of service [#1580](https://github.com/TecharoHQ/anubis/issues/1580).
|
||||||
|
- Add config option to add ASN to logs/metrics.
|
||||||
|
- Log weight when issuing challenge
|
||||||
|
- Fix `path_regex` and CEL `path` rules not matching when using Traefik `forwardAuth` middleware. Anubis now checks `X-Forwarded-Uri` (Traefik) in addition to `X-Original-URI` (nginx) when resolving the request path in subrequest mode ([#1628](https://github.com/TecharoHQ/anubis/issues/1628)).
|
||||||
|
|
||||||
## v1.25.0: Necron
|
## v1.25.0: Necron
|
||||||
|
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ Anubis uses these environment variables for configuration:
|
|||||||
| `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. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. |
|
| `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. **Required when using persistent storage backends** (like bbolt) to ensure challenges survive service restarts. When running multiple instances on the same base domain, the key must be the same across all instances. |
|
||||||
| `ERROR_TITLE` | unset | <EO /> If set, override the translation stack to show a custom title for error pages such as "Something went wrong!". See [Customizing messages](./botstopper.mdx#customizing-messages) for more details. |
|
| `ERROR_TITLE` | unset | <EO /> If set, override the translation stack to show a custom title for error pages such as "Something went wrong!". See [Customizing messages](./botstopper.mdx#customizing-messages) for more details. |
|
||||||
| `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. |
|
| `JWT_RESTRICTION_HEADER` | `X-Real-IP` | If set, the JWT is only valid if the current value of this header matches the value when the JWT was created. You can use it e.g. to restrict a JWT to the source IP of the user using `X-Real-IP`. |
|
||||||
| `METRICS_BIND` | `:9090` | The network address that Anubis serves Prometheus metrics on. See `BIND` for more information. |
|
| `METRICS_BIND` | `:9090` | The legacy configuration value for the network address that Anubis serves Prometheus metrics on. Please migrate this to [the policy file](./policies.mdx#metrics-server) as soon as possible. |
|
||||||
| `METRICS_BIND_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. |
|
| `METRICS_BIND_NETWORK` | `tcp` | The legacy configuration value for the address family that Anubis serves Prometheus metrics on. Please migrate this to [the policy file](./policies.mdx#metrics-server) as soon as possible. |
|
||||||
| `OG_EXPIRY_TIME` | `24h` | The expiration time for the Open Graph tag cache. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
| `OG_EXPIRY_TIME` | `24h` | The expiration time for the Open Graph tag cache. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
||||||
| `OG_PASSTHROUGH` | `false` | If set to `true`, Anubis will enable Open Graph tag passthrough. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
| `OG_PASSTHROUGH` | `false` | If set to `true`, Anubis will enable Open Graph tag passthrough. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
||||||
| `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
| `OG_CACHE_CONSIDER_HOST` | `false` | If set to `true`, Anubis will consider the host in the Open Graph tag cache key. Prefer using [the policy file](./configuration/open-graph.mdx) to configure the Open Graph subsystem. |
|
||||||
|
|||||||
@@ -117,6 +117,78 @@ remote_addresses:
|
|||||||
- 100.64.0.0/10
|
- 100.64.0.0/10
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Metrics server
|
||||||
|
|
||||||
|
Anubis includes support for [Prometheus-style metrics](https://prometheus.io/docs/introduction/overview/), allowing systems administrators to monitor Anubis' performance and effectiveness. This is a separate HTTP server with metrics, health checking, and debug routes.
|
||||||
|
|
||||||
|
Anubis' metrics server is configured with the `metrics` block in the configuration file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to bind metrics to a Unix socket, make sure to set the network to `unix` and add a socket mode:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metrics:
|
||||||
|
bind: "/tmp/anubis_metrics.sock"
|
||||||
|
network: unix
|
||||||
|
socketMode: "0700" # must be a string
|
||||||
|
```
|
||||||
|
|
||||||
|
### TLS
|
||||||
|
|
||||||
|
If you want to serve the metrics server over TLS, use the `tls` block:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|
||||||
|
tls:
|
||||||
|
certificate: /path/to/tls.crt
|
||||||
|
key: /path/to/tls.key
|
||||||
|
```
|
||||||
|
|
||||||
|
The certificate and key will automatically be reloaded when the respective files change.
|
||||||
|
|
||||||
|
### mTLS
|
||||||
|
|
||||||
|
If you want to validate requests to ensure that they use a client certificate signed by a certificate authority (mutual TLS or mTLS), set the `ca` value in the `tls` block:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|
||||||
|
tls:
|
||||||
|
certificate: /path/to/tls.crt
|
||||||
|
key: /path/to/tls.key
|
||||||
|
ca: /path/to/ca.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
As it is not expected for certificate authority certificates to change often, the CA certificate will NOT be automatically reloaded when the respective file changes.
|
||||||
|
|
||||||
|
### HTTP basic authentication
|
||||||
|
|
||||||
|
Anubis' metrics server also supports setting HTTP basic auth as a lightweight protection against unauthorized users viewing metrics data. As the basic auth credentials are hardcoded in the configuration file, administrators SHOULD use randomly generated credentials, such as type-4 UUIDs or other high entropy strings. These credentials MUST NOT be sensitive or used to protect other high value systems.
|
||||||
|
|
||||||
|
Configure it with the `basicAuth` block under `metrics`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|
||||||
|
basicAuth:
|
||||||
|
username: azurediamond
|
||||||
|
password: hunter2
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have Python installed, you can generate a high entropy password with `python -c 'import secrets; print(secrets.token_urlsafe(32))'`.
|
||||||
|
|
||||||
## Imprint / Impressum support
|
## Imprint / Impressum support
|
||||||
|
|
||||||
Anubis has support for showing imprint / impressum information. This is defined in the `impressum` block of your configuration. See [Imprint / Impressum configuration](./configuration/impressum.mdx) for more information.
|
Anubis has support for showing imprint / impressum information. This is defined in the `impressum` block of your configuration. See [Imprint / Impressum configuration](./configuration/impressum.mdx) for more information.
|
||||||
@@ -339,6 +411,7 @@ Anubis exposes the following logging settings in the policy file:
|
|||||||
| `level` | [log level](#log-levels) | `info` | The logging level threshold. Any logs that are at or above this threshold will be drained to the sink. Any other logs will be discarded. |
|
| `level` | [log level](#log-levels) | `info` | The logging level threshold. Any logs that are at or above this threshold will be drained to the sink. Any other logs will be discarded. |
|
||||||
| `sink` | string | `stdio`, `file` | The sink where the logs drain to as they are being recorded in Anubis. |
|
| `sink` | string | `stdio`, `file` | The sink where the logs drain to as they are being recorded in Anubis. |
|
||||||
| `parameters` | object | | Parameters for the given logging sink. This will vary based on the logging sink of choice. See below for more information. |
|
| `parameters` | object | | Parameters for the given logging sink. This will vary based on the logging sink of choice. See below for more information. |
|
||||||
|
| `asn` | bool | `true`, `false` | Add ASN information to logs/metrics. (Requires a Thoth client configured) |
|
||||||
|
|
||||||
Anubis supports the following logging sinks:
|
Anubis supports the following logging sinks:
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ Anubis is brought to you by sponsors and donors like:
|
|||||||
height="64"
|
height="64"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://dd-wrt.com/">
|
||||||
|
<img src="/img/sponsors/ddwrt-logo.webp" alt="embeDD GmbH" height="64" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.qwertiko.de?utm_campaign=github&utm_medium=referral&utm_content=anubis">
|
||||||
|
<img src="/img/sponsors/qwertiko-logo.webp" alt="Qwertiko" height="64" />
|
||||||
|
</a>
|
||||||
|
<a href="https://wenet.pl/?utm_campaign=github&utm_medium=referral&utm_content=anubis">
|
||||||
|
<img src="/img/sponsors/wenet-logo.webp" alt="Wenet" height="64" />
|
||||||
|
</a>
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
|||||||
@@ -32,3 +32,14 @@ This interpreter is much slower than native code because it has to translate eac
|
|||||||
Some users choose to disable JIT as a hardening measure against theoretical browser exploits. This is a reasonable choice if you face targeted attacks from well-resourced adversaries (such as nation-state actors), but it comes with real performance costs.
|
Some users choose to disable JIT as a hardening measure against theoretical browser exploits. This is a reasonable choice if you face targeted attacks from well-resourced adversaries (such as nation-state actors), but it comes with real performance costs.
|
||||||
|
|
||||||
If you've disabled JIT and find Anubis checks slow, re-enabling JIT is the fix. There is no way for Anubis to work around this on our end.
|
If you've disabled JIT and find Anubis checks slow, re-enabling JIT is the fix. There is no way for Anubis to work around this on our end.
|
||||||
|
|
||||||
|
## What versions of browsers does Anubis support?
|
||||||
|
|
||||||
|
Anubis is written mainly by a single person in a basement in Canada. As such it is impossible for Anubis to support every version of every browser on the planet. As such, here's a few rules of thumb for the browsers that Anubis focuses on supporting:
|
||||||
|
|
||||||
|
- At least the two (2) most recent LTS releases of Firefox and Chrome.
|
||||||
|
- At least the version of Chromium as used by the Samsung Browser on Android.
|
||||||
|
- At least the last version of Chromium and Firefox that are known to run on Windows 7.
|
||||||
|
- At least the version of Safari that runs on the second-to-oldest iPhone model currently on the market.
|
||||||
|
|
||||||
|
We cannot give more cohesive version bounds than this. If you run into problems, please file an issue. Sometimes you may just need to upgrade hardware though.
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
@@ -106,7 +106,7 @@ require (
|
|||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.16.2 // indirect
|
github.com/go-git/go-git/v5 v5.16.2 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
|||||||
@@ -189,8 +189,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
|
|||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
||||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
github.com/go-jose/go-jose/v3 v3.0.5/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.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 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BasicAuth wraps next in HTTP Basic authentication using the provided
|
||||||
|
// credentials. If either username or password is empty, next is returned
|
||||||
|
// unchanged and a debug log line is emitted.
|
||||||
|
//
|
||||||
|
// Credentials are compared in constant time to avoid leaking information
|
||||||
|
// through timing side channels.
|
||||||
|
func BasicAuth(realm, username, password string, next http.Handler) http.Handler {
|
||||||
|
if username == "" || password == "" {
|
||||||
|
slog.Debug("skipping middleware, basic auth credentials are empty")
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedUser := sha256.Sum256([]byte(username))
|
||||||
|
expectedPass := sha256.Sum256([]byte(password))
|
||||||
|
challenge := fmt.Sprintf("Basic realm=%q, charset=\"UTF-8\"", realm)
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, pass, ok := r.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
unauthorized(w, challenge)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gotUser := sha256.Sum256([]byte(user))
|
||||||
|
gotPass := sha256.Sum256([]byte(pass))
|
||||||
|
|
||||||
|
userMatch := subtle.ConstantTimeCompare(gotUser[:], expectedUser[:])
|
||||||
|
passMatch := subtle.ConstantTimeCompare(gotPass[:], expectedPass[:])
|
||||||
|
|
||||||
|
if userMatch&passMatch != 1 {
|
||||||
|
unauthorized(w, challenge)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func unauthorized(w http.ResponseWriter, challenge string) {
|
||||||
|
w.Header().Set("WWW-Authenticate", challenge)
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func okHandler() http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicAuth(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
realm = "test-realm"
|
||||||
|
username = "admin"
|
||||||
|
password = "hunter2"
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
setAuth bool
|
||||||
|
user string
|
||||||
|
pass string
|
||||||
|
wantStatus int
|
||||||
|
wantBody string
|
||||||
|
wantChall bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid credentials",
|
||||||
|
setAuth: true,
|
||||||
|
user: username,
|
||||||
|
pass: password,
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantBody: "ok",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing credentials",
|
||||||
|
setAuth: false,
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantChall: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong username",
|
||||||
|
setAuth: true,
|
||||||
|
user: "nobody",
|
||||||
|
pass: password,
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantChall: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong password",
|
||||||
|
setAuth: true,
|
||||||
|
user: username,
|
||||||
|
pass: "wrong",
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantChall: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty supplied credentials",
|
||||||
|
setAuth: true,
|
||||||
|
user: "",
|
||||||
|
pass: "",
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantChall: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
h := BasicAuth(realm, username, password, okHandler())
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
if tt.setAuth {
|
||||||
|
req.SetBasicAuth(tt.user, tt.pass)
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != tt.wantStatus {
|
||||||
|
t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantBody != "" && rec.Body.String() != tt.wantBody {
|
||||||
|
t.Errorf("body = %q, want %q", rec.Body.String(), tt.wantBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
chall := rec.Header().Get("WWW-Authenticate")
|
||||||
|
if tt.wantChall {
|
||||||
|
if chall == "" {
|
||||||
|
t.Error("WWW-Authenticate header missing on 401")
|
||||||
|
}
|
||||||
|
if !strings.Contains(chall, realm) {
|
||||||
|
t.Errorf("WWW-Authenticate = %q, want realm %q", chall, realm)
|
||||||
|
}
|
||||||
|
} else if chall != "" {
|
||||||
|
t.Errorf("unexpected WWW-Authenticate header: %q", chall)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicAuthPassthrough(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}{
|
||||||
|
{name: "empty username", username: "", password: "hunter2"},
|
||||||
|
{name: "empty password", username: "admin", password: ""},
|
||||||
|
{name: "both empty", username: "", password: ""},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
h := BasicAuth("realm", tt.username, tt.password, okHandler())
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Errorf("status = %d, want %d (passthrough expected)", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
if rec.Body.String() != "ok" {
|
||||||
|
t.Errorf("body = %q, want %q", rec.Body.String(), "ok")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"math"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -76,13 +77,6 @@ type Impl struct {
|
|||||||
affirmation, body, title spintax.Spintax
|
affirmation, body, title spintax.Spintax
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Impl) incrementUA(ctx context.Context, userAgent string) int {
|
|
||||||
result, _ := i.uaWeight.Get(ctx, internal.SHA256sum(userAgent))
|
|
||||||
result++
|
|
||||||
i.uaWeight.Set(ctx, internal.SHA256sum(userAgent), result, time.Hour)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Impl) incrementNetwork(ctx context.Context, network string) int {
|
func (i *Impl) incrementNetwork(ctx context.Context, network string) int {
|
||||||
result, _ := i.networkWeight.Get(ctx, internal.SHA256sum(network))
|
result, _ := i.networkWeight.Get(ctx, internal.SHA256sum(network))
|
||||||
result++
|
result++
|
||||||
@@ -90,20 +84,19 @@ func (i *Impl) incrementNetwork(ctx context.Context, network string) int {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Impl) CheckUA() checker.Impl {
|
|
||||||
return checker.Func(func(r *http.Request) (bool, error) {
|
|
||||||
result, _ := i.uaWeight.Get(r.Context(), internal.SHA256sum(r.UserAgent()))
|
|
||||||
if result >= 25 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Impl) CheckNetwork() checker.Impl {
|
func (i *Impl) CheckNetwork() checker.Impl {
|
||||||
return checker.Func(func(r *http.Request) (bool, error) {
|
return checker.Func(func(r *http.Request) (bool, error) {
|
||||||
result, _ := i.uaWeight.Get(r.Context(), internal.SHA256sum(r.UserAgent()))
|
realIP, _ := internal.RealIP(r)
|
||||||
|
if !realIP.IsValid() {
|
||||||
|
realIP = netip.MustParseAddr(r.Header.Get("X-Real-Ip"))
|
||||||
|
}
|
||||||
|
|
||||||
|
network, ok := internal.ClampIP(realIP)
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _ := i.networkWeight.Get(r.Context(), internal.SHA256sum(network.String()))
|
||||||
if result >= 25 {
|
if result >= 25 {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
@@ -164,7 +157,6 @@ func (i *Impl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
networkCount := i.incrementNetwork(r.Context(), network.String())
|
networkCount := i.incrementNetwork(r.Context(), network.String())
|
||||||
uaCount := i.incrementUA(r.Context(), r.UserAgent())
|
|
||||||
|
|
||||||
stage := r.PathValue("stage")
|
stage := r.PathValue("stage")
|
||||||
|
|
||||||
@@ -172,11 +164,14 @@ func (i *Impl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
lg.Debug("found new entrance point", "id", id, "stage", stage, "userAgent", r.UserAgent(), "clampedIP", network)
|
lg.Debug("found new entrance point", "id", id, "stage", stage, "userAgent", r.UserAgent(), "clampedIP", network)
|
||||||
} else {
|
} else {
|
||||||
switch {
|
switch {
|
||||||
case networkCount%256 == 0, uaCount%256 == 0:
|
case networkCount%256 == 0:
|
||||||
lg.Warn("found possible crawler", "id", id, "network", network)
|
lg.Warn("found possible crawler", "id", id, "network", network, "userAgent", r.UserAgent())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
millisecondAmount := math.Pow(float64(networkCount), 2)
|
||||||
|
time.Sleep(time.Duration(millisecondAmount) * time.Millisecond)
|
||||||
|
|
||||||
spins := i.makeSpins()
|
spins := i.makeSpins()
|
||||||
affirmations := i.makeAffirmations()
|
affirmations := i.makeAffirmations()
|
||||||
title := i.makeTitle()
|
title := i.makeTitle()
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseBindNetFromAddr determine bind network and address based on the given network and address.
|
||||||
|
func parseBindNetFromAddr(address string) (string, string, error) {
|
||||||
|
defaultScheme := "http://"
|
||||||
|
if !strings.Contains(address, "://") {
|
||||||
|
if strings.HasPrefix(address, ":") {
|
||||||
|
address = defaultScheme + "localhost" + address
|
||||||
|
} else {
|
||||||
|
address = defaultScheme + address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bindUri, err := url.Parse(address)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to parse bind URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch bindUri.Scheme {
|
||||||
|
case "unix":
|
||||||
|
return "unix", bindUri.Path, nil
|
||||||
|
case "tcp", "http", "https":
|
||||||
|
return "tcp", bindUri.Host, nil
|
||||||
|
default:
|
||||||
|
return "", "", fmt.Errorf("unsupported network scheme %s in address %s", bindUri.Scheme, address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupListener sets up a network listener based on the input from configuration
|
||||||
|
// envvars. It returns a network listener and the URL to that listener or an error.
|
||||||
|
func SetupListener(network, address, socketMode string) (net.Listener, string, error) {
|
||||||
|
formattedAddress := ""
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if network == "" {
|
||||||
|
// keep compatibility
|
||||||
|
network, address, err = parseBindNetFromAddr(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("can't parse bind and network: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch network {
|
||||||
|
case "unix":
|
||||||
|
formattedAddress = "unix:" + address
|
||||||
|
case "tcp":
|
||||||
|
if strings.HasPrefix(address, ":") { // assume it's just a port e.g. :4259
|
||||||
|
formattedAddress = "http://localhost" + address
|
||||||
|
} else {
|
||||||
|
formattedAddress = "http://" + address
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
formattedAddress = fmt.Sprintf(`(%s) %s`, network, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen(network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to bind to %s: %w", formattedAddress, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// additional permission handling for unix sockets
|
||||||
|
if network == "unix" {
|
||||||
|
mode, err := strconv.ParseUint(socketMode, 8, 0)
|
||||||
|
if err != nil {
|
||||||
|
ln.Close()
|
||||||
|
return nil, "", fmt.Errorf("could not parse socket mode %s: %w", socketMode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Chmod(address, os.FileMode(mode))
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("could not change socket mode: %w", err)
|
||||||
|
clErr := ln.Close()
|
||||||
|
if clErr != nil {
|
||||||
|
return nil, "", errors.Join(err, clErr)
|
||||||
|
}
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ln, formattedAddress, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseBindNetFromAddr(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
address string
|
||||||
|
wantErr bool
|
||||||
|
network string
|
||||||
|
bind string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple tcp",
|
||||||
|
address: "localhost:9090",
|
||||||
|
wantErr: false,
|
||||||
|
network: "tcp",
|
||||||
|
bind: "localhost:9090",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple unix",
|
||||||
|
address: "unix:///tmp/foo.sock",
|
||||||
|
wantErr: false,
|
||||||
|
network: "unix",
|
||||||
|
bind: "/tmp/foo.sock",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid network",
|
||||||
|
address: "foo:///tmp/bar.sock",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tcp uri",
|
||||||
|
address: "tcp://[::]:9090",
|
||||||
|
wantErr: false,
|
||||||
|
network: "tcp",
|
||||||
|
bind: "[::]:9090",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "http uri",
|
||||||
|
address: "http://[::]:9090",
|
||||||
|
wantErr: false,
|
||||||
|
network: "tcp",
|
||||||
|
bind: "[::]:9090",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "https uri",
|
||||||
|
address: "https://[::]:9090",
|
||||||
|
wantErr: false,
|
||||||
|
network: "tcp",
|
||||||
|
bind: "[::]:9090",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
network, bind, err := parseBindNetFromAddr(tt.address)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tt.wantErr && err == nil:
|
||||||
|
t.Errorf("parseBindNetFromAddr(%q) should have errored but did not", tt.address)
|
||||||
|
case !tt.wantErr && err != nil:
|
||||||
|
t.Errorf("parseBindNetFromAddr(%q) threw an error: %v", tt.address, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if network != tt.network {
|
||||||
|
t.Errorf("parseBindNetFromAddr(%q) wanted network: %q, got: %q", tt.address, tt.network, network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bind != tt.bind {
|
||||||
|
t.Errorf("parseBindNetFromAddr(%q) wanted bind: %q, got: %q", tt.address, tt.bind, bind)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupListener(t *testing.T) {
|
||||||
|
td := t.TempDir()
|
||||||
|
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
network, address, socketMode string
|
||||||
|
wantErr bool
|
||||||
|
socketURLPrefix string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple tcp",
|
||||||
|
network: "",
|
||||||
|
address: ":0",
|
||||||
|
wantErr: false,
|
||||||
|
socketURLPrefix: "http://localhost:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple unix",
|
||||||
|
network: "",
|
||||||
|
address: "unix://" + filepath.Join(td, "a"),
|
||||||
|
socketMode: "0770",
|
||||||
|
wantErr: false,
|
||||||
|
socketURLPrefix: "unix:" + filepath.Join(td, "a"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tcp",
|
||||||
|
network: "tcp",
|
||||||
|
address: ":0",
|
||||||
|
wantErr: false,
|
||||||
|
socketURLPrefix: "http://localhost:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "udp",
|
||||||
|
network: "udp",
|
||||||
|
address: ":0",
|
||||||
|
wantErr: true,
|
||||||
|
socketURLPrefix: "http://localhost:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unix socket",
|
||||||
|
network: "unix",
|
||||||
|
socketMode: "0770",
|
||||||
|
address: filepath.Join(td, "a"),
|
||||||
|
wantErr: false,
|
||||||
|
socketURLPrefix: "unix:" + filepath.Join(td, "a"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid socket mode",
|
||||||
|
network: "unix",
|
||||||
|
socketMode: "taco bell",
|
||||||
|
address: filepath.Join(td, "a"),
|
||||||
|
wantErr: true,
|
||||||
|
socketURLPrefix: "unix:" + filepath.Join(td, "a"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty socket mode",
|
||||||
|
network: "unix",
|
||||||
|
socketMode: "",
|
||||||
|
address: filepath.Join(td, "a"),
|
||||||
|
wantErr: true,
|
||||||
|
socketURLPrefix: "unix:" + filepath.Join(td, "a"),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ln, socketURL, err := SetupListener(tt.network, tt.address, tt.socketMode)
|
||||||
|
switch {
|
||||||
|
case tt.wantErr && err == nil:
|
||||||
|
t.Errorf("SetupListener(%q, %q, %q) should have errored but did not", tt.network, tt.address, tt.socketMode)
|
||||||
|
case !tt.wantErr && err != nil:
|
||||||
|
t.Fatalf("SetupListener(%q, %q, %q) threw an error: %v", tt.network, tt.address, tt.socketMode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ln != nil {
|
||||||
|
defer ln.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantErr && !strings.HasPrefix(socketURL, tt.socketURLPrefix) {
|
||||||
|
t.Errorf("SetupListener(%q, %q, %q) should have returned a URL with prefix %q but got: %q", tt.network, tt.address, tt.socketMode, tt.socketURLPrefix, socketURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.socketMode != "" {
|
||||||
|
mode, err := strconv.ParseUint(tt.socketMode, 8, 0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sockPath := strings.TrimPrefix(socketURL, "unix:")
|
||||||
|
st, err := os.Stat(sockPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't os.Stat(%q): %v", sockPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if st.Mode().Perm() != fs.FileMode(mode) {
|
||||||
|
t.Errorf("file mode of %q should be %s but is actually %s", sockPath, strconv.FormatUint(mode, 8), strconv.FormatUint(uint64(st.Mode()), 8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -595,7 +595,7 @@ func spawnAnubisWithOptions(t *testing.T, basePrefix string) string {
|
|||||||
fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix())
|
fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix())
|
||||||
})
|
})
|
||||||
|
|
||||||
policy, err := libanubis.LoadPoliciesOrDefault(t.Context(), "", anubis.DefaultDifficulty, "info")
|
policy, err := libanubis.LoadPoliciesOrDefault(t.Context(), "", anubis.DefaultDifficulty, "info", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-15
@@ -11,6 +11,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ import (
|
|||||||
"github.com/TecharoHQ/anubis/lib/policy"
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
||||||
"github.com/TecharoHQ/anubis/lib/store"
|
"github.com/TecharoHQ/anubis/lib/store"
|
||||||
|
iptoasnv1 "github.com/TecharoHQ/thoth-proto/gen/techaro/thoth/iptoasn/v1"
|
||||||
|
|
||||||
// challenge implementations
|
// challenge implementations
|
||||||
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
|
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
|
||||||
@@ -39,31 +41,52 @@ import (
|
|||||||
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
|
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const asnContextKey contextKey = iota
|
||||||
|
|
||||||
|
type asnInfo struct {
|
||||||
|
ASN string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func asnFromContext(ctx context.Context) (string, string) {
|
||||||
|
if v, ok := ctx.Value(asnContextKey).(asnInfo); ok {
|
||||||
|
return v.ASN, v.Description
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
challengesIssued = promauto.NewCounterVec(prometheus.CounterOpts{
|
challengesIssued = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_challenges_issued",
|
Name: "anubis_challenges_issued",
|
||||||
Help: "The total number of challenges issued",
|
Help: "The total number of challenges issued",
|
||||||
}, []string{"method"})
|
}, []string{"method", "asn", "asn_description"})
|
||||||
|
|
||||||
challengesValidated = promauto.NewCounterVec(prometheus.CounterOpts{
|
challengesValidated = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_challenges_validated",
|
Name: "anubis_challenges_validated",
|
||||||
Help: "The total number of challenges validated",
|
Help: "The total number of challenges validated",
|
||||||
}, []string{"method"})
|
}, []string{"method", "asn", "asn_description"})
|
||||||
|
|
||||||
droneBLHits = promauto.NewCounterVec(prometheus.CounterOpts{
|
droneBLHits = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_dronebl_hits",
|
Name: "anubis_dronebl_hits",
|
||||||
Help: "The total number of hits from DroneBL",
|
Help: "The total number of hits from DroneBL",
|
||||||
}, []string{"status"})
|
}, []string{"status", "asn", "asn_description"})
|
||||||
|
|
||||||
failedValidations = promauto.NewCounterVec(prometheus.CounterOpts{
|
failedValidations = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_failed_validations",
|
Name: "anubis_failed_validations",
|
||||||
Help: "The total number of failed validations",
|
Help: "The total number of failed validations",
|
||||||
}, []string{"method"})
|
}, []string{"method", "asn", "asn_description"})
|
||||||
|
|
||||||
requestsProxied = promauto.NewCounterVec(prometheus.CounterOpts{
|
requestsProxied = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_proxied_requests_total",
|
Name: "anubis_proxied_requests_total",
|
||||||
Help: "Number of requests proxied through Anubis to upstream targets",
|
Help: "Number of requests proxied through Anubis to upstream targets",
|
||||||
}, []string{"host"})
|
}, []string{"host", "asn", "asn_description"})
|
||||||
|
|
||||||
|
requestsByASN = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Name: "anubis_requests_by_asn_total",
|
||||||
|
Help: "Number of requests by ASN",
|
||||||
|
}, []string{"asn", "asn_description"})
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@@ -78,6 +101,28 @@ type Server struct {
|
|||||||
hs512Secret []byte
|
hs512Secret []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) getRequestLogger(r *http.Request) (*slog.Logger, *http.Request) {
|
||||||
|
lg := internal.GetRequestLogger(s.logger, r)
|
||||||
|
|
||||||
|
if s.policy.LogASN && s.policy.ThothClient != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ip := r.Header.Get("X-Real-Ip")
|
||||||
|
if info, err := s.policy.ThothClient.IPToASN.Lookup(ctx, &iptoasnv1.LookupRequest{IpAddress: ip}); err == nil && info.GetAnnounced() {
|
||||||
|
asn := strconv.FormatUint(uint64(info.GetAsNumber()), 10)
|
||||||
|
lg = lg.With("asn", info.GetAsNumber(), "asn_description", info.GetDescription())
|
||||||
|
requestsByASN.WithLabelValues(asn, info.GetDescription()).Inc()
|
||||||
|
r = r.WithContext(context.WithValue(r.Context(), asnContextKey, asnInfo{
|
||||||
|
ASN: asn,
|
||||||
|
Description: info.GetDescription(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lg, r
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) getTokenKeyfunc() jwt.Keyfunc {
|
func (s *Server) getTokenKeyfunc() jwt.Keyfunc {
|
||||||
// return ED25519 key if HS512 is not set
|
// return ED25519 key if HS512 is not set
|
||||||
if len(s.hs512Secret) == 0 {
|
if len(s.hs512Secret) == 0 {
|
||||||
@@ -141,7 +186,7 @@ func (s *Server) issueChallenge(ctx context.Context, r *http.Request, lg *slog.L
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lg.Info("new challenge issued", "challenge", id.String())
|
lg.Info("new challenge issued", "challenge", id.String(), "weight", cr.Weight)
|
||||||
|
|
||||||
return &chall, err
|
return &chall, err
|
||||||
}
|
}
|
||||||
@@ -193,7 +238,7 @@ func (s *Server) maybeReverseProxyOrPage(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
|
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, r := s.getRequestLogger(r)
|
||||||
|
|
||||||
if val, _ := s.store.Get(r.Context(), fmt.Sprintf("ogtags:allow:%s%s", r.Host, r.URL.String())); val != nil {
|
if val, _ := s.store.Get(r.Context(), fmt.Sprintf("ogtags:allow:%s%s", r.Host, r.URL.String())); val != nil {
|
||||||
lg.Debug("serving opengraph tag asset")
|
lg.Debug("serving opengraph tag asset")
|
||||||
@@ -218,7 +263,10 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
|||||||
r.Header.Add("X-Anubis-Rule", cr.Name)
|
r.Header.Add("X-Anubis-Rule", cr.Name)
|
||||||
r.Header.Add("X-Anubis-Action", string(cr.Rule))
|
r.Header.Add("X-Anubis-Action", string(cr.Rule))
|
||||||
lg = lg.With("check_result", cr)
|
lg = lg.With("check_result", cr)
|
||||||
policy.Applications.WithLabelValues(cr.Name, string(cr.Rule)).Add(1)
|
{
|
||||||
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
policy.Applications.WithLabelValues(cr.Name, string(cr.Rule), asn, asnDesc).Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
ip := r.Header.Get("X-Real-Ip")
|
ip := r.Header.Get("X-Real-Ip")
|
||||||
|
|
||||||
@@ -348,7 +396,8 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
|
|||||||
lg.Error("can't look up ip in dnsbl", "err", err)
|
lg.Error("can't look up ip in dnsbl", "err", err)
|
||||||
}
|
}
|
||||||
db.Set(r.Context(), ip, resp, 24*time.Hour)
|
db.Set(r.Context(), ip, resp, 24*time.Hour)
|
||||||
droneBLHits.WithLabelValues(resp.String()).Inc()
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
droneBLHits.WithLabelValues(resp.String(), asn, asnDesc).Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp != dnsbl.AllGood {
|
if resp != dnsbl.AllGood {
|
||||||
@@ -366,7 +415,7 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, r := s.getRequestLogger(r)
|
||||||
localizer := localization.GetLocalizer(r)
|
localizer := localization.GetLocalizer(r)
|
||||||
|
|
||||||
redir := r.FormValue("redir")
|
redir := r.FormValue("redir")
|
||||||
@@ -435,11 +484,14 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
lg.Debug("made challenge", "challenge", chall, "rules", rule.Challenge, "cr", cr)
|
lg.Debug("made challenge", "challenge", chall, "rules", rule.Challenge, "cr", cr)
|
||||||
challengesIssued.WithLabelValues("api").Inc()
|
{
|
||||||
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
challengesIssued.WithLabelValues("api", asn, asnDesc).Inc()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, r := s.getRequestLogger(r)
|
||||||
localizer := localization.GetLocalizer(r)
|
localizer := localization.GetLocalizer(r)
|
||||||
|
|
||||||
redir := r.FormValue("redir")
|
redir := r.FormValue("redir")
|
||||||
@@ -530,7 +582,8 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := impl.Validate(r, lg, in); err != nil {
|
if err := impl.Validate(r, lg, in); err != nil {
|
||||||
failedValidations.WithLabelValues(rule.Challenge.Algorithm).Inc()
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
failedValidations.WithLabelValues(rule.Challenge.Algorithm, asn, asnDesc).Inc()
|
||||||
var cerr *challenge.Error
|
var cerr *challenge.Error
|
||||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||||
lg.Debug("challenge validate call failed", "err", err)
|
lg.Debug("challenge validate call failed", "err", err)
|
||||||
@@ -590,7 +643,10 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
lg.Debug("can't update information about challenge", "err", err)
|
lg.Debug("can't update information about challenge", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
challengesValidated.WithLabelValues(rule.Challenge.Algorithm).Inc()
|
{
|
||||||
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
challengesValidated.WithLabelValues(rule.Challenge.Algorithm, asn, asnDesc).Inc()
|
||||||
|
}
|
||||||
lg.Debug("challenge passed, redirecting to app")
|
lg.Debug("challenge passed, redirecting to app")
|
||||||
http.Redirect(w, r, redir, http.StatusFound)
|
http.Redirect(w, r, redir, http.StatusFound)
|
||||||
}
|
}
|
||||||
@@ -629,7 +685,8 @@ func (s *Server) check(r *http.Request, lg *slog.Logger) (policy.CheckResult, *p
|
|||||||
return cr("bot/"+b.Name, b.Action, weight), &b, nil
|
return cr("bot/"+b.Name, b.Action, weight), &b, nil
|
||||||
case config.RuleWeigh:
|
case config.RuleWeigh:
|
||||||
lg.Debug("adjusting weight", "name", b.Name, "delta", b.Weight.Adjust)
|
lg.Debug("adjusting weight", "name", b.Name, "delta", b.Weight.Adjust)
|
||||||
policy.Applications.WithLabelValues("bot/"+b.Name, "WEIGH").Add(1)
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
policy.Applications.WithLabelValues("bot/"+b.Name, "WEIGH", asn, asnDesc).Add(1)
|
||||||
weight += b.Weight.Adjust
|
weight += b.Weight.Adjust
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -58,7 +58,7 @@ func loadPolicies(t *testing.T, fname string, difficulty int) *policy.ParsedConf
|
|||||||
|
|
||||||
t.Logf("loading policy file: %s", fname)
|
t.Logf("loading policy file: %s", fname)
|
||||||
|
|
||||||
anubisPolicy, err := LoadPoliciesOrDefault(ctx, fname, difficulty, "info")
|
anubisPolicy, err := LoadPoliciesOrDefault(ctx, fname, difficulty, "info", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -250,7 +250,7 @@ func TestLoadPolicies(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := policy.ParseConfig(t.Context(), fin, fname, 4, "info"); err != nil {
|
if _, err := policy.ParseConfig(t.Context(), fin, fname, 4, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-10
@@ -55,7 +55,7 @@ type Options struct {
|
|||||||
DifficultyInJWT bool
|
DifficultyInJWT bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string) (*policy.ParsedConfig, error) {
|
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string, subrequestMode bool) (*policy.ParsedConfig, error) {
|
||||||
var fin io.ReadCloser
|
var fin io.ReadCloser
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty
|
|||||||
}
|
}
|
||||||
}(fin)
|
}(fin)
|
||||||
|
|
||||||
anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty, logLevel)
|
anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty, logLevel, subrequestMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
||||||
}
|
}
|
||||||
@@ -190,14 +190,6 @@ func New(opts Options) (*Server, error) {
|
|||||||
},
|
},
|
||||||
Name: "honeypot/network",
|
Name: "honeypot/network",
|
||||||
},
|
},
|
||||||
policy.Bot{
|
|
||||||
Rules: mazeGen.CheckUA(),
|
|
||||||
Action: config.RuleWeigh,
|
|
||||||
Weight: &config.Weight{
|
|
||||||
Adjust: 30,
|
|
||||||
},
|
|
||||||
Name: "honeypot/user-agent",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
result.logger.Error("can't init honeypot subsystem", "err", err)
|
result.logger.Error("can't init honeypot subsystem", "err", err)
|
||||||
|
|||||||
@@ -334,6 +334,7 @@ type fileConfig struct {
|
|||||||
DNSBL bool `json:"dnsbl"`
|
DNSBL bool `json:"dnsbl"`
|
||||||
DNSTTL DnsTTL `json:"dns_ttl"`
|
DNSTTL DnsTTL `json:"dns_ttl"`
|
||||||
Logging *Logging `json:"logging"`
|
Logging *Logging `json:"logging"`
|
||||||
|
Metrics *Metrics `json:"metrics,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fileConfig) Valid() error {
|
func (c *fileConfig) Valid() error {
|
||||||
@@ -375,6 +376,12 @@ func (c *fileConfig) Valid() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Metrics != nil {
|
||||||
|
if err := c.Metrics.Valid(); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...))
|
return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...))
|
||||||
}
|
}
|
||||||
@@ -417,6 +424,7 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
|||||||
StatusCodes: c.StatusCodes,
|
StatusCodes: c.StatusCodes,
|
||||||
Store: c.Store,
|
Store: c.Store,
|
||||||
Logging: c.Logging,
|
Logging: c.Logging,
|
||||||
|
Metrics: c.Metrics,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.OpenGraph.TimeToLive != "" {
|
if c.OpenGraph.TimeToLive != "" {
|
||||||
@@ -508,6 +516,7 @@ type Config struct {
|
|||||||
Logging *Logging
|
Logging *Logging
|
||||||
DNSBL bool
|
DNSBL bool
|
||||||
DNSTTL DnsTTL
|
DNSTTL DnsTTL
|
||||||
|
Metrics *Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) Valid() error {
|
func (c Config) Valid() error {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Logging struct {
|
|||||||
Sink string `json:"sink"` // Logging sink, either "stdio" or "file"
|
Sink string `json:"sink"` // Logging sink, either "stdio" or "file"
|
||||||
Level *slog.Level `json:"level"` // Log level, if set supersedes the level in flags
|
Level *slog.Level `json:"level"` // Log level, if set supersedes the level in flags
|
||||||
Parameters *LoggingFileConfig `json:"parameters"` // Logging parameters, to be dynamic in the future
|
Parameters *LoggingFileConfig `json:"parameters"` // Logging parameters, to be dynamic in the future
|
||||||
|
LogASN bool `json:"asn" yaml:"asn"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidMetricsConfig = errors.New("config: invalid metrics configuration")
|
||||||
|
ErrInvalidMetricsTLSConfig = errors.New("config: invalid metrics TLS configuration")
|
||||||
|
ErrInvalidMetricsBasicAuthConfig = errors.New("config: invalid metrics basic auth configuration")
|
||||||
|
ErrNoMetricsBind = errors.New("config.Metrics: must define bind")
|
||||||
|
ErrNoMetricsNetwork = errors.New("config.Metrics: must define network")
|
||||||
|
ErrNoMetricsSocketMode = errors.New("config.Metrics: must define socket mode when using unix sockets")
|
||||||
|
ErrInvalidMetricsSocketMode = errors.New("config.Metrics: invalid unix socket mode")
|
||||||
|
ErrInvalidMetricsNetwork = errors.New("config.Metrics: invalid metrics network")
|
||||||
|
ErrNoMetricsTLSCertificate = errors.New("config.Metrics.TLS: must define certificate file")
|
||||||
|
ErrNoMetricsTLSKey = errors.New("config.Metrics.TLS: must define key file")
|
||||||
|
ErrInvalidMetricsTLSKeypair = errors.New("config.Metrics.TLS: keypair is invalid")
|
||||||
|
ErrInvalidMetricsCACertificate = errors.New("config.Metrics.TLS: invalid CA certificate")
|
||||||
|
ErrCantReadFile = errors.New("config: can't read required file")
|
||||||
|
ErrNoMetricsBasicAuthUsername = errors.New("config.Metrics.BasicAuth: must define username")
|
||||||
|
ErrNoMetricsBasicAuthPassword = errors.New("config.Metrics.BasicAuth: must define password")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Metrics struct {
|
||||||
|
Bind string `json:"bind" yaml:"bind"`
|
||||||
|
Network string `json:"network" yaml:"network"`
|
||||||
|
SocketMode string `json:"socketMode" yaml:"socketMode"`
|
||||||
|
TLS *MetricsTLS `json:"tls" yaml:"tls"`
|
||||||
|
BasicAuth *MetricsBasicAuth `json:"basicAuth" yaml:"basicAuth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Metrics) Valid() error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if m.Bind == "" {
|
||||||
|
errs = append(errs, ErrNoMetricsBind)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Network == "" {
|
||||||
|
errs = append(errs, ErrNoMetricsNetwork)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m.Network {
|
||||||
|
case "tcp", "tcp4", "tcp6": // https://pkg.go.dev/net#Listen
|
||||||
|
case "unix":
|
||||||
|
if m.SocketMode == "" {
|
||||||
|
errs = append(errs, ErrNoMetricsSocketMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := strconv.ParseUint(m.SocketMode, 8, 0); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w: %w", ErrInvalidMetricsSocketMode, err))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
errs = append(errs, ErrInvalidMetricsNetwork)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.TLS != nil {
|
||||||
|
if err := m.TLS.Valid(); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.BasicAuth != nil {
|
||||||
|
if err := m.BasicAuth.Valid(); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return errors.Join(ErrInvalidMetricsConfig, errors.Join(errs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsTLS struct {
|
||||||
|
Certificate string `json:"certificate" yaml:"certificate"`
|
||||||
|
Key string `json:"key" yaml:"key"`
|
||||||
|
CA string `json:"ca" yaml:"ca"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *MetricsTLS) Valid() error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if mt.Certificate == "" {
|
||||||
|
errs = append(errs, ErrNoMetricsTLSCertificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := canReadFile(mt.Certificate); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w %s: %w", ErrCantReadFile, mt.Certificate, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if mt.Key == "" {
|
||||||
|
errs = append(errs, ErrNoMetricsTLSKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := canReadFile(mt.Key); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w %s: %w", ErrCantReadFile, mt.Key, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tls.LoadX509KeyPair(mt.Certificate, mt.Key); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w: %w", ErrInvalidMetricsTLSKeypair, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if mt.CA != "" {
|
||||||
|
caCert, err := os.ReadFile(mt.CA)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w %s: %w", ErrCantReadFile, mt.CA, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
certPool := x509.NewCertPool()
|
||||||
|
if !certPool.AppendCertsFromPEM(caCert) {
|
||||||
|
errs = append(errs, fmt.Errorf("%w %s", ErrInvalidMetricsCACertificate, mt.CA))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return errors.Join(ErrInvalidMetricsTLSConfig, errors.Join(errs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func canReadFile(fname string) error {
|
||||||
|
fin, err := os.Open(fname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fin.Close()
|
||||||
|
|
||||||
|
data := make([]byte, 64)
|
||||||
|
if _, err := fin.Read(data); err != nil {
|
||||||
|
return fmt.Errorf("can't read %s: %w", fname, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsBasicAuth struct {
|
||||||
|
Username string `json:"username" yaml:"username"`
|
||||||
|
Password string `json:"password" yaml:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mba *MetricsBasicAuth) Valid() error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if mba.Username == "" {
|
||||||
|
errs = append(errs, ErrNoMetricsBasicAuthUsername)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mba.Password == "" {
|
||||||
|
errs = append(errs, ErrNoMetricsBasicAuthPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return errors.Join(ErrInvalidMetricsBasicAuthConfig, errors.Join(errs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMetricsValid(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
input *Metrics
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic TCP",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic TCP4",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic TCP6",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic unix",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: "/tmp/anubis-metrics.sock",
|
||||||
|
Network: "unix",
|
||||||
|
SocketMode: "0770",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no bind",
|
||||||
|
input: &Metrics{},
|
||||||
|
err: ErrNoMetricsBind,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no network",
|
||||||
|
input: &Metrics{},
|
||||||
|
err: ErrNoMetricsNetwork,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no unix socket mode",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: "/tmp/anubis-metrics.sock",
|
||||||
|
Network: "unix",
|
||||||
|
},
|
||||||
|
err: ErrNoMetricsSocketMode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid unix socket mode",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: "/tmp/anubis-metrics.sock",
|
||||||
|
Network: "unix",
|
||||||
|
SocketMode: "taco bell",
|
||||||
|
},
|
||||||
|
err: ErrInvalidMetricsSocketMode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid network",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "taco",
|
||||||
|
},
|
||||||
|
err: ErrInvalidMetricsNetwork,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid TLS config",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp",
|
||||||
|
TLS: &MetricsTLS{},
|
||||||
|
},
|
||||||
|
err: ErrInvalidMetricsTLSConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selfsigned TLS cert",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp",
|
||||||
|
TLS: &MetricsTLS{
|
||||||
|
Certificate: "./testdata/tls/selfsigned.crt",
|
||||||
|
Key: "./testdata/tls/selfsigned.key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong path to selfsigned TLS cert",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp",
|
||||||
|
TLS: &MetricsTLS{
|
||||||
|
Certificate: "./testdata/tls2/selfsigned.crt",
|
||||||
|
Key: "./testdata/tls2/selfsigned.key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: ErrCantReadFile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unparseable TLS cert",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp",
|
||||||
|
TLS: &MetricsTLS{
|
||||||
|
Certificate: "./testdata/tls/invalid.crt",
|
||||||
|
Key: "./testdata/tls/invalid.key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: ErrInvalidMetricsTLSKeypair,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mTLS with CA",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp",
|
||||||
|
TLS: &MetricsTLS{
|
||||||
|
Certificate: "./testdata/tls/selfsigned.crt",
|
||||||
|
Key: "./testdata/tls/selfsigned.key",
|
||||||
|
CA: "./testdata/tls/minica.pem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mTLS with nonexistent CA",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp",
|
||||||
|
TLS: &MetricsTLS{
|
||||||
|
Certificate: "./testdata/tls/selfsigned.crt",
|
||||||
|
Key: "./testdata/tls/selfsigned.key",
|
||||||
|
CA: "./testdata/tls/nonexistent.crt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: ErrCantReadFile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mTLS with invalid CA",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp",
|
||||||
|
TLS: &MetricsTLS{
|
||||||
|
Certificate: "./testdata/tls/selfsigned.crt",
|
||||||
|
Key: "./testdata/tls/selfsigned.key",
|
||||||
|
CA: "./testdata/tls/invalid.crt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: ErrInvalidMetricsCACertificate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic auth credentials set",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp",
|
||||||
|
BasicAuth: &MetricsBasicAuth{
|
||||||
|
Username: "admin",
|
||||||
|
Password: "hunter2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid basic auth config",
|
||||||
|
input: &Metrics{
|
||||||
|
Bind: ":9090",
|
||||||
|
Network: "tcp",
|
||||||
|
BasicAuth: &MetricsBasicAuth{},
|
||||||
|
},
|
||||||
|
err: ErrInvalidMetricsBasicAuthConfig,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
|
||||||
|
t.Logf("wanted error: %v", tt.err)
|
||||||
|
t.Logf("got error: %v", err)
|
||||||
|
t.Error("validation failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetricsBasicAuthValid(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
input *MetricsBasicAuth
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "both set",
|
||||||
|
input: &MetricsBasicAuth{
|
||||||
|
Username: "admin",
|
||||||
|
Password: "hunter2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty username and password",
|
||||||
|
input: &MetricsBasicAuth{},
|
||||||
|
err: ErrInvalidMetricsBasicAuthConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing username",
|
||||||
|
input: &MetricsBasicAuth{
|
||||||
|
Password: "hunter2",
|
||||||
|
},
|
||||||
|
err: ErrNoMetricsBasicAuthUsername,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing password",
|
||||||
|
input: &MetricsBasicAuth{
|
||||||
|
Username: "admin",
|
||||||
|
},
|
||||||
|
err: ErrNoMetricsBasicAuthPassword,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing both surfaces wrapper error",
|
||||||
|
input: &MetricsBasicAuth{},
|
||||||
|
err: ErrNoMetricsBasicAuthUsername,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing both surfaces password error",
|
||||||
|
input: &MetricsBasicAuth{},
|
||||||
|
err: ErrNoMetricsBasicAuthPassword,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
|
||||||
|
t.Logf("wanted error: %v", tt.err)
|
||||||
|
t.Logf("got error: %v", err)
|
||||||
|
t.Error("validation failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: taco
|
||||||
+5
-1
@@ -5,5 +5,9 @@
|
|||||||
"remote_addresses": ["0.0.0.0/0", "::/0"],
|
"remote_addresses": ["0.0.0.0/0", "::/0"],
|
||||||
"action": "ALLOW"
|
"action": "ALLOW"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"metrics": {
|
||||||
|
"bind": ":9090",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ bots:
|
|||||||
- "0.0.0.0/0"
|
- "0.0.0.0/0"
|
||||||
- "::/0"
|
- "::/0"
|
||||||
action: ALLOW
|
action: ALLOW
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+5
-1
@@ -8,5 +8,9 @@
|
|||||||
"action": "DENY"
|
"action": "DENY"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dnsbl": false
|
"dnsbl": false,
|
||||||
|
"metrics": {
|
||||||
|
"bind": ":9090",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ bots:
|
|||||||
headers_regex:
|
headers_regex:
|
||||||
CF-Worker: .*
|
CF-Worker: .*
|
||||||
action: DENY
|
action: DENY
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ bots:
|
|||||||
asns:
|
asns:
|
||||||
match:
|
match:
|
||||||
- 13335 # Cloudflare
|
- 13335 # Cloudflare
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+5
-1
@@ -5,5 +5,9 @@
|
|||||||
"user_agent_regex": "Mozilla",
|
"user_agent_regex": "Mozilla",
|
||||||
"action": "CHALLENGE"
|
"action": "CHALLENGE"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"metrics": {
|
||||||
|
"bind": ":9090",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,7 @@ bots:
|
|||||||
- name: generic-browser
|
- name: generic-browser
|
||||||
user_agent_regex: Mozilla
|
user_agent_regex: Mozilla
|
||||||
action: CHALLENGE
|
action: CHALLENGE
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
@@ -6,3 +6,7 @@ bots:
|
|||||||
- name: "test"
|
- name: "test"
|
||||||
user_agent_regex: ".*"
|
user_agent_regex: ".*"
|
||||||
action: "DENY"
|
action: "DENY"
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+4
@@ -6,3 +6,7 @@ bots:
|
|||||||
- '"Accept" in headers'
|
- '"Accept" in headers'
|
||||||
- headers["Accept"].contains("text/html")
|
- headers["Accept"].contains("text/html")
|
||||||
- randInt(1) == 0
|
- randInt(1) == 0
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+5
-1
@@ -6,5 +6,9 @@
|
|||||||
"action": "DENY"
|
"action": "DENY"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dnsbl": false
|
"dnsbl": false,
|
||||||
|
"metrics": {
|
||||||
|
"bind": ":9090",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,7 @@ bots:
|
|||||||
- name: everything
|
- name: everything
|
||||||
user_agent_regex: .*
|
user_agent_regex: .*
|
||||||
action: DENY
|
action: DENY
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+4
@@ -4,3 +4,7 @@ bots:
|
|||||||
geoip:
|
geoip:
|
||||||
countries:
|
countries:
|
||||||
- US
|
- US
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+5
-1
@@ -10,5 +10,9 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"metrics": {
|
||||||
|
"bind": ":9090",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -6,3 +6,7 @@ bots:
|
|||||||
- userAgent.startsWith("git/") || userAgent.contains("libgit")
|
- userAgent.startsWith("git/") || userAgent.contains("libgit")
|
||||||
- >
|
- >
|
||||||
"Git-Protocol" in headers && headers["Git-Protocol"] == "version=2"
|
"Git-Protocol" in headers && headers["Git-Protocol"] == "version=2"
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+5
-1
@@ -3,5 +3,9 @@
|
|||||||
{
|
{
|
||||||
"import": "./testdata/hack-test.json"
|
"import": "./testdata/hack-test.json"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"metrics": {
|
||||||
|
"bind": ":9090",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
bots:
|
bots:
|
||||||
- import: ./testdata/hack-test.yaml
|
- import: ./testdata/hack-test.yaml
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
@@ -3,5 +3,9 @@
|
|||||||
{
|
{
|
||||||
"import": "(data)/common/keep-internet-working.yaml"
|
"import": "(data)/common/keep-internet-working.yaml"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"metrics": {
|
||||||
|
"bind": ":9090",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
bots:
|
bots:
|
||||||
- import: (data)/common/keep-internet-working.yaml
|
- import: (data)/common/keep-internet-working.yaml
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+4
@@ -8,3 +8,7 @@ impressum:
|
|||||||
page:
|
page:
|
||||||
title: Test
|
title: Test
|
||||||
body: <p>This is a test</p>
|
body: <p>This is a test</p>
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+4
@@ -13,3 +13,7 @@ logs:
|
|||||||
oldFileTimeFormat: 2006-01-02T15-04-05 # RFC 3339-ish
|
oldFileTimeFormat: 2006-01-02T15-04-05 # RFC 3339-ish
|
||||||
compress: true
|
compress: true
|
||||||
useLocalTime: false # timezone for rotated files is UTC
|
useLocalTime: false # timezone for rotated files is UTC
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+4
@@ -5,3 +5,7 @@ bots:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
sink: "stdio"
|
sink: "stdio"
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+4
@@ -6,3 +6,7 @@ bots:
|
|||||||
adjust: 5
|
adjust: 5
|
||||||
|
|
||||||
thresholds: []
|
thresholds: []
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+5
-1
@@ -75,5 +75,9 @@
|
|||||||
"user_agent_regex": "Mozilla",
|
"user_agent_regex": "Mozilla",
|
||||||
"action": "CHALLENGE"
|
"action": "CHALLENGE"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"metrics": {
|
||||||
|
"bind": ":9090",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,3 +10,7 @@ openGraph:
|
|||||||
default:
|
default:
|
||||||
"og:title": "Xe's magic land of fun"
|
"og:title": "Xe's magic land of fun"
|
||||||
"og:description": "We're no strangers to love, you know the rules and so do I"
|
"og:description": "We're no strangers to love, you know the rules and so do I"
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+4
@@ -4,3 +4,7 @@ bots:
|
|||||||
user_agent_regex: Mozilla
|
user_agent_regex: Mozilla
|
||||||
weight:
|
weight:
|
||||||
adjust: 5
|
adjust: 5
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
@@ -9,5 +9,9 @@
|
|||||||
"status_codes": {
|
"status_codes": {
|
||||||
"CHALLENGE": 200,
|
"CHALLENGE": 200,
|
||||||
"DENY": 200
|
"DENY": 200
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"bind": ":9090",
|
||||||
|
"network": "tcp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,7 @@ bots:
|
|||||||
status_codes:
|
status_codes:
|
||||||
CHALLENGE: 200
|
CHALLENGE: 200
|
||||||
DENY: 200
|
DENY: 200
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
@@ -9,5 +9,9 @@
|
|||||||
"status_codes": {
|
"status_codes": {
|
||||||
"CHALLENGE": 403,
|
"CHALLENGE": 403,
|
||||||
"DENY": 403
|
"DENY": 403
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"bind": ":9090",
|
||||||
|
"network": "tcp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,7 @@ bots:
|
|||||||
status_codes:
|
status_codes:
|
||||||
CHALLENGE: 403
|
CHALLENGE: 403
|
||||||
DENY: 403
|
DENY: 403
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+4
@@ -33,3 +33,7 @@ thresholds:
|
|||||||
challenge:
|
challenge:
|
||||||
algorithm: fast
|
algorithm: fast
|
||||||
difficulty: 4
|
difficulty: 4
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
@@ -2,3 +2,7 @@ bots:
|
|||||||
- name: weight
|
- name: weight
|
||||||
action: WEIGH
|
action: WEIGH
|
||||||
user_agent_regex: Mozilla
|
user_agent_regex: Mozilla
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
bind: ":9090"
|
||||||
|
network: "tcp"
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIB1zCCAVygAwIBAgIIYO0SAFtXlVgwCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
|
||||||
|
bWluaWNhIHJvb3QgY2EgNDE2MmMwMB4XDTI2MDQyMjIzMjUwMVoXDTI4MDUyMjIz
|
||||||
|
MjUwMVowEjEQMA4GA1UEAxMHMS4xLjEuMTB2MBAGByqGSM49AgEGBSuBBAAiA2IA
|
||||||
|
BLsuA2LKGbEBuSA4LTm1KaKc7/QCkUOsipXR4+D5/3sWBZiAH7iWUgHwpx5YZf2q
|
||||||
|
kZn6oRda+ks0JLTQ6VhteQedmb7l86bMeDMR8p4Lg2b38l/xEr7S25UfUDKudXrO
|
||||||
|
AqNxMG8wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
|
||||||
|
BQcDAjAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFE/7VDxF2+cUs9bu0pJM3xoC
|
||||||
|
L1TSMA8GA1UdEQQIMAaHBAEBAQEwCgYIKoZIzj0EAwMDaQAwZgIxAPLXds9MMH4K
|
||||||
|
F5FxTf9i0PKPsLQARsABVTgwB94hMR70rqW8Pwbjl7ZGNaYlaeRHUwIxAPMQ8zoF
|
||||||
|
nim+YS1xLqQek/LXuJto8jxcfkQQBsboVzcTa5uaNRhNd5YwrpomGl3lKA==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBN8QsHxxHGJpStu8K7
|
||||||
|
D/FmaBBNo6c514KGFSIfqGFuREF5aOL3gN/W11yk2OIibdWhZANiAAS7LgNiyhmx
|
||||||
|
AbkgOC05tSminO/0ApFDrIqV0ePg+f97FgWYgB+4llIB8KceWGX9qpGZ+qEXWvpL
|
||||||
|
NCS00OlYbXkHnZm+5fOmzHgzEfKeC4Nm9/Jf8RK+0tuVH1AyrnV6zgI=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDr9QQo7ZaTgUL6d73G
|
||||||
|
2BG7+YRTFJHAZa0FogRglfc+jYttL1J4/xTig3RmHoqSgrehZANiAASDhijM9Xe0
|
||||||
|
G9Vam6AJMeKC6aWDNSLwrxNVmPxemsY/yJ1urBgnxRd9GFH6YW1ki/B8rS+Xl1UX
|
||||||
|
NnhBrukLaXvgAQQq782/5IUYGsvK5jw8+dSscYVMCQJwGfmQuaNeczQ=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIB+zCCAYKgAwIBAgIIQWLAtv4ijQ0wCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
|
||||||
|
bWluaWNhIHJvb3QgY2EgNDE2MmMwMCAXDTI2MDQyMjIzMjUwMVoYDzIxMjYwNDIy
|
||||||
|
MjMyNTAxWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA0MTYyYzAwdjAQBgcq
|
||||||
|
hkjOPQIBBgUrgQQAIgNiAASDhijM9Xe0G9Vam6AJMeKC6aWDNSLwrxNVmPxemsY/
|
||||||
|
yJ1urBgnxRd9GFH6YW1ki/B8rS+Xl1UXNnhBrukLaXvgAQQq782/5IUYGsvK5jw8
|
||||||
|
+dSscYVMCQJwGfmQuaNeczSjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQW
|
||||||
|
MBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud
|
||||||
|
DgQWBBRP+1Q8RdvnFLPW7tKSTN8aAi9U0jAfBgNVHSMEGDAWgBRP+1Q8RdvnFLPW
|
||||||
|
7tKSTN8aAi9U0jAKBggqhkjOPQQDAwNnADBkAjBfY7vb7cuLTjg7uoe+kl07FMYT
|
||||||
|
BGMSnWdhN3yXqMUS3A6XZxD/LntXT6V7yFOlAJYCMH7w8/ATYaTqbk2jBRyQt9/x
|
||||||
|
ajN+kZ6ZK+fKttqE8CD62mbHg09xoNxRq+K2I3PVyQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBnzCCAVGgAwIBAgIUK39B3Ft+kU5o81IuISs79O4u1ncwBQYDK2VwMEUxCzAJ
|
||||||
|
BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
|
||||||
|
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjYwNDIyMTQyNjE4WhcNMjYwNTIyMTQyNjE4
|
||||||
|
WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY
|
||||||
|
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAfgpAUpp8MIOOdQpH
|
||||||
|
fxaw3R7mFKQRMR6Kmxzk1Xn/2VujUzBRMB0GA1UdDgQWBBSmkBmzo0RiZ2iocMR8
|
||||||
|
uIIpz9cZyTAfBgNVHSMEGDAWgBSmkBmzo0RiZ2iocMR8uIIpz9cZyTAPBgNVHRMB
|
||||||
|
Af8EBTADAQH/MAUGAytlcANBAG37XXZrVUUzGyy3T9qsPIzvJQAGpGhdjJ7bt9O6
|
||||||
|
sBhzrliTONPrudYuyUggWsHgFb0JlN2xs4/2HhKU+PY7AAQ=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEIL0HxjjfVlg6zQPB9/zTLq0IBzfp8gEoifEYzQZYIj+T
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
+4
-4
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestInvalidChallengeMethod(t *testing.T) {
|
func TestInvalidChallengeMethod(t *testing.T) {
|
||||||
if _, err := LoadPoliciesOrDefault(t.Context(), "testdata/invalid-challenge-method.yaml", 4, "info"); !errors.Is(err, policy.ErrChallengeRuleHasWrongAlgorithm) {
|
if _, err := LoadPoliciesOrDefault(t.Context(), "testdata/invalid-challenge-method.yaml", 4, "info", false); !errors.Is(err, policy.ErrChallengeRuleHasWrongAlgorithm) {
|
||||||
t.Fatalf("wanted error %v but got %v", policy.ErrChallengeRuleHasWrongAlgorithm, err)
|
t.Fatalf("wanted error %v but got %v", policy.ErrChallengeRuleHasWrongAlgorithm, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ func TestBadConfigs(t *testing.T) {
|
|||||||
|
|
||||||
for _, st := range finfos {
|
for _, st := range finfos {
|
||||||
t.Run(st.Name(), func(t *testing.T) {
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "bad", st.Name()), anubis.DefaultDifficulty, "info"); err == nil {
|
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "bad", st.Name()), anubis.DefaultDifficulty, "info", false); err == nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
@@ -44,13 +44,13 @@ func TestGoodConfigs(t *testing.T) {
|
|||||||
t.Run(st.Name(), func(t *testing.T) {
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
t.Run("with-thoth", func(t *testing.T) {
|
t.Run("with-thoth", func(t *testing.T) {
|
||||||
ctx := thothmock.WithMockThoth(t)
|
ctx := thothmock.WithMockThoth(t)
|
||||||
if _, err := LoadPoliciesOrDefault(ctx, filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := LoadPoliciesOrDefault(ctx, filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("without-thoth", func(t *testing.T) {
|
t.Run("without-thoth", func(t *testing.T) {
|
||||||
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+11
-7
@@ -207,7 +207,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, r := s.getRequestLogger(r)
|
||||||
|
|
||||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) {
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) {
|
||||||
lg.Error("client was given a challenge but does not in fact support gzip compression")
|
lg.Error("client was given a challenge but does not in fact support gzip compression")
|
||||||
@@ -215,7 +215,10 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
challengesIssued.WithLabelValues("embedded").Add(1)
|
{
|
||||||
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
challengesIssued.WithLabelValues("embedded", asn, asnDesc).Add(1)
|
||||||
|
}
|
||||||
chall, err := s.issueChallenge(r.Context(), r, lg, cr, rule)
|
chall, err := s.issueChallenge(r.Context(), r, lg, cr, rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("can't get challenge", "err", err)
|
lg.Error("can't get challenge", "err", err)
|
||||||
@@ -306,14 +309,14 @@ func (s *Server) constructRedirectURL(r *http.Request) (string, error) {
|
|||||||
case "http", "https":
|
case "http", "https":
|
||||||
// allowed
|
// allowed
|
||||||
default:
|
default:
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, _ := s.getRequestLogger(r)
|
||||||
lg.Warn("invalid protocol in X-Forwarded-Proto", "proto", proto)
|
lg.Warn("invalid protocol in X-Forwarded-Proto", "proto", proto)
|
||||||
return "", errors.New(localizer.T("invalid_redirect"))
|
return "", errors.New(localizer.T("invalid_redirect"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if host is allowed in RedirectDomains (supports '*' via glob)
|
// Check if host is allowed in RedirectDomains (supports '*' via glob)
|
||||||
if len(s.opts.RedirectDomains) > 0 && !matchRedirectDomain(s.opts.RedirectDomains, host) {
|
if len(s.opts.RedirectDomains) > 0 && !matchRedirectDomain(s.opts.RedirectDomains, host) {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, _ := s.getRequestLogger(r)
|
||||||
lg.Debug("domain not allowed", "domain", host)
|
lg.Debug("domain not allowed", "domain", host)
|
||||||
return "", errors.New(localizer.T("redirect_domain_not_allowed"))
|
return "", errors.New(localizer.T("redirect_domain_not_allowed"))
|
||||||
}
|
}
|
||||||
@@ -415,7 +418,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
|||||||
case "", "http", "https":
|
case "", "http", "https":
|
||||||
// allowed: empty scheme means relative URL
|
// allowed: empty scheme means relative URL
|
||||||
default:
|
default:
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, _ := s.getRequestLogger(r)
|
||||||
lg.Warn("XSS attempt blocked, invalid redirect scheme", "scheme", urlParsed.Scheme, "redir", redir)
|
lg.Warn("XSS attempt blocked, invalid redirect scheme", "scheme", urlParsed.Scheme, "redir", redir)
|
||||||
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), "", http.StatusBadRequest)
|
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -427,7 +430,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
|||||||
hostMismatch := r.URL.Host != "" && urlParsed.Host != "" && urlParsed.Host != r.URL.Host
|
hostMismatch := r.URL.Host != "" && urlParsed.Host != "" && urlParsed.Host != r.URL.Host
|
||||||
|
|
||||||
if hostNotAllowed || hostMismatch {
|
if hostNotAllowed || hostMismatch {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, _ := s.getRequestLogger(r)
|
||||||
lg.Debug("domain not allowed", "domain", urlParsed.Host)
|
lg.Debug("domain not allowed", "domain", urlParsed.Host)
|
||||||
s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), makeCode(err), http.StatusBadRequest)
|
s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), makeCode(err), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -442,7 +445,8 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
|||||||
web.Base(localizer.T("you_are_not_a_bot"), web.StaticHappy(localizer), s.policy.Impressum, localizer),
|
web.Base(localizer.T("you_are_not_a_bot"), web.StaticHappy(localizer), s.policy.Impressum, localizer),
|
||||||
).ServeHTTP(w, r)
|
).ServeHTTP(w, r)
|
||||||
} else {
|
} else {
|
||||||
requestsProxied.WithLabelValues(r.Host).Inc()
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
requestsProxied.WithLabelValues(r.Host, asn, asnDesc).Inc()
|
||||||
r = s.stripBasePrefixFromRequest(r)
|
r = s.stripBasePrefixFromRequest(r)
|
||||||
s.next.ServeHTTP(w, r)
|
s.next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeypairReloader struct {
|
||||||
|
certMu sync.RWMutex
|
||||||
|
cert *tls.Certificate
|
||||||
|
certPath string
|
||||||
|
keyPath string
|
||||||
|
modTime time.Time
|
||||||
|
lg *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKeypairReloader(certPath, keyPath string, lg *slog.Logger) (*KeypairReloader, error) {
|
||||||
|
result := &KeypairReloader{
|
||||||
|
certPath: certPath,
|
||||||
|
keyPath: keyPath,
|
||||||
|
lg: lg,
|
||||||
|
}
|
||||||
|
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.cert = &cert
|
||||||
|
|
||||||
|
st, err := os.Stat(certPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.modTime = st.ModTime()
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kpr *KeypairReloader) maybeReload() error {
|
||||||
|
kpr.lg.Debug("loading new keypair", "cert", kpr.certPath, "key", kpr.keyPath)
|
||||||
|
newCert, err := tls.LoadX509KeyPair(kpr.certPath, kpr.keyPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
st, err := os.Stat(kpr.certPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kpr.certMu.Lock()
|
||||||
|
defer kpr.certMu.Unlock()
|
||||||
|
kpr.cert = &newCert
|
||||||
|
kpr.modTime = st.ModTime()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kpr *KeypairReloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
st, err := os.Stat(kpr.certPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("stat(%q): %w", kpr.certPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
kpr.certMu.RLock()
|
||||||
|
needsReload := st.ModTime().After(kpr.modTime)
|
||||||
|
kpr.certMu.RUnlock()
|
||||||
|
|
||||||
|
if needsReload {
|
||||||
|
if err := kpr.maybeReload(); err != nil {
|
||||||
|
return nil, fmt.Errorf("reload cert: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kpr.certMu.RLock()
|
||||||
|
defer kpr.certMu.RUnlock()
|
||||||
|
return kpr.cert, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func discardLogger() *slog.Logger {
|
||||||
|
return slog.New(slog.DiscardHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeKeypair generates a fresh self-signed cert + RSA key and writes them
|
||||||
|
// as PEM files in dir. Returns the paths and the cert's DER bytes so callers
|
||||||
|
// can identify which pair was loaded.
|
||||||
|
func writeKeypair(t *testing.T, dir, prefix string) (certPath, keyPath string, certDER []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rsa.GenerateKey: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||||
|
Subject: pkix.Name{CommonName: "keypairreloader-test"},
|
||||||
|
NotBefore: time.Now().Add(-time.Hour),
|
||||||
|
NotAfter: time.Now().Add(time.Hour),
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
DNSNames: []string{"keypairreloader-test"},
|
||||||
|
}
|
||||||
|
|
||||||
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("x509.CreateCertificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certPath = filepath.Join(dir, prefix+"cert.pem")
|
||||||
|
keyPath = filepath.Join(dir, prefix+"key.pem")
|
||||||
|
|
||||||
|
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||||
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||||
|
|
||||||
|
if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
|
||||||
|
t.Fatalf("write cert: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
|
||||||
|
t.Fatalf("write key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return certPath, keyPath, der
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewKeypairReloader(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
goodCert, goodKey, _ := writeKeypair(t, dir, "good-")
|
||||||
|
|
||||||
|
garbagePath := filepath.Join(dir, "garbage.pem")
|
||||||
|
if err := os.WriteFile(garbagePath, []byte("not a pem file"), 0o600); err != nil {
|
||||||
|
t.Fatalf("write garbage: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
certPath string
|
||||||
|
keyPath string
|
||||||
|
wantErr error
|
||||||
|
wantNil bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid cert and key",
|
||||||
|
certPath: goodCert,
|
||||||
|
keyPath: goodKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing cert file",
|
||||||
|
certPath: filepath.Join(dir, "does-not-exist.pem"),
|
||||||
|
keyPath: goodKey,
|
||||||
|
wantErr: os.ErrNotExist,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing key file",
|
||||||
|
certPath: goodCert,
|
||||||
|
keyPath: filepath.Join(dir, "does-not-exist-key.pem"),
|
||||||
|
wantErr: os.ErrNotExist,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cert file is garbage",
|
||||||
|
certPath: garbagePath,
|
||||||
|
keyPath: goodKey,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
kpr, err := NewKeypairReloader(tt.certPath, tt.keyPath, discardLogger())
|
||||||
|
|
||||||
|
if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
|
||||||
|
t.Errorf("err = %v, want errors.Is(..., %v)", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if tt.wantErr == nil && !tt.wantNil && err != nil {
|
||||||
|
t.Errorf("unexpected err: %v", err)
|
||||||
|
}
|
||||||
|
if tt.wantNil && kpr != nil {
|
||||||
|
t.Errorf("kpr = %+v, want nil", kpr)
|
||||||
|
}
|
||||||
|
if !tt.wantNil && kpr == nil {
|
||||||
|
t.Errorf("kpr is nil, want non-nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeypairReloader_GetCertificate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
run func(t *testing.T)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "returns loaded cert",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
certPath, keyPath, wantDER := writeKeypair(t, dir, "a-")
|
||||||
|
|
||||||
|
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewKeypairReloader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := kpr.GetCertificate(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCertificate: %v", err)
|
||||||
|
}
|
||||||
|
if len(got.Certificate) == 0 || !bytes.Equal(got.Certificate[0], wantDER) {
|
||||||
|
t.Errorf("GetCertificate returned wrong cert bytes")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reloads when mtime advances",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
certPath, keyPath, _ := writeKeypair(t, dir, "a-")
|
||||||
|
|
||||||
|
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewKeypairReloader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite with a new pair at the same paths and bump mtime.
|
||||||
|
newCertPath, newKeyPath, newDER := writeKeypair(t, dir, "b-")
|
||||||
|
mustRename(t, newCertPath, certPath)
|
||||||
|
mustRename(t, newKeyPath, keyPath)
|
||||||
|
future := time.Now().Add(time.Hour)
|
||||||
|
if err := os.Chtimes(certPath, future, future); err != nil {
|
||||||
|
t.Fatalf("Chtimes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := kpr.GetCertificate(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCertificate: %v", err)
|
||||||
|
}
|
||||||
|
if len(got.Certificate) == 0 || !bytes.Equal(got.Certificate[0], newDER) {
|
||||||
|
t.Errorf("GetCertificate did not return reloaded cert")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not reload when mtime unchanged",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
certPath, keyPath, originalDER := writeKeypair(t, dir, "a-")
|
||||||
|
|
||||||
|
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewKeypairReloader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite the cert/key files with a *different* keypair, then
|
||||||
|
// rewind mtime so the reloader must not pick up the change.
|
||||||
|
newCertPath, newKeyPath, newDER := writeKeypair(t, dir, "b-")
|
||||||
|
mustRename(t, newCertPath, certPath)
|
||||||
|
mustRename(t, newKeyPath, keyPath)
|
||||||
|
past := time.Unix(0, 0)
|
||||||
|
if err := os.Chtimes(certPath, past, past); err != nil {
|
||||||
|
t.Fatalf("Chtimes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := kpr.GetCertificate(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCertificate: %v", err)
|
||||||
|
}
|
||||||
|
if len(got.Certificate) == 0 {
|
||||||
|
t.Fatal("empty cert chain")
|
||||||
|
}
|
||||||
|
if bytes.Equal(got.Certificate[0], newDER) {
|
||||||
|
t.Errorf("GetCertificate reloaded despite unchanged mtime")
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got.Certificate[0], originalDER) {
|
||||||
|
t.Errorf("GetCertificate did not return original cert")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not panic when reload fails after mtime bump",
|
||||||
|
run: func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
certPath, keyPath, _ := writeKeypair(t, dir, "a-")
|
||||||
|
|
||||||
|
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewKeypairReloader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corrupt the cert file and bump mtime. maybeReload will fail.
|
||||||
|
if err := os.WriteFile(certPath, []byte("not a pem file"), 0o600); err != nil {
|
||||||
|
t.Fatalf("corrupt cert: %v", err)
|
||||||
|
}
|
||||||
|
future := time.Now().Add(time.Hour)
|
||||||
|
if err := os.Chtimes(certPath, future, future); err != nil {
|
||||||
|
t.Fatalf("Chtimes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Fatalf("GetCertificate panicked on reload failure: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
got, err := kpr.GetCertificate(nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("GetCertificate returned nil err for corrupt cert; got %+v", got)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tt.run(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustRename(t *testing.T, from, to string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.Rename(from, to); err != nil {
|
||||||
|
t.Fatalf("rename %q -> %q: %v", from, to, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/http/pprof"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/config"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Config *config.Metrics
|
||||||
|
Log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Run(ctx context.Context, done func()) {
|
||||||
|
defer done()
|
||||||
|
lg := s.Log.With("subsystem", "metrics")
|
||||||
|
|
||||||
|
if err := s.run(ctx, lg); err != nil {
|
||||||
|
lg.Error("can't serve metrics server", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) run(ctx context.Context, lg *slog.Logger) error {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("GET /debug/pprof/", pprof.Index)
|
||||||
|
mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
|
||||||
|
mux.HandleFunc("GET /debug/pprof/profile", pprof.Profile)
|
||||||
|
mux.HandleFunc("GET /debug/pprof/symbol", pprof.Symbol)
|
||||||
|
mux.HandleFunc("GET /debug/pprof/trace", pprof.Trace)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := http.Server{
|
||||||
|
Handler: mux,
|
||||||
|
ErrorLog: internal.GetFilteredHTTPLogger(),
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, metricsURL, err := internal.SetupListener(s.Config.Network, s.Config.Bind, s.Config.SocketMode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't setup listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
if s.Config.TLS != nil {
|
||||||
|
kpr, err := NewKeypairReloader(s.Config.TLS.Certificate, s.Config.TLS.Key, lg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't setup keypair reloader: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.TLSConfig = &tls.Config{
|
||||||
|
GetCertificate: kpr.GetCertificate,
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Config.TLS.CA != "" {
|
||||||
|
caCert, err := os.ReadFile(s.Config.TLS.CA)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w %s: %w", config.ErrCantReadFile, s.Config.TLS.CA, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certPool := x509.NewCertPool()
|
||||||
|
if !certPool.AppendCertsFromPEM(caCert) {
|
||||||
|
return fmt.Errorf("%w %s", config.ErrInvalidMetricsCACertificate, s.Config.TLS.CA)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.TLSConfig.ClientCAs = certPool
|
||||||
|
srv.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Config.BasicAuth != nil {
|
||||||
|
var h http.Handler = mux
|
||||||
|
h = internal.BasicAuth("anubis-metrics", s.Config.BasicAuth.Username, s.Config.BasicAuth.Password, mux)
|
||||||
|
|
||||||
|
srv.Handler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
lg.Debug("listening for metrics", "url", metricsURL)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(c); err != nil {
|
||||||
|
lg.Error("can't shut down metrics server", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
switch s.Config.TLS != nil {
|
||||||
|
case true:
|
||||||
|
if err := srv.ServeTLS(ln, "", ""); !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return fmt.Errorf("can't serve TLS metrics server: %w", err)
|
||||||
|
}
|
||||||
|
case false:
|
||||||
|
if err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return fmt.Errorf("can't serve metrics server: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -15,9 +15,10 @@ import (
|
|||||||
type CELChecker struct {
|
type CELChecker struct {
|
||||||
program cel.Program
|
program cel.Program
|
||||||
src string
|
src string
|
||||||
|
subRequestMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns) (*CELChecker, error) {
|
func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns, subRequestMode bool) (*CELChecker, error) {
|
||||||
env, err := expressions.BotEnvironment(dnsObj)
|
env, err := expressions.BotEnvironment(dnsObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -31,6 +32,7 @@ func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns) (*CELChecker,
|
|||||||
return &CELChecker{
|
return &CELChecker{
|
||||||
src: cfg.String(),
|
src: cfg.String(),
|
||||||
program: program,
|
program: program,
|
||||||
|
subRequestMode: subRequestMode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ func (cc *CELChecker) Hash() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cc *CELChecker) Check(r *http.Request) (bool, error) {
|
func (cc *CELChecker) Check(r *http.Request) (bool, error) {
|
||||||
result, _, err := cc.program.ContextEval(r.Context(), &CELRequest{r})
|
result, _, err := cc.program.ContextEval(r.Context(), &CELRequest{r, cc.subRequestMode})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -54,6 +56,7 @@ func (cc *CELChecker) Check(r *http.Request) (bool, error) {
|
|||||||
|
|
||||||
type CELRequest struct {
|
type CELRequest struct {
|
||||||
*http.Request
|
*http.Request
|
||||||
|
subRequestMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *CELRequest) Parent() cel.Activation { return nil }
|
func (cr *CELRequest) Parent() cel.Activation { return nil }
|
||||||
@@ -71,6 +74,14 @@ func (cr *CELRequest) ResolveName(name string) (any, bool) {
|
|||||||
case "userAgent":
|
case "userAgent":
|
||||||
return cr.UserAgent(), true
|
return cr.UserAgent(), true
|
||||||
case "path":
|
case "path":
|
||||||
|
if cr.subRequestMode {
|
||||||
|
if xou := cr.Header.Get("X-Original-URI"); xou != "" {
|
||||||
|
return xou, true
|
||||||
|
}
|
||||||
|
if xfu := cr.Header.Get("X-Forwarded-Uri"); xfu != "" {
|
||||||
|
return xfu, true
|
||||||
|
}
|
||||||
|
}
|
||||||
return cr.URL.Path, true
|
return cr.URL.Path, true
|
||||||
case "query":
|
case "query":
|
||||||
return expressions.URLValues{Values: cr.URL.Query()}, true
|
return expressions.URLValues{Values: cr.URL.Query()}, true
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestCELChecker_MapIterationWrappers(t *testing.T) {
|
|||||||
Expression: `headers.exists(k, k == "Accept") && query.exists(k, k == "format")`,
|
Expression: `headers.exists(k, k == "Accept") && query.exists(k, k == "format")`,
|
||||||
}
|
}
|
||||||
|
|
||||||
checker, err := NewCELChecker(cfg, newTestDNS(t))
|
checker, err := NewCELChecker(cfg, newTestDNS(t), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("creating CEL checker failed: %v", err)
|
t.Fatalf("creating CEL checker failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -42,3 +42,77 @@ func TestCELChecker_MapIterationWrappers(t *testing.T) {
|
|||||||
t.Fatal("expected expression to evaluate true")
|
t.Fatal("expected expression to evaluate true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCELChecker_PathWithForwardedUri(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expression string
|
||||||
|
xForwardedUri string
|
||||||
|
urlPath string
|
||||||
|
subRequestMode bool
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "path matches X-Forwarded-Uri in subrequest mode",
|
||||||
|
expression: `path.startsWith("/admin")`,
|
||||||
|
xForwardedUri: "/admin/secret",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path with query string",
|
||||||
|
expression: `path.startsWith("/api/secret")`,
|
||||||
|
xForwardedUri: "/api/secret?token=abc",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path falls back to url path when no header",
|
||||||
|
expression: `path == "/public/page"`,
|
||||||
|
urlPath: "/public/page",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-subrequest mode ignores X-Forwarded-Uri",
|
||||||
|
expression: `path.startsWith("/admin")`,
|
||||||
|
xForwardedUri: "/admin/secret",
|
||||||
|
urlPath: "/public/page",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := &config.ExpressionOrList{
|
||||||
|
Expression: tt.expression,
|
||||||
|
}
|
||||||
|
checker, err := NewCELChecker(cfg, newTestDNS(t), tt.subRequestMode)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewCELChecker() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.urlPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("http.NewRequest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.xForwardedUri != "" {
|
||||||
|
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := checker.Check(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Check() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Check() = %v, want %v (subRequestMode=%v, urlPath=%q, X-Forwarded-Uri=%q)",
|
||||||
|
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xForwardedUri)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,23 +96,29 @@ func (hmc *HeaderMatchesChecker) Hash() string {
|
|||||||
type PathChecker struct {
|
type PathChecker struct {
|
||||||
regexp *regexp.Regexp
|
regexp *regexp.Regexp
|
||||||
hash string
|
hash string
|
||||||
|
subRequestMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPathChecker(rexStr string) (checker.Impl, error) {
|
func NewPathChecker(rexStr string, subrequestMode bool) (checker.Impl, error) {
|
||||||
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
||||||
}
|
}
|
||||||
return &PathChecker{rex, internal.FastHash(rexStr)}, nil
|
return &PathChecker{rex, internal.FastHash(rexStr), subrequestMode}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pc *PathChecker) Check(r *http.Request) (bool, error) {
|
func (pc *PathChecker) Check(r *http.Request) (bool, error) {
|
||||||
|
if pc.subRequestMode {
|
||||||
originalUrl := r.Header.Get("X-Original-URI")
|
originalUrl := r.Header.Get("X-Original-URI")
|
||||||
|
if originalUrl == "" {
|
||||||
|
originalUrl = r.Header.Get("X-Forwarded-Uri")
|
||||||
|
}
|
||||||
if originalUrl != "" {
|
if originalUrl != "" {
|
||||||
if pc.regexp.MatchString(originalUrl) {
|
if pc.regexp.MatchString(originalUrl) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if pc.regexp.MatchString(r.URL.Path) {
|
if pc.regexp.MatchString(r.URL.Path) {
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|||||||
+223
-2
@@ -272,8 +272,8 @@ func TestPathChecker_XOriginalURI(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Create the PathChecker
|
// Create the PathChecker in subrequest mode so X-Original-URI is honored.
|
||||||
pc, err := NewPathChecker(tt.regex)
|
pc, err := NewPathChecker(tt.regex, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !tt.expectError {
|
if !tt.expectError {
|
||||||
t.Fatalf("NewPathChecker() unexpected error: %v", err)
|
t.Fatalf("NewPathChecker() unexpected error: %v", err)
|
||||||
@@ -305,3 +305,224 @@ func TestPathChecker_XOriginalURI(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPathChecker_GHSA_6wcg_mqvh_fcvg is a regression test for
|
||||||
|
// https://github.com/TecharoHQ/anubis/security/advisories/GHSA-6wcg-mqvh-fcvg.
|
||||||
|
//
|
||||||
|
// PR https://github.com/TecharoHQ/anubis/pull/1015 added the ability for
|
||||||
|
// reverse proxies using Anubis in subrequest auth mode to look at the path
|
||||||
|
// of a request as there are many rules in the wild that rely on checking
|
||||||
|
// the path. This is how access to things like robots.txt or anything in the
|
||||||
|
// .well-known directory is unaffected by Anubis.
|
||||||
|
//
|
||||||
|
// However this logic was also enabled for non-subrequest deployments of Anubis,
|
||||||
|
// meaning that a specially crafted request could include a /.well-known/
|
||||||
|
// path in it and then get around Anubis with little effort.
|
||||||
|
//
|
||||||
|
// This fix gates the logic behind a new plumbed variable named subrequestMode
|
||||||
|
// that only fires when Anubis is running in subrequest auth mode. This
|
||||||
|
// properly contains that workaround so that the logic does not fire in
|
||||||
|
// most deployments.
|
||||||
|
func TestPathChecker_GHSA_6wcg_mqvh_fcvg(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
regex string
|
||||||
|
urlPath string
|
||||||
|
xOriginalURI string
|
||||||
|
subRequestMode bool
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default mode ignores spoofed X-Original-URI when real path matches",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/secret",
|
||||||
|
xOriginalURI: "/public/index",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default mode ignores spoofed X-Original-URI when real path does not match",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/public/index",
|
||||||
|
xOriginalURI: "/admin/secret",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default mode without X-Original-URI matches real path",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/dashboard",
|
||||||
|
xOriginalURI: "",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subrequest mode honors X-Original-URI",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/auth",
|
||||||
|
xOriginalURI: "/admin/secret",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subrequest mode falls back to URL.Path when X-Original-URI does not match",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/dashboard",
|
||||||
|
xOriginalURI: "/public/index",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subrequest mode with empty X-Original-URI uses URL.Path",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/dashboard",
|
||||||
|
xOriginalURI: "",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pc, err := NewPathChecker(tt.regex, tt.subRequestMode)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPathChecker(%q, %v) returned error: %v", tt.regex, tt.subRequestMode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.urlPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("http.NewRequest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.xOriginalURI != "" {
|
||||||
|
req.Header.Set("X-Original-URI", tt.xOriginalURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := pc.Check(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Check() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Check() = %v, want %v (subRequestMode=%v, urlPath=%q, X-Original-URI=%q)",
|
||||||
|
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xOriginalURI)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathChecker_XForwardedUri(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
regex string
|
||||||
|
xForwardedUri string
|
||||||
|
xOriginalURI string
|
||||||
|
urlPath string
|
||||||
|
subRequestMode bool
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "X-Forwarded-Uri matches regex in subrequest mode",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/admin/users",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X-Forwarded-Uri with query string",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/admin/users?page=1",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X-Original-URI takes priority over X-Forwarded-Uri",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/public/page",
|
||||||
|
xOriginalURI: "/admin/users",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "falls back to X-Forwarded-Uri when no X-Original-URI",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/admin/dashboard",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "neither header matches, url path matches",
|
||||||
|
regex: "^/public/.*",
|
||||||
|
xForwardedUri: "/admin/users",
|
||||||
|
urlPath: "/public/page",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nothing matches",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/public/page",
|
||||||
|
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-subrequest mode ignores X-Forwarded-Uri",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/admin/users",
|
||||||
|
urlPath: "/public/page",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-subrequest mode uses url path",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
xForwardedUri: "/public/page",
|
||||||
|
urlPath: "/admin/secret",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty X-Forwarded-Uri falls back to url path",
|
||||||
|
regex: "^/check$",
|
||||||
|
urlPath: "/check",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pc, err := NewPathChecker(tt.regex, tt.subRequestMode)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPathChecker(%q, %v) returned error: %v", tt.regex, tt.subRequestMode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.urlPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("http.NewRequest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.xForwardedUri != "" {
|
||||||
|
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
|
||||||
|
}
|
||||||
|
if tt.xOriginalURI != "" {
|
||||||
|
req.Header.Set("X-Original-URI", tt.xOriginalURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := pc.Check(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Check() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Check() = %v, want %v (subRequestMode=%v, urlPath=%q, X-Forwarded-Uri=%q, X-Original-URI=%q)",
|
||||||
|
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xForwardedUri, tt.xOriginalURI)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+16
-4
@@ -27,7 +27,7 @@ var (
|
|||||||
Applications = promauto.NewCounterVec(prometheus.CounterOpts{
|
Applications = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_policy_results",
|
Name: "anubis_policy_results",
|
||||||
Help: "The results of each policy rule",
|
Help: "The results of each policy rule",
|
||||||
}, []string{"rule", "action"})
|
}, []string{"rule", "action", "asn", "asn_description"})
|
||||||
|
|
||||||
ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid")
|
ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid")
|
||||||
warnedAboutThresholds = &atomic.Bool{}
|
warnedAboutThresholds = &atomic.Bool{}
|
||||||
@@ -46,6 +46,9 @@ type ParsedConfig struct {
|
|||||||
DnsCache *dns.DnsCache
|
DnsCache *dns.DnsCache
|
||||||
Dns *dns.Dns
|
Dns *dns.Dns
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
|
Metrics *config.Metrics
|
||||||
|
ThothClient *thoth.Client
|
||||||
|
LogASN bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newParsedConfig(orig *config.Config) *ParsedConfig {
|
func newParsedConfig(orig *config.Config) *ParsedConfig {
|
||||||
@@ -53,10 +56,11 @@ func newParsedConfig(orig *config.Config) *ParsedConfig {
|
|||||||
orig: orig,
|
orig: orig,
|
||||||
OpenGraph: orig.OpenGraph,
|
OpenGraph: orig.OpenGraph,
|
||||||
StatusCodes: orig.StatusCodes,
|
StatusCodes: orig.StatusCodes,
|
||||||
|
Metrics: orig.Metrics,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDifficulty int, logLevel string) (*ParsedConfig, error) {
|
func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDifficulty int, logLevel string, subrequestMode bool) (*ParsedConfig, error) {
|
||||||
c, err := config.Load(fin, fname)
|
c, err := config.Load(fin, fname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -68,6 +72,10 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
|
|
||||||
result := newParsedConfig(c)
|
result := newParsedConfig(c)
|
||||||
result.DefaultDifficulty = defaultDifficulty
|
result.DefaultDifficulty = defaultDifficulty
|
||||||
|
result.LogASN = c.Logging.LogASN
|
||||||
|
if hasThothClient {
|
||||||
|
result.ThothClient = tc
|
||||||
|
}
|
||||||
|
|
||||||
if c.Logging.Level != nil {
|
if c.Logging.Level != nil {
|
||||||
logLevel = c.Logging.Level.String()
|
logLevel = c.Logging.Level.String()
|
||||||
@@ -92,6 +100,10 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
|
|
||||||
lg := result.Logger.With("at", "config-validate")
|
lg := result.Logger.With("at", "config-validate")
|
||||||
|
|
||||||
|
if result.LogASN && !hasThothClient {
|
||||||
|
lg.Warn("logging.asn is enabled but no Thoth client is configured; ASN logging and metrics will be skipped. Please read https://anubis.techaro.lol/docs/admin/thoth for more information")
|
||||||
|
}
|
||||||
|
|
||||||
stFac, ok := store.Get(c.Store.Backend)
|
stFac, ok := store.Get(c.Store.Backend)
|
||||||
switch ok {
|
switch ok {
|
||||||
case true:
|
case true:
|
||||||
@@ -140,7 +152,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b.PathRegex != nil {
|
if b.PathRegex != nil {
|
||||||
c, err := NewPathChecker(*b.PathRegex)
|
c, err := NewPathChecker(*b.PathRegex, subrequestMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s path regex: %w", b.Name, err))
|
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s path regex: %w", b.Name, err))
|
||||||
} else {
|
} else {
|
||||||
@@ -158,7 +170,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b.Expression != nil {
|
if b.Expression != nil {
|
||||||
c, err := NewCELChecker(b.Expression, result.Dns)
|
c, err := NewCELChecker(b.Expression, result.Dns, subrequestMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
|
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func TestDefaultPolicyMustParse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := ParseConfig(ctx, fin, "botPolicies.yaml", anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := ParseConfig(ctx, fin, "botPolicies.yaml", anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatalf("can't parse config: %v", err)
|
t.Fatalf("can't parse config: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ func TestGoodConfigs(t *testing.T) {
|
|||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
ctx := thothmock.WithMockThoth(t)
|
ctx := thothmock.WithMockThoth(t)
|
||||||
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -53,7 +53,7 @@ func TestGoodConfigs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := ParseConfig(t.Context(), fin, fin.Name(), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := ParseConfig(t.Context(), fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -77,7 +77,7 @@ func TestBadConfigs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info"); err == nil {
|
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err == nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func (c *Client) GeoIPCheckerFor(countries []string) checker.Impl {
|
|||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
fmt.Fprintln(&sb, "GeoIPChecker")
|
fmt.Fprintln(&sb, "GeoIPChecker")
|
||||||
for _, cc := range countries {
|
for _, cc := range countries {
|
||||||
countryMap[cc] = struct{}{}
|
countryMap[strings.ToLower(cc)] = struct{}{}
|
||||||
fmt.Fprintln(&sb, cc)
|
fmt.Fprintln(&sb, cc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+444
-477
File diff suppressed because it is too large
Load Diff
+11
-9
@@ -15,28 +15,30 @@
|
|||||||
"package": "go tool yeet",
|
"package": "go tool yeet",
|
||||||
"lint": "make lint",
|
"lint": "make lint",
|
||||||
"prepare": "husky && go mod download",
|
"prepare": "husky && go mod download",
|
||||||
"format": "prettier -w . 2>&1 >/dev/null && go run goimports -w ."
|
"format": "prettier -w . 2>&1 >/dev/null && go run goimports -w .",
|
||||||
|
"deploy:ci": "kubectl apply -k .tekton -n ci --context admin@alrest",
|
||||||
|
"deploy:ci:invoke": "npm run deploy:ci && kubectl create -f .tekton/anubis-pipelinerun.yaml -n ci --context admin@alrest"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^20.4.3",
|
"@commitlint/cli": "^20.5.3",
|
||||||
"@commitlint/config-conventional": "^20.4.3",
|
"@commitlint/config-conventional": "^20.5.3",
|
||||||
"baseline-browser-mapping": "^2.10.0",
|
"baseline-browser-mapping": "^2.10.27",
|
||||||
"cssnano": "^7.1.3",
|
"cssnano": "^7.1.8",
|
||||||
"cssnano-preset-advanced": "^7.0.11",
|
"cssnano-preset-advanced": "^7.0.16",
|
||||||
"esbuild": "^0.27.3",
|
"esbuild": "^0.28.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"playwright": "^1.52.0",
|
"playwright": "^1.52.0",
|
||||||
"postcss-cli": "^11.0.1",
|
"postcss-cli": "^11.0.1",
|
||||||
"postcss-import": "^16.1.1",
|
"postcss-import": "^16.1.1",
|
||||||
"postcss-import-url": "^7.2.0",
|
"postcss-import-url": "^7.2.0",
|
||||||
"postcss-url": "^10.1.3",
|
"postcss-url": "^10.1.3",
|
||||||
"prettier": "^3.8.1"
|
"prettier": "^3.8.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-crypto/sha256-js": "^5.2.0",
|
"@aws-crypto/sha256-js": "^5.2.0",
|
||||||
"preact": "^10.28.4"
|
"preact": "^10.29.1"
|
||||||
},
|
},
|
||||||
"commitlint": {
|
"commitlint": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user