mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-05 16:28:17 +00:00
Compare commits
4 Commits
Xe/log-fil
...
json/docs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8393df522 | ||
|
|
74c85bb971 | ||
|
|
4a527a304b | ||
|
|
c2ead79823 |
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -9,4 +9,3 @@ Checklist:
|
||||
- [ ] Added a description of the changes to the `[Unreleased]` section of docs/docs/CHANGELOG.md
|
||||
- [ ] Added test cases to [the relevant parts of the codebase](https://anubis.techaro.lol/docs/developer/code-quality)
|
||||
- [ ] Ran integration tests `npm run test:integration` (unsupported on Windows, please use WSL)
|
||||
- [ ] All of my commits have [verified signatures](https://anubis.techaro.lol/docs/developer/signed-commits)
|
||||
|
||||
3
.github/actions/spelling/allow.txt
vendored
3
.github/actions/spelling/allow.txt
vendored
@@ -5,5 +5,4 @@ ubuntu
|
||||
workarounds
|
||||
rjack
|
||||
msgbox
|
||||
xeact
|
||||
ABee
|
||||
xeact
|
||||
7
.github/actions/spelling/expect.txt
vendored
7
.github/actions/spelling/expect.txt
vendored
@@ -140,7 +140,6 @@ headermap
|
||||
healthcheck
|
||||
healthz
|
||||
hec
|
||||
Hetzner
|
||||
hmc
|
||||
homelab
|
||||
hostable
|
||||
@@ -214,7 +213,6 @@ nicksnyder
|
||||
nobots
|
||||
NONINFRINGEMENT
|
||||
nosleep
|
||||
nullglob
|
||||
OCOB
|
||||
ogtag
|
||||
oklch
|
||||
@@ -239,6 +237,7 @@ pki
|
||||
podkova
|
||||
podman
|
||||
poststart
|
||||
poxied
|
||||
prebaked
|
||||
privkey
|
||||
promauto
|
||||
@@ -251,6 +250,7 @@ pwuser
|
||||
qualys
|
||||
qwant
|
||||
qwantbot
|
||||
QWEN
|
||||
rac
|
||||
rawler
|
||||
rcvar
|
||||
@@ -279,10 +279,10 @@ Seo
|
||||
setsebool
|
||||
shellcheck
|
||||
shirou
|
||||
shopt
|
||||
Sidetrade
|
||||
simprint
|
||||
sitemap
|
||||
Slackware
|
||||
sls
|
||||
Smartphone
|
||||
sni
|
||||
@@ -360,7 +360,6 @@ XOriginal
|
||||
XReal
|
||||
yae
|
||||
YAMLTo
|
||||
Yda
|
||||
yeet
|
||||
yeetfile
|
||||
yourdomain
|
||||
|
||||
6
.github/workflows/docker-pr.yml
vendored
6
.github/workflows/docker-pr.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
|
||||
|
||||
10
.github/workflows/docker.yml
vendored
10
.github/workflows/docker.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
brew bundle
|
||||
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
images: ${{ env.IMAGE }}
|
||||
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
SLOG_LEVEL: debug
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||
with:
|
||||
subject-name: ${{ env.IMAGE }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
|
||||
10
.github/workflows/docs-deploy.yml
vendored
10
.github/workflows/docs-deploy.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: techarohq
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
images: ghcr.io/techarohq/anubis/docs
|
||||
tags: |
|
||||
@@ -53,14 +53,14 @@ jobs:
|
||||
push: true
|
||||
|
||||
- name: Apply k8s manifests to limsa lominsa
|
||||
uses: actions-hub/kubectl@af345ed727f0268738e65be48422e463cc67c220 # v1.34.0
|
||||
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||
with:
|
||||
args: apply -k docs/manifest
|
||||
|
||||
- name: Apply k8s manifests to limsa lominsa
|
||||
uses: actions-hub/kubectl@af345ed727f0268738e65be48422e463cc67c220 # v1.34.0
|
||||
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||
with:
|
||||
|
||||
4
.github/workflows/docs-test.yml
vendored
4
.github/workflows/docs-test.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
images: ghcr.io/techarohq/anubis/docs
|
||||
tags: |
|
||||
|
||||
8
.github/workflows/go.yml
vendored
8
.github/workflows/go.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
#runs-on: alrest-techarohq
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
brew bundle
|
||||
|
||||
- name: Setup Golang caches
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
${{ runner.os }}-golang-
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
|
||||
6
.github/workflows/package-builds-stable.yml
vendored
6
.github/workflows/package-builds-stable.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
#runs-on: alrest-techarohq
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-tags: true
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
brew bundle
|
||||
|
||||
- name: Setup Golang caches
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
#runs-on: alrest-techarohq
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-tags: true
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
brew bundle
|
||||
|
||||
- name: Setup Golang caches
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
|
||||
6
.github/workflows/smoke-tests.yml
vendored
6
.github/workflows/smoke-tests.yml
vendored
@@ -24,15 +24,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
|
||||
4
.github/workflows/ssh-ci-runner-cron.yml
vendored
4
.github/workflows/ssh-ci-runner-cron.yml
vendored
@@ -18,13 +18,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
4
.github/workflows/ssh-ci.yml
vendored
4
.github/workflows/ssh-ci.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
- ci@ppc64le.techaro.lol
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
name: id_rsa
|
||||
known_hosts: ${{ secrets.CI_SSH_KNOWN_HOSTS }}
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
|
||||
6
.github/workflows/zizmor.yml
vendored
6
.github/workflows/zizmor.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1
|
||||
uses: github/codeql-action/upload-sarif@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
@@ -31,7 +31,6 @@ import (
|
||||
"github.com/TecharoHQ/anubis/data"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
libanubis "github.com/TecharoHQ/anubis/lib"
|
||||
"github.com/TecharoHQ/anubis/lib/logging"
|
||||
botPolicy "github.com/TecharoHQ/anubis/lib/policy"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/lib/thoth"
|
||||
@@ -52,7 +51,6 @@ var (
|
||||
cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for")
|
||||
cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis")
|
||||
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
|
||||
difficultyInJWT = flag.Bool("difficulty-in-jwt", false, "if true, adds a difficulty field in the JWT claims")
|
||||
useSimplifiedExplanation = flag.Bool("use-simplified-explanation", false, "if true, replaces the text when clicking \"Why am I seeing this?\" with a more simplified text for a non-tech-savvy audience.")
|
||||
forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header")
|
||||
hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set")
|
||||
@@ -251,7 +249,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
slog.SetDefault(slog.New(logging.Init(*slogLevel)))
|
||||
internal.InitSlog(*slogLevel)
|
||||
internal.SetHealth("anubis", healthv1.HealthCheckResponse_NOT_SERVING)
|
||||
|
||||
if *healthcheck {
|
||||
@@ -319,16 +317,6 @@ func main() {
|
||||
log.Fatalf("can't parse policy file: %v", err)
|
||||
}
|
||||
|
||||
// Warn if persistent storage is used without a configured signing key
|
||||
if policy.Store.IsPersistent() {
|
||||
if *hs512Secret == "" && *ed25519PrivateKeyHex == "" && *ed25519PrivateKeyHexFile == "" {
|
||||
slog.Warn("[misconfiguration] persistent storage backend is configured, but no private key is set. " +
|
||||
"Challenges will be invalidated when Anubis restarts. " +
|
||||
"Set HS512_SECRET, ED25519_PRIVATE_KEY_HEX, or ED25519_PRIVATE_KEY_HEX_FILE to ensure challenges survive service restarts. " +
|
||||
"See: https://anubis.techaro.lol/docs/admin/installation#key-generation")
|
||||
}
|
||||
}
|
||||
|
||||
ruleErrorIDs := make(map[string]string)
|
||||
for _, rule := range policy.Bots {
|
||||
if rule.Action != config.RuleDeny {
|
||||
@@ -435,7 +423,6 @@ func main() {
|
||||
CookieSecure: *cookieSecure,
|
||||
PublicUrl: *publicUrl,
|
||||
JWTRestrictionHeader: *jwtRestrictionHeader,
|
||||
DifficultyInJWT: *difficultyInJWT,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("can't construct libanubis.Server: %v", err)
|
||||
@@ -448,10 +435,7 @@ func main() {
|
||||
h = internal.XForwardedForUpdate(*xffStripPrivate, h)
|
||||
h = internal.JA4H(h)
|
||||
|
||||
srv := http.Server{
|
||||
Handler: h,
|
||||
ErrorLog: logging.StdlibLogger(s.GetLogger("http-server").Handler(), slog.LevelDebug),
|
||||
}
|
||||
srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()}
|
||||
listener, listenerUrl := setupListener(*bindNetwork, *bind)
|
||||
slog.Info(
|
||||
"listening",
|
||||
@@ -511,10 +495,7 @@ func metricsServer(ctx context.Context, done func()) {
|
||||
}
|
||||
})
|
||||
|
||||
srv := http.Server{
|
||||
Handler: mux,
|
||||
ErrorLog: logging.StdlibLogger(slog.With("subsystem", "metrics-server").Handler(), slog.LevelDebug),
|
||||
}
|
||||
srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
|
||||
listener, metricsUrl := setupListener(*metricsBindNetwork, *metricsBind)
|
||||
slog.Debug("listening for metrics", "url", metricsUrl)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/logging"
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
"github.com/facebookgo/flagenv"
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ func main() {
|
||||
flagenv.Parse()
|
||||
flag.Parse()
|
||||
|
||||
slog.SetDefault(slog.New(logging.Init(*slogLevel)))
|
||||
internal.InitSlog(*slogLevel)
|
||||
|
||||
koDockerRepo := strings.TrimSuffix(*dockerRepo, "/"+filepath.Base(*dockerRepo))
|
||||
|
||||
@@ -46,11 +46,6 @@ func main() {
|
||||
)
|
||||
}
|
||||
|
||||
if strings.Contains(*dockerTags, ",") {
|
||||
newTags := strings.Join(strings.Split(*dockerTags, ","), "\n")
|
||||
dockerTags = &newTags
|
||||
}
|
||||
|
||||
setOutput("docker_image", strings.SplitN(*dockerTags, "\n", 2)[0])
|
||||
|
||||
version, err := run("git describe --tags --always --dirty")
|
||||
|
||||
@@ -29,7 +29,7 @@ var (
|
||||
)
|
||||
|
||||
type RobotsRule struct {
|
||||
UserAgents []string
|
||||
UserAgent string
|
||||
Disallows []string
|
||||
Allows []string
|
||||
CrawlDelay int
|
||||
@@ -130,26 +130,10 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func createRuleFromAccumulated(userAgents, disallows, allows []string, crawlDelay int) RobotsRule {
|
||||
rule := RobotsRule{
|
||||
UserAgents: make([]string, len(userAgents)),
|
||||
Disallows: make([]string, len(disallows)),
|
||||
Allows: make([]string, len(allows)),
|
||||
CrawlDelay: crawlDelay,
|
||||
}
|
||||
copy(rule.UserAgents, userAgents)
|
||||
copy(rule.Disallows, disallows)
|
||||
copy(rule.Allows, allows)
|
||||
return rule
|
||||
}
|
||||
|
||||
func parseRobotsTxt(input io.Reader) ([]RobotsRule, error) {
|
||||
scanner := bufio.NewScanner(input)
|
||||
var rules []RobotsRule
|
||||
var currentUserAgents []string
|
||||
var currentDisallows []string
|
||||
var currentAllows []string
|
||||
var currentCrawlDelay int
|
||||
var currentRule *RobotsRule
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
@@ -170,42 +154,38 @@ func parseRobotsTxt(input io.Reader) ([]RobotsRule, error) {
|
||||
|
||||
switch directive {
|
||||
case "user-agent":
|
||||
// If we have accumulated rules with directives and encounter a new user-agent,
|
||||
// flush the current rules
|
||||
if len(currentUserAgents) > 0 && (len(currentDisallows) > 0 || len(currentAllows) > 0 || currentCrawlDelay > 0) {
|
||||
rule := createRuleFromAccumulated(currentUserAgents, currentDisallows, currentAllows, currentCrawlDelay)
|
||||
rules = append(rules, rule)
|
||||
// Reset for next group
|
||||
currentUserAgents = nil
|
||||
currentDisallows = nil
|
||||
currentAllows = nil
|
||||
currentCrawlDelay = 0
|
||||
// Start a new rule section
|
||||
if currentRule != nil {
|
||||
rules = append(rules, *currentRule)
|
||||
}
|
||||
currentRule = &RobotsRule{
|
||||
UserAgent: value,
|
||||
Disallows: make([]string, 0),
|
||||
Allows: make([]string, 0),
|
||||
}
|
||||
currentUserAgents = append(currentUserAgents, value)
|
||||
|
||||
case "disallow":
|
||||
if len(currentUserAgents) > 0 && value != "" {
|
||||
currentDisallows = append(currentDisallows, value)
|
||||
if currentRule != nil && value != "" {
|
||||
currentRule.Disallows = append(currentRule.Disallows, value)
|
||||
}
|
||||
|
||||
case "allow":
|
||||
if len(currentUserAgents) > 0 && value != "" {
|
||||
currentAllows = append(currentAllows, value)
|
||||
if currentRule != nil && value != "" {
|
||||
currentRule.Allows = append(currentRule.Allows, value)
|
||||
}
|
||||
|
||||
case "crawl-delay":
|
||||
if len(currentUserAgents) > 0 {
|
||||
if currentRule != nil {
|
||||
if delay, err := parseIntSafe(value); err == nil {
|
||||
currentCrawlDelay = delay
|
||||
currentRule.CrawlDelay = delay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last group of rules
|
||||
if len(currentUserAgents) > 0 {
|
||||
rule := createRuleFromAccumulated(currentUserAgents, currentDisallows, currentAllows, currentCrawlDelay)
|
||||
rules = append(rules, rule)
|
||||
// Don't forget the last rule
|
||||
if currentRule != nil {
|
||||
rules = append(rules, *currentRule)
|
||||
}
|
||||
|
||||
// Mark blacklisted user agents (those with "Disallow: /")
|
||||
@@ -231,11 +211,10 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
|
||||
var anubisRules []AnubisRule
|
||||
ruleCounter := 0
|
||||
|
||||
// Process each robots rule individually
|
||||
for _, robotsRule := range robotsRules {
|
||||
userAgents := robotsRule.UserAgents
|
||||
userAgent := robotsRule.UserAgent
|
||||
|
||||
// Handle crawl delay
|
||||
// Handle crawl delay as weight adjustment (do this first before any continues)
|
||||
if robotsRule.CrawlDelay > 0 && *crawlDelay > 0 {
|
||||
ruleCounter++
|
||||
rule := AnubisRule{
|
||||
@@ -244,32 +223,20 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
|
||||
Weight: &config.Weight{Adjust: *crawlDelay},
|
||||
}
|
||||
|
||||
if len(userAgents) == 1 && userAgents[0] == "*" {
|
||||
if userAgent == "*" {
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
All: []string{"true"}, // Always applies
|
||||
}
|
||||
} else if len(userAgents) == 1 {
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
All: []string{fmt.Sprintf("userAgent.contains(%q)", userAgents[0])},
|
||||
}
|
||||
} else {
|
||||
// Multiple user agents - use any block
|
||||
var expressions []string
|
||||
for _, ua := range userAgents {
|
||||
if ua == "*" {
|
||||
expressions = append(expressions, "true")
|
||||
} else {
|
||||
expressions = append(expressions, fmt.Sprintf("userAgent.contains(%q)", ua))
|
||||
}
|
||||
}
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
Any: expressions,
|
||||
All: []string{fmt.Sprintf("userAgent.contains(%q)", userAgent)},
|
||||
}
|
||||
}
|
||||
|
||||
anubisRules = append(anubisRules, rule)
|
||||
}
|
||||
|
||||
// Handle blacklisted user agents
|
||||
// Handle blacklisted user agents (complete deny/challenge)
|
||||
if robotsRule.IsBlacklist {
|
||||
ruleCounter++
|
||||
rule := AnubisRule{
|
||||
@@ -277,36 +244,21 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
|
||||
Action: *userAgentDeny,
|
||||
}
|
||||
|
||||
if len(userAgents) == 1 {
|
||||
userAgent := userAgents[0]
|
||||
if userAgent == "*" {
|
||||
// This would block everything - convert to a weight adjustment instead
|
||||
rule.Name = fmt.Sprintf("%s-global-restriction-%d", *policyName, ruleCounter)
|
||||
rule.Action = "WEIGH"
|
||||
rule.Weight = &config.Weight{Adjust: 20} // Increase difficulty significantly
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
All: []string{"true"}, // Always applies
|
||||
}
|
||||
} else {
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
All: []string{fmt.Sprintf("userAgent.contains(%q)", userAgent)},
|
||||
}
|
||||
if userAgent == "*" {
|
||||
// This would block everything - convert to a weight adjustment instead
|
||||
rule.Name = fmt.Sprintf("%s-global-restriction-%d", *policyName, ruleCounter)
|
||||
rule.Action = "WEIGH"
|
||||
rule.Weight = &config.Weight{Adjust: 20} // Increase difficulty significantly
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
All: []string{"true"}, // Always applies
|
||||
}
|
||||
} else {
|
||||
// Multiple user agents - use any block
|
||||
var expressions []string
|
||||
for _, ua := range userAgents {
|
||||
if ua == "*" {
|
||||
expressions = append(expressions, "true")
|
||||
} else {
|
||||
expressions = append(expressions, fmt.Sprintf("userAgent.contains(%q)", ua))
|
||||
}
|
||||
}
|
||||
rule.Expression = &config.ExpressionOrList{
|
||||
Any: expressions,
|
||||
All: []string{fmt.Sprintf("userAgent.contains(%q)", userAgent)},
|
||||
}
|
||||
}
|
||||
anubisRules = append(anubisRules, rule)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle specific disallow rules
|
||||
@@ -324,33 +276,9 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
|
||||
// Build CEL expression
|
||||
var conditions []string
|
||||
|
||||
// Add user agent conditions
|
||||
if len(userAgents) == 1 && userAgents[0] == "*" {
|
||||
// Wildcard user agent - no user agent condition needed
|
||||
} else if len(userAgents) == 1 {
|
||||
conditions = append(conditions, fmt.Sprintf("userAgent.contains(%q)", userAgents[0]))
|
||||
} else {
|
||||
// For multiple user agents, we need to use a more complex expression
|
||||
// This is a limitation - we can't easily combine any for user agents with all for path
|
||||
// So we'll create separate rules for each user agent
|
||||
for _, ua := range userAgents {
|
||||
if ua == "*" {
|
||||
continue // Skip wildcard as it's handled separately
|
||||
}
|
||||
ruleCounter++
|
||||
subRule := AnubisRule{
|
||||
Name: fmt.Sprintf("%s-disallow-%d", *policyName, ruleCounter),
|
||||
Action: *baseAction,
|
||||
Expression: &config.ExpressionOrList{
|
||||
All: []string{
|
||||
fmt.Sprintf("userAgent.contains(%q)", ua),
|
||||
buildPathCondition(disallow),
|
||||
},
|
||||
},
|
||||
}
|
||||
anubisRules = append(anubisRules, subRule)
|
||||
}
|
||||
continue
|
||||
// Add user agent condition if not wildcard
|
||||
if userAgent != "*" {
|
||||
conditions = append(conditions, fmt.Sprintf("userAgent.contains(%q)", userAgent))
|
||||
}
|
||||
|
||||
// Add path condition
|
||||
@@ -363,6 +291,7 @@ func convertToAnubisRules(robotsRules []RobotsRule) []AnubisRule {
|
||||
|
||||
anubisRules = append(anubisRules, rule)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return anubisRules
|
||||
|
||||
@@ -78,12 +78,6 @@ func TestDataFileConversion(t *testing.T) {
|
||||
expectedFile: "complex.yaml",
|
||||
options: TestOptions{format: "yaml", crawlDelayWeight: 5},
|
||||
},
|
||||
{
|
||||
name: "consecutive_user_agents",
|
||||
robotsFile: "consecutive.robots.txt",
|
||||
expectedFile: "consecutive.yaml",
|
||||
options: TestOptions{format: "yaml", crawlDelayWeight: 3},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
6
cmd/robots2policy/testdata/blacklist.yaml
vendored
6
cmd/robots2policy/testdata/blacklist.yaml
vendored
@@ -25,6 +25,6 @@
|
||||
- action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("Googlebot")
|
||||
- path.startsWith("/search")
|
||||
name: robots-txt-policy-disallow-7
|
||||
- userAgent.contains("Googlebot")
|
||||
- path.startsWith("/search")
|
||||
name: robots-txt-policy-disallow-7
|
||||
24
cmd/robots2policy/testdata/complex.yaml
vendored
24
cmd/robots2policy/testdata/complex.yaml
vendored
@@ -20,8 +20,8 @@
|
||||
- action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("Googlebot")
|
||||
- path.startsWith("/search/")
|
||||
- userAgent.contains("Googlebot")
|
||||
- path.startsWith("/search/")
|
||||
name: robots-txt-policy-disallow-6
|
||||
- action: WEIGH
|
||||
expression: userAgent.contains("Bingbot")
|
||||
@@ -31,14 +31,14 @@
|
||||
- action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("Bingbot")
|
||||
- path.startsWith("/search/")
|
||||
- userAgent.contains("Bingbot")
|
||||
- path.startsWith("/search/")
|
||||
name: robots-txt-policy-disallow-8
|
||||
- action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("Bingbot")
|
||||
- path.startsWith("/admin/")
|
||||
- userAgent.contains("Bingbot")
|
||||
- path.startsWith("/admin/")
|
||||
name: robots-txt-policy-disallow-9
|
||||
- action: DENY
|
||||
expression: userAgent.contains("BadBot")
|
||||
@@ -54,18 +54,18 @@
|
||||
- action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("TestBot")
|
||||
- path.matches("^/.*/admin")
|
||||
- userAgent.contains("TestBot")
|
||||
- path.matches("^/.*/admin")
|
||||
name: robots-txt-policy-disallow-13
|
||||
- action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("TestBot")
|
||||
- path.matches("^/temp.*\\.html")
|
||||
- userAgent.contains("TestBot")
|
||||
- path.matches("^/temp.*\\.html")
|
||||
name: robots-txt-policy-disallow-14
|
||||
- action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("TestBot")
|
||||
- path.matches("^/file.\\.log")
|
||||
- userAgent.contains("TestBot")
|
||||
- path.matches("^/file.\\.log")
|
||||
name: robots-txt-policy-disallow-15
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Test consecutive user agents that should be grouped into any: blocks
|
||||
User-agent: *
|
||||
Disallow: /admin
|
||||
Crawl-delay: 10
|
||||
|
||||
# Multiple consecutive user agents - should be grouped
|
||||
User-agent: BadBot
|
||||
User-agent: SpamBot
|
||||
User-agent: EvilBot
|
||||
Disallow: /
|
||||
|
||||
# Single user agent - should be separate
|
||||
User-agent: GoodBot
|
||||
Disallow: /private
|
||||
|
||||
# Multiple consecutive user agents with crawl delay
|
||||
User-agent: SlowBot1
|
||||
User-agent: SlowBot2
|
||||
Crawl-delay: 5
|
||||
|
||||
# Multiple consecutive user agents with specific path
|
||||
User-agent: SearchBot1
|
||||
User-agent: SearchBot2
|
||||
User-agent: SearchBot3
|
||||
Disallow: /search
|
||||
47
cmd/robots2policy/testdata/consecutive.yaml
vendored
47
cmd/robots2policy/testdata/consecutive.yaml
vendored
@@ -1,47 +0,0 @@
|
||||
- action: WEIGH
|
||||
expression: "true"
|
||||
name: robots-txt-policy-crawl-delay-1
|
||||
weight:
|
||||
adjust: 3
|
||||
- action: CHALLENGE
|
||||
expression: path.startsWith("/admin")
|
||||
name: robots-txt-policy-disallow-2
|
||||
- action: DENY
|
||||
expression:
|
||||
any:
|
||||
- userAgent.contains("BadBot")
|
||||
- userAgent.contains("SpamBot")
|
||||
- userAgent.contains("EvilBot")
|
||||
name: robots-txt-policy-blacklist-3
|
||||
- action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("GoodBot")
|
||||
- path.startsWith("/private")
|
||||
name: robots-txt-policy-disallow-4
|
||||
- action: WEIGH
|
||||
expression:
|
||||
any:
|
||||
- userAgent.contains("SlowBot1")
|
||||
- userAgent.contains("SlowBot2")
|
||||
name: robots-txt-policy-crawl-delay-5
|
||||
weight:
|
||||
adjust: 3
|
||||
- action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("SearchBot1")
|
||||
- path.startsWith("/search")
|
||||
name: robots-txt-policy-disallow-7
|
||||
- action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("SearchBot2")
|
||||
- path.startsWith("/search")
|
||||
name: robots-txt-policy-disallow-8
|
||||
- action: CHALLENGE
|
||||
expression:
|
||||
all:
|
||||
- userAgent.contains("SearchBot3")
|
||||
- path.startsWith("/search")
|
||||
name: robots-txt-policy-disallow-9
|
||||
8
cmd/robots2policy/testdata/simple.json
vendored
8
cmd/robots2policy/testdata/simple.json
vendored
@@ -1,12 +1,12 @@
|
||||
[
|
||||
{
|
||||
"action": "CHALLENGE",
|
||||
"expression": "path.startsWith(\"/admin/\")",
|
||||
"name": "robots-txt-policy-disallow-1",
|
||||
"action": "CHALLENGE"
|
||||
"name": "robots-txt-policy-disallow-1"
|
||||
},
|
||||
{
|
||||
"action": "CHALLENGE",
|
||||
"expression": "path.startsWith(\"/private\")",
|
||||
"name": "robots-txt-policy-disallow-2",
|
||||
"action": "CHALLENGE"
|
||||
"name": "robots-txt-policy-disallow-2"
|
||||
}
|
||||
]
|
||||
@@ -132,28 +132,6 @@ dnsbl: false
|
||||
|
||||
# <!-- ... -->
|
||||
|
||||
# Logging settings for Anubis
|
||||
logging:
|
||||
# CEL log filters. Note that this is a very powerful feature and it is very easy to get
|
||||
# yourself into trouble with this. Avoid using log filters unless you are running into
|
||||
# circumstances like https://github.com/TecharoHQ/anubis/issues/942. This has a nonzero
|
||||
# impact on logging, which spirals out into a more than zero impact on Anubis'
|
||||
# performance and memory usage.
|
||||
filters:
|
||||
# Every filter must have a name and an expression. You can use the same expression
|
||||
# syntax as you can with bots or thresholds.
|
||||
#
|
||||
# If the expression returns `true`, then the log line is filtered _out_.
|
||||
- name: "http-stdlib"
|
||||
# Log lines where the message starts with "http:" are filtered out.
|
||||
expression: msg.startsWith("http:")
|
||||
- name: "context-canceled"
|
||||
# Log lines relating to context cancellation are filtered out.
|
||||
expression: msg.contains("context canceled")
|
||||
- name: "http-pipelining"
|
||||
# Log lines relating to HTTP/1.1 pipelining being improperly handled are filtered out.
|
||||
expression: msg.contains("Unsolicited response received on idle HTTP channel")
|
||||
|
||||
# Open Graph passthrough configuration, see here for more information:
|
||||
# https://anubis.techaro.lol/docs/admin/configuration/open-graph/
|
||||
openGraph:
|
||||
|
||||
@@ -14,12 +14,6 @@ func Zilch[T any]() T {
|
||||
type Impl[K comparable, V any] struct {
|
||||
data map[K]decayMapEntry[V]
|
||||
lock sync.RWMutex
|
||||
|
||||
// deleteCh receives decay-deletion requests from readers.
|
||||
deleteCh chan deleteReq[K]
|
||||
// stopCh stops the background cleanup worker.
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type decayMapEntry[V any] struct {
|
||||
@@ -27,38 +21,30 @@ type decayMapEntry[V any] struct {
|
||||
expiry time.Time
|
||||
}
|
||||
|
||||
// deleteReq is a request to remove a key if its expiry timestamp still matches
|
||||
// the observed one. This prevents racing with concurrent Set updates.
|
||||
type deleteReq[K comparable] struct {
|
||||
key K
|
||||
expiry time.Time
|
||||
}
|
||||
|
||||
// New creates a new DecayMap of key type K and value type V.
|
||||
//
|
||||
// Key types must be comparable to work with maps.
|
||||
func New[K comparable, V any]() *Impl[K, V] {
|
||||
m := &Impl[K, V]{
|
||||
data: make(map[K]decayMapEntry[V]),
|
||||
deleteCh: make(chan deleteReq[K], 1024),
|
||||
stopCh: make(chan struct{}),
|
||||
return &Impl[K, V]{
|
||||
data: make(map[K]decayMapEntry[V]),
|
||||
}
|
||||
m.wg.Add(1)
|
||||
go m.cleanupWorker()
|
||||
return m
|
||||
}
|
||||
|
||||
// expire forcibly expires a key by setting its time-to-live one second in the past.
|
||||
func (m *Impl[K, V]) expire(key K) bool {
|
||||
// Use a single write lock to avoid RUnlock->Lock convoy.
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
m.lock.RLock()
|
||||
val, ok := m.data[key]
|
||||
m.lock.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
m.lock.Lock()
|
||||
val.expiry = time.Now().Add(-1 * time.Second)
|
||||
m.data[key] = val
|
||||
m.lock.Unlock()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -67,14 +53,19 @@ func (m *Impl[K, V]) expire(key K) bool {
|
||||
// If the value does not exist, return false. Return true after
|
||||
// deletion.
|
||||
func (m *Impl[K, V]) Delete(key K) bool {
|
||||
// Use a single write lock to avoid RUnlock->Lock convoy.
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
m.lock.RLock()
|
||||
_, ok := m.data[key]
|
||||
if ok {
|
||||
delete(m.data, key)
|
||||
m.lock.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
|
||||
m.lock.Lock()
|
||||
delete(m.data, key)
|
||||
m.lock.Unlock()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Get gets a value from the DecayMap by key.
|
||||
@@ -90,12 +81,13 @@ func (m *Impl[K, V]) Get(key K) (V, bool) {
|
||||
}
|
||||
|
||||
if time.Now().After(value.expiry) {
|
||||
// Defer decay deletion to the background worker to avoid convoy.
|
||||
select {
|
||||
case m.deleteCh <- deleteReq[K]{key: key, expiry: value.expiry}:
|
||||
default:
|
||||
// Channel full: drop request; a future Cleanup() or Get will retry.
|
||||
m.lock.Lock()
|
||||
// Since previously reading m.data[key], the value may have been updated.
|
||||
// Delete the entry only if the expiry time is still the same.
|
||||
if m.data[key].expiry.Equal(value.expiry) {
|
||||
delete(m.data, key)
|
||||
}
|
||||
m.lock.Unlock()
|
||||
|
||||
return Zilch[V](), false
|
||||
}
|
||||
@@ -133,64 +125,3 @@ func (m *Impl[K, V]) Len() int {
|
||||
defer m.lock.RUnlock()
|
||||
return len(m.data)
|
||||
}
|
||||
|
||||
// Close stops the background cleanup worker. It's optional to call; maps live
|
||||
// for the process lifetime in many cases. Call in tests or when you know you no
|
||||
// longer need the map to avoid goroutine leaks.
|
||||
func (m *Impl[K, V]) Close() {
|
||||
close(m.stopCh)
|
||||
m.wg.Wait()
|
||||
}
|
||||
|
||||
// cleanupWorker batches decay deletions to minimize lock contention.
|
||||
func (m *Impl[K, V]) cleanupWorker() {
|
||||
defer m.wg.Done()
|
||||
batch := make([]deleteReq[K], 0, 64)
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
flush := func() {
|
||||
if len(batch) == 0 {
|
||||
return
|
||||
}
|
||||
m.applyDeletes(batch)
|
||||
// reset batch without reallocating
|
||||
batch = batch[:0]
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case req := <-m.deleteCh:
|
||||
batch = append(batch, req)
|
||||
case <-ticker.C:
|
||||
flush()
|
||||
case <-m.stopCh:
|
||||
// Drain any remaining requests then exit
|
||||
for {
|
||||
select {
|
||||
case req := <-m.deleteCh:
|
||||
batch = append(batch, req)
|
||||
default:
|
||||
flush()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Impl[K, V]) applyDeletes(batch []deleteReq[K]) {
|
||||
now := time.Now()
|
||||
m.lock.Lock()
|
||||
for _, req := range batch {
|
||||
entry, ok := m.data[req.key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Only delete if the expiry is unchanged and already past.
|
||||
if entry.expiry.Equal(req.expiry) && now.After(entry.expiry) {
|
||||
delete(m.data, req.key)
|
||||
}
|
||||
}
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
func TestImpl(t *testing.T) {
|
||||
dm := New[string, string]()
|
||||
t.Cleanup(dm.Close)
|
||||
|
||||
dm.Set("test", "hi", 5*time.Minute)
|
||||
|
||||
@@ -29,24 +28,10 @@ func TestImpl(t *testing.T) {
|
||||
if ok {
|
||||
t.Error("got value even though it was supposed to be expired")
|
||||
}
|
||||
|
||||
// Deletion of expired entries after Get is deferred to a background worker.
|
||||
// Assert it eventually disappears from the map.
|
||||
deadline := time.Now().Add(200 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
if dm.Len() == 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
if dm.Len() != 0 {
|
||||
t.Fatalf("expected background cleanup to remove expired key; len=%d", dm.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanup(t *testing.T) {
|
||||
dm := New[string, string]()
|
||||
t.Cleanup(dm.Close)
|
||||
|
||||
dm.Set("test1", "hi1", 1*time.Second)
|
||||
dm.Set("test2", "hi2", 2*time.Second)
|
||||
|
||||
@@ -11,22 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
<!-- This changes the project to: -->
|
||||
|
||||
- Fix lock convoy problem in decaymap ([#1103](https://github.com/TecharoHQ/anubis/issues/1103))
|
||||
- [Log filtering](./admin/configuration/expressions.mdx#log-filtering) rules have been added. This allows users to write custom log filtering logic.
|
||||
- Document missing environment variables in installation guide: `SLOG_LEVEL`, `COOKIE_PREFIX`, `FORCED_LANGUAGE`, and `TARGET_DISABLE_KEEPALIVE` ([#1086](https://github.com/TecharoHQ/anubis/pull/1086))
|
||||
- Add validation warning when persistent storage is used without setting signing keys
|
||||
- Fixed `robots2policy` to properly group consecutive user agents into `any:` instead of only processing the last one ([#925](https://github.com/TecharoHQ/anubis/pull/925))
|
||||
- Add the [`s3api` storage backend](./admin/policies.mdx#s3api) to allow Anubis to use S3 API compatible object storage as its storage backend.
|
||||
- Make `cmd/containerbuild` support commas for separating elements of the `--docker-tags` argument as well as newlines.
|
||||
- Add the `DIFFICULTY_IN_JWT` option, which allows one to add the `difficulty` field in the JWT claims which indicates the difficulty of the token ([#1063](https://github.com/TecharoHQ/anubis/pull/1063))
|
||||
- Ported the client-side JS to TypeScript to avoid egregious errors in the future.
|
||||
- Fixes concurrency problems with very old browsers ([#1082](https://github.com/TecharoHQ/anubis/issues/1082)).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
Sometimes the enhanced temporal assurance in [#1038](https://github.com/TecharoHQ/anubis/pull/1038) and [#1068](https://github.com/TecharoHQ/anubis/pull/1068) could backfire because Chromium and its ilk randomize the amount of time they wait in order to avoid a timing side channel attack. This has been fixed by both increasing the amount of time a client has to wait for the metarefresh and preact challenges as well as making the server side logic more permissive.
|
||||
<!-- This changes the project to: -->
|
||||
|
||||
## v1.22.0: Yda Hext
|
||||
|
||||
|
||||
@@ -99,10 +99,6 @@ For this rule, if a request comes in matching [the signature of the `go get` com
|
||||
|
||||
Anubis exposes the following variables to expressions:
|
||||
|
||||
### Bot expressions
|
||||
|
||||
Bot expressions are used for evaluating [bot rules](../policies.mdx#bot-policies).
|
||||
|
||||
| Name | Type | Explanation | Example |
|
||||
| :-------------- | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- |
|
||||
| `headers` | `map[string, string]` | The [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers) of the request being processed. | `{"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/137.0"}` |
|
||||
@@ -186,23 +182,6 @@ Something to keep in mind about system load average is that it is not aware of t
|
||||
|
||||
Also keep in mind that this does not account for other kinds of latency like I/O latency. A system can have its web applications unresponsive due to high latency from a MySQL server but still have that web application server report a load near or at zero.
|
||||
|
||||
### Log filtering
|
||||
|
||||
Log filters are run on every time Anubis logs data. These are high throughput filters and should be written with care.
|
||||
|
||||
| Name | Type | Explanation | Example |
|
||||
| :------ | :-------------------- | :----------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
||||
| `time` | Timestamp | The time that the log line was emitted. | `2025-08-18T06:45:38-04:00` |
|
||||
| `msg` | `string` | The text-based message for the given log line. | `"invalid response"` |
|
||||
| `level` | `string` | The [log level](https://pkg.go.dev/log/slog#Level) for the log message. | `"INFO"` |
|
||||
| `attrs` | `map[string, string]` | The key -> value attributes for the given log line. Note that this is an expensive variable to access. | `{"err": "internal: the sun exploded"}` |
|
||||
|
||||
:::note
|
||||
|
||||
When you define a log filter, anything matching that filter is _removed_. Any remaining logs are sent through to the system journal or standard error.
|
||||
|
||||
:::
|
||||
|
||||
## Functions exposed to Anubis expressions
|
||||
|
||||
Anubis expressions can be augmented with the following functions:
|
||||
|
||||
@@ -59,7 +59,7 @@ Currently the following settings are configurable via the policy file:
|
||||
Anubis uses these environment variables for configuration:
|
||||
|
||||
| Environment Variable | Default value | Explanation |
|
||||
| :----------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|:-------------------------------|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `BASE_PREFIX` | unset | If set, adds a global prefix to all Anubis endpoints (everything starting with `/.within.website/x/anubis/`). For example, setting this to `/myapp` would make Anubis accessible at `/myapp/` instead of `/`. This is useful when running Anubis behind a reverse proxy that routes based on path prefixes. |
|
||||
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
|
||||
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
|
||||
@@ -70,9 +70,8 @@ Anubis uses these environment variables for configuration:
|
||||
| `COOKIE_PREFIX` | `anubis-cookie` | The prefix used for browser cookies created by Anubis. Useful for customization or avoiding conflicts with other applications. |
|
||||
| `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false |
|
||||
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
||||
| `DIFFICULTY_IN_JWT` | `false` | If set to `true`, adds the `difficulty` field into JWT claims, which indicates the difficulty the token has been generated. This may be useful for statistics and debugging. |
|
||||
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. **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. See below for details. |
|
||||
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. **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` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. When running multiple instances on the same base domain, the key must be the same across all instances. See below for details. |
|
||||
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. |
|
||||
| `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_NETWORK` | `tcp` | The address family that the Anubis metrics server listens on. See `BIND_NETWORK` for more information. |
|
||||
@@ -101,14 +100,14 @@ If you don't know or understand what these settings mean, ignore them. These are
|
||||
|
||||
:::
|
||||
|
||||
| Environment Variable | Default value | Explanation |
|
||||
| :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `FORCED_LANGUAGE` | unset | If set, forces Anubis to display challenge pages in the specified language instead of using the browser's Accept-Language header. Use ISO 639-1 language codes (e.g., `de` for German, `fr` for French). |
|
||||
| `HS512_SECRET` | unset | Secret string for JWT HS512 algorithm. If this is not set, Anubis will use ED25519 as defined via the variables above. The longer the better; 128 chars should suffice. **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. |
|
||||
| `TARGET_DISABLE_KEEPALIVE` | `false` | If `true`, disables HTTP keep-alive for connections to the target backend. Useful for backends that don't handle keep-alive properly. |
|
||||
| `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. |
|
||||
| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. |
|
||||
| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. |
|
||||
| Environment Variable | Default value | Explanation |
|
||||
| :---------------------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `FORCED_LANGUAGE` | unset | If set, forces Anubis to display challenge pages in the specified language instead of using the browser's Accept-Language header. Use ISO 639-1 language codes (e.g., `de` for German, `fr` for French). |
|
||||
| `HS512_SECRET` | unset | Secret string for JWT HS512 algorithm. If this is not set, Anubis will use ED25519 as defined via the variables above. The longer the better; 128 chars should suffice. |
|
||||
| `TARGET_DISABLE_KEEPALIVE` | `false` | If `true`, disables HTTP keep-alive for connections to the target backend. Useful for backends that don't handle keep-alive properly. |
|
||||
| `TARGET_HOST` | unset | If set, overrides the Host header in requests forwarded to `TARGET`. |
|
||||
| `TARGET_INSECURE_SKIP_VERIFY` | `false` | If `true`, skip TLS certificate validation for targets that listen over `https`. If your backend does not listen over `https`, ignore this setting. |
|
||||
| `TARGET_SNI` | unset | If set, overrides the TLS handshake hostname in requests forwarded to `TARGET`. |
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -123,10 +123,6 @@ remote_addresses:
|
||||
|
||||
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.
|
||||
|
||||
## Logging
|
||||
|
||||
Anubis has support for configuring log filtering using expressions. See the [log filters](./configuration/expressions.mdx#log-filters) of the [expression](./configuration/expressions.mdx) documentation for more information.
|
||||
|
||||
## Storage backends
|
||||
|
||||
Anubis needs to store temporary data in order to determine if a user is legitimate or not. Administrators should choose a storage backend based on their infrastructure needs. Each backend has its own advantages and disadvantages.
|
||||
@@ -200,83 +196,6 @@ store:
|
||||
path: /data/anubis.bdb
|
||||
```
|
||||
|
||||
### `s3api`
|
||||
|
||||
A network-backed storage layer backed by [object storage](https://en.wikipedia.org/wiki/Object_storage), specifically using the [S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_Reference.html). This can be backed by any S3-compatible object storage service such as:
|
||||
|
||||
- [AWS S3](https://aws.amazon.com/s3/)
|
||||
- [Cloudflare R2](https://www.cloudflare.com/developer-platform/products/r2/)
|
||||
- [Hetzner Object Storage](https://www.hetzner.com/storage/object-storage/)
|
||||
- [Minio](https://www.min.io/)
|
||||
- [Tigris](https://www.tigrisdata.com/)
|
||||
|
||||
If you are using a cloud platform, they likely provide an S3 compatible object storage service. If not, you may want to choose [one of the fastest options](https://www.tigrisdata.com/blog/benchmark-small-objects/).
|
||||
|
||||
| Should I use this backend? | Yes/no |
|
||||
| :------------------------------------------------------------ | :----- |
|
||||
| Are you running only one instance of Anubis for this service? | 🚫 No |
|
||||
| Does your service get a lot of traffic? | ✅ Yes |
|
||||
| Do you want to store data persistently when Anubis restarts? | ✅ Yes |
|
||||
| Do you run Anubis without mutable filesystem storage? | ✅ Yes |
|
||||
|
||||
:::note
|
||||
|
||||
Using this backend will cause a lot of S3 operations, at least one for creating challenges, one for invalidating challenges, one for updating challenges to prevent double-spends, and one for removing challenges.
|
||||
|
||||
:::
|
||||
|
||||
#### Configuration
|
||||
|
||||
The `s3api` backend takes the following configuration options:
|
||||
|
||||
| Name | Type | Example | Description |
|
||||
| :----------- | :------ | :------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `bucketName` | string | The name of the dedicated bucket for Anubis to store information in. |
|
||||
| `pathStyle` | boolean | `false` | If true, use path-style S3 API operations. Please consult your storage provider's documentation if you don't know what you should put here. |
|
||||
|
||||
:::note
|
||||
|
||||
You should probably enable a lifecycle expiration rule for buckets containing Anubis data. Here is an example policy:
|
||||
|
||||
```json
|
||||
{
|
||||
"Rules": [
|
||||
{
|
||||
"Status": "Enabled",
|
||||
"Expiration": {
|
||||
"Days": 7
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Adjust this as facts and circumstances demand, but 7 days should be enough for anyone.
|
||||
|
||||
:::
|
||||
|
||||
Example:
|
||||
|
||||
Assuming your environment looks like this:
|
||||
|
||||
```sh
|
||||
# All of the following are fake credentials that look like real ones.
|
||||
AWS_ACCESS_KEY_ID=accordingToAllKnownRulesOfAviation
|
||||
AWS_SECRET_ACCESS_KEY=thereIsNoWayABeeShouldBeAbleToFly
|
||||
AWS_REGION=yow
|
||||
AWS_ENDPOINT_URL_S3=https://yow.s3.probably-not-malware.lol
|
||||
```
|
||||
|
||||
Then your configuration would look like this:
|
||||
|
||||
```yaml
|
||||
store:
|
||||
backend: s3api
|
||||
parameters:
|
||||
bucketName: techaro-prod-anubis
|
||||
pathStyle: false
|
||||
```
|
||||
|
||||
### `valkey`
|
||||
|
||||
[Valkey](https://valkey.io/) is an in-memory key/value store that clients access over the network. This allows multiple instances of Anubis to share information and does not require each instance of Anubis to have persistent filesystem storage.
|
||||
|
||||
677
docs/package-lock.json
generated
677
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.0.1",
|
||||
"@docusaurus/module-type-aliases": "^3.8.1",
|
||||
"@docusaurus/tsconfig": "^3.8.1",
|
||||
"@docusaurus/types": "^3.8.1",
|
||||
"typescript": "~5.6.2"
|
||||
@@ -45,4 +45,4 @@
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
go.mod
18
go.mod
@@ -5,9 +5,6 @@ go 1.24.2
|
||||
require (
|
||||
github.com/TecharoHQ/thoth-proto v0.4.0
|
||||
github.com/a-h/templ v0.3.924
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
||||
github.com/gaissmai/bart v0.23.0
|
||||
@@ -52,21 +49,6 @@ require (
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
||||
|
||||
36
go.sum
36
go.sum
@@ -51,42 +51,6 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 h1:R0tNFJqfjHL3900cqhXuwQ+1K4G0xc9Yf8EDbFXCKEw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6/go.mod h1:y/7sDdu+aJvPtGXr4xYosdpq9a6T9Z0jkXfugmti0rI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 h1:hncKj/4gR+TPauZgTAsxOxNcvBayhUlYZ6LO/BYiQ30=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6/go.mod h1:OiIh45tp6HdJDDJGnja0mw8ihQGz3VGrUflLqSL0SmM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 h1:nEXUSAwyUfLTgnc9cxlDWy637qsq4UWwp3sNAfl0Z3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6/go.mod h1:HGzIULx4Ge3Do2V0FaiYKcyKzOqwrhUZgCI77NisswQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 h1:ETkfWcXP2KNPLecaDa++5bsQhCRa5M5sLUJa5DWYIIg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3/go.mod h1:+/3ZTqoYb3Ur7DObD00tarKMLMuKg8iqz5CHEanqTnw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func InitSlog(level string) {
|
||||
var programLevel slog.Level
|
||||
if err := (&programLevel).UnmarshalText([]byte(level)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid log level %s: %v, using info\n", level, err)
|
||||
programLevel = slog.LevelInfo
|
||||
}
|
||||
|
||||
leveler := &slog.LevelVar{}
|
||||
leveler.Set(programLevel)
|
||||
|
||||
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
Level: leveler,
|
||||
})
|
||||
slog.SetDefault(slog.New(h))
|
||||
}
|
||||
|
||||
func GetRequestLogger(base *slog.Logger, r *http.Request) *slog.Logger {
|
||||
host := r.Host
|
||||
if host == "" {
|
||||
@@ -23,3 +44,27 @@ func GetRequestLogger(base *slog.Logger, r *http.Request) *slog.Logger {
|
||||
"x-real-ip", r.Header.Get("X-Real-Ip"),
|
||||
)
|
||||
}
|
||||
|
||||
// ErrorLogFilter is used to suppress "context canceled" logs from the http server when a request is canceled (e.g., when a client disconnects).
|
||||
type ErrorLogFilter struct {
|
||||
Unwrap *log.Logger
|
||||
}
|
||||
|
||||
func (elf *ErrorLogFilter) Write(p []byte) (n int, err error) {
|
||||
logMessage := string(p)
|
||||
if strings.Contains(logMessage, "context canceled") {
|
||||
return len(p), nil // Suppress the log by doing nothing
|
||||
}
|
||||
if strings.Contains(logMessage, "Unsolicited response received on idle HTTP channel") {
|
||||
return len(p), nil
|
||||
}
|
||||
if elf.Unwrap != nil {
|
||||
return elf.Unwrap.Writer().Write(p)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func GetFilteredHTTPLogger() *log.Logger {
|
||||
stdErrLogger := log.New(os.Stderr, "", log.LstdFlags) // essentially what the default logger is.
|
||||
return log.New(&ErrorLogFilter{Unwrap: stdErrLogger}, "", 0)
|
||||
}
|
||||
|
||||
82
internal/log_test.go
Normal file
82
internal/log_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestErrorLogFilter(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
destLogger := log.New(&buf, "", 0)
|
||||
errorFilterWriter := &ErrorLogFilter{Unwrap: destLogger}
|
||||
testErrorLogger := log.New(errorFilterWriter, "", 0)
|
||||
|
||||
// Test Case 1: Suppressed message
|
||||
suppressedMessage := "http: proxy error: context canceled"
|
||||
testErrorLogger.Println(suppressedMessage)
|
||||
|
||||
if buf.Len() != 0 {
|
||||
t.Errorf("Suppressed message was written to output. Output: %q", buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
|
||||
// Test Case 2: Allowed message
|
||||
allowedMessage := "http: another error occurred"
|
||||
testErrorLogger.Println(allowedMessage)
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, allowedMessage) {
|
||||
t.Errorf("Allowed message was not written to output. Output: %q", output)
|
||||
}
|
||||
if !strings.HasSuffix(output, "\n") {
|
||||
t.Errorf("Allowed message output is missing newline. Output: %q", output)
|
||||
}
|
||||
buf.Reset()
|
||||
|
||||
// Test Case 3: Partially matching message (should be suppressed)
|
||||
partiallyMatchingMessage := "Some other log before http: proxy error: context canceled and after"
|
||||
testErrorLogger.Println(partiallyMatchingMessage)
|
||||
|
||||
if buf.Len() != 0 {
|
||||
t.Errorf("Partially matching message was written to output. Output: %q", buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
}
|
||||
|
||||
func TestGetRequestLogger(t *testing.T) {
|
||||
// Test case 1: Normal request with Host header
|
||||
req1, _ := http.NewRequest("GET", "http://example.com/test", nil)
|
||||
req1.Host = "example.com"
|
||||
|
||||
logger := slog.Default()
|
||||
reqLogger := GetRequestLogger(logger, req1)
|
||||
|
||||
// We can't easily test the actual log output without setting up a test handler,
|
||||
// but we can verify the function doesn't panic and returns a logger
|
||||
if reqLogger == nil {
|
||||
t.Error("GetRequestLogger returned nil")
|
||||
}
|
||||
|
||||
// Test case 2: Subrequest auth mode with X-Forwarded-Host
|
||||
req2, _ := http.NewRequest("GET", "http://test.com/auth", nil)
|
||||
req2.Host = ""
|
||||
req2.Header.Set("X-Forwarded-Host", "original-site.com")
|
||||
|
||||
reqLogger2 := GetRequestLogger(logger, req2)
|
||||
if reqLogger2 == nil {
|
||||
t.Error("GetRequestLogger returned nil for X-Forwarded-Host case")
|
||||
}
|
||||
|
||||
// Test case 3: No host information available
|
||||
req3, _ := http.NewRequest("GET", "http://test.com/nohost", nil)
|
||||
req3.Host = ""
|
||||
|
||||
reqLogger3 := GetRequestLogger(logger, req3)
|
||||
if reqLogger3 == nil {
|
||||
t.Error("GetRequestLogger returned nil for no host case")
|
||||
}
|
||||
}
|
||||
@@ -78,10 +78,6 @@ type Server struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func (s *Server) GetLogger(subsystem string) *slog.Logger {
|
||||
return s.logger.With("subsystem", subsystem)
|
||||
}
|
||||
|
||||
func (s *Server) getTokenKeyfunc() jwt.Keyfunc {
|
||||
// return ED25519 key if HS512 is not set
|
||||
if len(s.hs512Secret) == 0 {
|
||||
@@ -505,12 +501,6 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
var tokenString string
|
||||
|
||||
// check if JWTRestrictionHeader is set and header is in request
|
||||
claims := jwt.MapClaims{
|
||||
"challenge": chall.ID,
|
||||
"method": rule.Challenge.Algorithm,
|
||||
"policyRule": rule.Hash(),
|
||||
"action": string(cr.Rule),
|
||||
}
|
||||
if s.opts.JWTRestrictionHeader != "" {
|
||||
if r.Header.Get(s.opts.JWTRestrictionHeader) == "" {
|
||||
lg.Error("JWTRestrictionHeader is set in config but not found in request, please check your reverse proxy config.")
|
||||
@@ -518,13 +508,22 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
s.respondWithError(w, r, "failed to sign JWT")
|
||||
return
|
||||
} else {
|
||||
claims["restriction"] = internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader))
|
||||
tokenString, err = s.signJWT(jwt.MapClaims{
|
||||
"challenge": chall.ID,
|
||||
"method": rule.Challenge.Algorithm,
|
||||
"policyRule": rule.Hash(),
|
||||
"action": string(cr.Rule),
|
||||
"restriction": internal.SHA256sum(r.Header.Get(s.opts.JWTRestrictionHeader)),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
tokenString, err = s.signJWT(jwt.MapClaims{
|
||||
"challenge": chall.ID,
|
||||
"method": rule.Challenge.Algorithm,
|
||||
"policyRule": rule.Hash(),
|
||||
"action": string(cr.Rule),
|
||||
})
|
||||
}
|
||||
if s.opts.DifficultyInJWT {
|
||||
claims["difficulty"] = rule.Challenge.Difficulty
|
||||
}
|
||||
tokenString, err = s.signJWT(claims)
|
||||
|
||||
if err != nil {
|
||||
lg.Error("failed to sign JWT", "err", err)
|
||||
|
||||
@@ -43,7 +43,7 @@ func (i *Impl) Issue(r *http.Request, lg *slog.Logger, in *challenge.IssueInput)
|
||||
}
|
||||
|
||||
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
|
||||
wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 800 * time.Millisecond)
|
||||
wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 950 * time.Millisecond)
|
||||
|
||||
if time.Now().Before(wantTime) {
|
||||
return challenge.NewError("validate", "insufficent time", fmt.Errorf("%w: wanted user to wait until at least %s", challenge.ErrFailed, wantTime.Format(time.RFC3339)))
|
||||
|
||||
@@ -13,6 +13,6 @@ templ page(redir string, difficulty int, loc *localization.SimpleLocalizer) {
|
||||
<img style="display:none;" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/>
|
||||
<p id="status">{ loc.T("loading") }</p>
|
||||
<p>{ loc.T("connection_security") }</p>
|
||||
<meta http-equiv="refresh" content={ fmt.Sprintf("%d; url=%s", difficulty+1, redir) }/>
|
||||
<meta http-equiv="refresh" content={ fmt.Sprintf("%d; url=%s", difficulty, redir) }/>
|
||||
</div>
|
||||
}
|
||||
|
||||
4
lib/challenge/metarefresh/metarefresh_templ.go
generated
4
lib/challenge/metarefresh/metarefresh_templ.go
generated
@@ -93,9 +93,9 @@ func page(redir string, difficulty int, loc *localization.SimpleLocalizer) templ
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty+1, redir))
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -40,9 +40,9 @@ for the JavaScript code in this page.
|
||||
|
||||
mkdir -p static/js
|
||||
|
||||
for file in js/*.tsx; do
|
||||
for file in js/*.jsx; do
|
||||
filename="${file##*/}" # Extracts "app.jsx" from "./js/app.jsx"
|
||||
output="${filename%.tsx}.js" # Changes "app.jsx" to "app.js"
|
||||
output="${filename%.jsx}.js" # Changes "app.jsx" to "app.js"
|
||||
echo $output
|
||||
|
||||
esbuild "${file}" --minify --bundle --outfile=static/"${output}" --banner:js="${LICENSE}"
|
||||
|
||||
62
lib/challenge/preact/js/app.jsx
Normal file
62
lib/challenge/preact/js/app.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { render, h, Fragment } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
import { g, j, u, x } from "./xeact.js";
|
||||
import { Sha256 } from '@aws-crypto/sha256-js';
|
||||
|
||||
/** @jsx h */
|
||||
/** @jsxFrag Fragment */
|
||||
|
||||
function toHexString(arr) {
|
||||
return Array.from(arr)
|
||||
.map((c) => c.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const [state, setState] = useState(null);
|
||||
const [imageURL, setImageURL] = useState(null);
|
||||
const [passed, setPassed] = useState(false);
|
||||
const [challenge, setChallenge] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setState(j("preact_info"));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setImageURL(state.pensive_url);
|
||||
const hash = new Sha256('');
|
||||
hash.update(state.challenge);
|
||||
setChallenge(toHexString(hash.digestSync()));
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setPassed(true);
|
||||
}, state.difficulty * 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [challenge]);
|
||||
|
||||
useEffect(() => {
|
||||
window.location.href = u(state.redir, {
|
||||
result: challenge,
|
||||
});
|
||||
}, [passed]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{imageURL !== null && (
|
||||
<img src={imageURL} style="width:100%;max-width:256px;" />
|
||||
)}
|
||||
{state !== null && (
|
||||
<>
|
||||
<p id="status">{state.loading_message}</p>
|
||||
<p>{state.connection_security_message}</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
x(g("app"));
|
||||
render(<App />, g("app"));
|
||||
@@ -1,87 +0,0 @@
|
||||
import { render, h, Fragment } from "preact";
|
||||
import { useState, useEffect } from "preact/hooks";
|
||||
import { g, j, r, u, x } from "./xeact.js";
|
||||
import { Sha256 } from "@aws-crypto/sha256-js";
|
||||
|
||||
/** @jsx h */
|
||||
/** @jsxFrag Fragment */
|
||||
|
||||
function toHexString(arr: Uint8Array) {
|
||||
return Array.from(arr)
|
||||
.map((c) => c.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
interface PreactInfo {
|
||||
redir: string;
|
||||
challenge: string;
|
||||
difficulty: number;
|
||||
connection_security_message: string;
|
||||
loading_message: string;
|
||||
pensive_url: string;
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const [state, setState] = useState<PreactInfo>();
|
||||
const [imageURL, setImageURL] = useState<string | null>(null);
|
||||
const [passed, setPassed] = useState<boolean>(false);
|
||||
const [challenge, setChallenge] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setState(j("preact_info"));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (state === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
setImageURL(state?.pensive_url);
|
||||
const hash = new Sha256("");
|
||||
hash.update(state.challenge);
|
||||
setChallenge(toHexString(hash.digestSync()));
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setPassed(true);
|
||||
}, state?.difficulty * 125);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [challenge]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (challenge === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = u(state.redir, {
|
||||
result: challenge,
|
||||
});
|
||||
}, [passed]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{imageURL !== null && (
|
||||
<img src={imageURL} style={{ width: "100%", maxWidth: "256px" }} />
|
||||
)}
|
||||
{state !== undefined && (
|
||||
<>
|
||||
<p id="status">{state.loading_message}</p>
|
||||
<p>{state.connection_security_message}</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
x(g("app"));
|
||||
render(<App />, g("app"));
|
||||
@@ -57,7 +57,7 @@ func (i *impl) Issue(r *http.Request, lg *slog.Logger, in *challenge.IssueInput)
|
||||
}
|
||||
|
||||
func (i *impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
|
||||
wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 80 * time.Millisecond)
|
||||
wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 95 * time.Millisecond)
|
||||
|
||||
if time.Now().Before(wantTime) {
|
||||
return challenge.NewError("validate", "insufficent time", fmt.Errorf("%w: wanted user to wait until at least %s", challenge.ErrFailed, wantTime.Format(time.RFC3339)))
|
||||
|
||||
@@ -46,7 +46,6 @@ type Options struct {
|
||||
Logger *slog.Logger
|
||||
PublicUrl string
|
||||
JWTRestrictionHeader string
|
||||
DifficultyInJWT bool
|
||||
}
|
||||
|
||||
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
||||
@@ -120,14 +119,6 @@ func New(opts Options) (*Server, error) {
|
||||
logger: opts.Logger,
|
||||
}
|
||||
|
||||
if opts.Policy.Logging != nil {
|
||||
var err error
|
||||
result.logger, err = opts.Policy.ApplyLogFilters(opts.Logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't create log filters: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
xess.Mount(mux)
|
||||
|
||||
|
||||
@@ -62,6 +62,5 @@
|
||||
"js_iterations": "iteracijų",
|
||||
"js_finished_reading": "Viską perskaičiau, tęskime →",
|
||||
"js_calculation_error": "Skaičiavimo klaida!",
|
||||
"js_calculation_error_msg": "Nepavyko įveikti iššūkio:",
|
||||
"missing_required_forwarded_headers": "Trūksta privalomų X-Forwarded-* antraščių"
|
||||
"js_calculation_error_msg": "Nepavyko įveikti iššūkio:"
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// Filterer is the shape of any type that can perform log filtering. This takes
|
||||
// the context of the log filtering call and the log record to be filtered.
|
||||
type Filterer interface {
|
||||
Filter(ctx context.Context, r slog.Record) bool
|
||||
}
|
||||
|
||||
// FilterFunc lets you make inline log filters with plain functions.
|
||||
type FilterFunc func(ctx context.Context, r *slog.Record) bool
|
||||
|
||||
// Filter implements Filterer for FilterFunc.
|
||||
func (ff FilterFunc) Filter(ctx context.Context, r *slog.Record) bool {
|
||||
return ff(ctx, r)
|
||||
}
|
||||
|
||||
// FilterHandler wraps a slog Handler with one or more filters, enabling administrators
|
||||
// to customize the logging subsystem of Anubis.
|
||||
type FilterHandler struct {
|
||||
next slog.Handler
|
||||
filters []Filterer
|
||||
}
|
||||
|
||||
// NewFilterHandler creates a new filtering handler with the given base handler and filters.
|
||||
func NewFilterHandler(handler slog.Handler, filters ...Filterer) *FilterHandler {
|
||||
return &FilterHandler{
|
||||
next: handler,
|
||||
filters: filters,
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled passes through to the upstream slog Handler.
|
||||
func (h *FilterHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
return h.next.Enabled(ctx, level)
|
||||
}
|
||||
|
||||
// Handle implements slog.Handler and applies all filters before delegating to the base handler.
|
||||
func (h *FilterHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
// Apply all filters - if any filter returns false, skip the log
|
||||
for _, filter := range h.filters {
|
||||
if !filter.Filter(ctx, r) {
|
||||
return nil // Skip this log record
|
||||
}
|
||||
}
|
||||
return h.next.Handle(ctx, r)
|
||||
}
|
||||
|
||||
// WithAttrs implements slog.Handler.
|
||||
func (h *FilterHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &FilterHandler{
|
||||
next: h.next.WithAttrs(attrs),
|
||||
filters: h.filters,
|
||||
}
|
||||
}
|
||||
|
||||
// WithGroup implements slog.Handler.
|
||||
func (h *FilterHandler) WithGroup(name string) slog.Handler {
|
||||
return &FilterHandler{
|
||||
next: h.next.WithGroup(name),
|
||||
filters: h.filters,
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
func Init(level string) slog.Handler {
|
||||
var programLevel slog.Level
|
||||
if err := (&programLevel).UnmarshalText([]byte(level)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid log level %s: %v, using info\n", level, err)
|
||||
programLevel = slog.LevelInfo
|
||||
}
|
||||
|
||||
leveler := &slog.LevelVar{}
|
||||
leveler.Set(programLevel)
|
||||
|
||||
h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
Level: leveler,
|
||||
})
|
||||
return h
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
// handlerWriter is an io.Writer that calls a Handler.
|
||||
// It is used to link the default log.Logger to the default slog.Logger.
|
||||
//
|
||||
// Adapted from https://cs.opensource.google/go/go/+/refs/tags/go1.24.5:src/log/slog/logger.go;l=62
|
||||
type handlerWriter struct {
|
||||
h slog.Handler
|
||||
level slog.Leveler
|
||||
}
|
||||
|
||||
func (w *handlerWriter) Write(buf []byte) (int, error) {
|
||||
level := w.level.Level()
|
||||
if !w.h.Enabled(context.Background(), level) {
|
||||
return 0, nil
|
||||
}
|
||||
var pc uintptr
|
||||
|
||||
// Remove final newline.
|
||||
origLen := len(buf) // Report that the entire buf was written.
|
||||
buf = bytes.TrimSuffix(buf, []byte{'\n'})
|
||||
r := slog.NewRecord(time.Now(), level, string(buf), pc)
|
||||
return origLen, w.h.Handle(context.Background(), r)
|
||||
}
|
||||
|
||||
func StdlibLogger(next slog.Handler, level slog.Level) *log.Logger {
|
||||
return log.New(&handlerWriter{h: next, level: level}, "", log.LstdFlags)
|
||||
}
|
||||
@@ -326,7 +326,6 @@ func (sc StatusCodes) Valid() error {
|
||||
type fileConfig struct {
|
||||
Bots []BotOrImport `json:"bots"`
|
||||
DNSBL bool `json:"dnsbl"`
|
||||
Logging *Logging `json:"logging"`
|
||||
OpenGraph openGraphFileConfig `json:"openGraph,omitempty"`
|
||||
Impressum *Impressum `json:"impressum,omitempty"`
|
||||
StatusCodes StatusCodes `json:"status_codes"`
|
||||
@@ -369,12 +368,6 @@ func (c *fileConfig) Valid() error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Logging != nil {
|
||||
if err := c.Logging.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...))
|
||||
}
|
||||
@@ -408,7 +401,6 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
||||
ConsiderHost: c.OpenGraph.ConsiderHost,
|
||||
Override: c.OpenGraph.Override,
|
||||
},
|
||||
Logging: c.Logging,
|
||||
StatusCodes: c.StatusCodes,
|
||||
Store: c.Store,
|
||||
}
|
||||
@@ -449,12 +441,6 @@ func Load(fin io.Reader, fname string) (*Config, error) {
|
||||
result.Impressum = c.Impressum
|
||||
}
|
||||
|
||||
if c.Logging != nil {
|
||||
if err := c.Logging.Valid(); err != nil {
|
||||
validationErrs = append(validationErrs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Thresholds) == 0 {
|
||||
c.Thresholds = DefaultThresholds
|
||||
}
|
||||
@@ -479,7 +465,6 @@ type Config struct {
|
||||
Bots []BotConfig
|
||||
Thresholds []Threshold
|
||||
DNSBL bool
|
||||
Logging *Logging
|
||||
Impressum *Impressum
|
||||
OpenGraph OpenGraph
|
||||
StatusCodes StatusCodes
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Logging struct {
|
||||
Filters []LogFilter `json:"filters,omitempty" yaml:"filters,omitempty"`
|
||||
}
|
||||
|
||||
func (l *Logging) Valid() error {
|
||||
var errs []error
|
||||
|
||||
for _, lf := range l.Filters {
|
||||
if err := lf.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type LogFilter struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Expression ExpressionOrList `json:"expression" yaml:"expression"`
|
||||
}
|
||||
|
||||
func (lf LogFilter) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if lf.Name == "" {
|
||||
errs = append(errs, fmt.Errorf("%w: log filter has no name", ErrMissingValue))
|
||||
}
|
||||
|
||||
if err := lf.Expression.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("log filter %q is not valid: %w", lf.Name, errors.Join(errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package expressions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
timestamp "google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
var (
|
||||
filterExecutionTime = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "anubis",
|
||||
Subsystem: "slog",
|
||||
Name: "filter_execution_time_nanoseconds",
|
||||
Help: "How long each log filter took to execute (nanoseconds)",
|
||||
Buckets: []float64{10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1000000, 2000000, 5000000, 10000000}, // 10 nanoseconds to 10 milliseconds
|
||||
}, []string{"name"})
|
||||
)
|
||||
|
||||
func LogFilter(opts ...cel.EnvOption) (*cel.Env, error) {
|
||||
return New(
|
||||
// Slog record metadata
|
||||
cel.Variable("time", cel.TimestampType),
|
||||
cel.Variable("msg", cel.StringType),
|
||||
cel.Variable("level", cel.StringType),
|
||||
cel.Variable("attrs", cel.MapType(cel.StringType, cel.StringType)),
|
||||
)
|
||||
}
|
||||
|
||||
func NewFilter(lg *slog.Logger, name, src string) (*Filter, error) {
|
||||
env, err := LogFilter()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("logging: can't create CEL env: %w", err)
|
||||
}
|
||||
|
||||
program, err := Compile(env, src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("logging: can't compile expression: Compile(%q): %w", src, err)
|
||||
}
|
||||
|
||||
return &Filter{
|
||||
program: program,
|
||||
name: name,
|
||||
src: src,
|
||||
log: lg.With("filter", name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Filter struct {
|
||||
program cel.Program
|
||||
name string
|
||||
src string
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func (f Filter) Filter(ctx context.Context, r slog.Record) bool {
|
||||
t0 := time.Now()
|
||||
|
||||
result, _, err := f.program.ContextEval(ctx, &Record{
|
||||
Record: r,
|
||||
})
|
||||
if err != nil {
|
||||
f.log.Error("error executing log filter", "err", err, "src", f.src)
|
||||
return false
|
||||
}
|
||||
dur := time.Since(t0)
|
||||
filterExecutionTime.WithLabelValues(f.name).Observe(float64(dur.Nanoseconds()))
|
||||
//f.log.Debug("filter execution", "dur", dur.Nanoseconds())
|
||||
|
||||
if val, ok := result.(types.Bool); ok {
|
||||
return !bool(val)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
slog.Record
|
||||
attrs map[string]string
|
||||
}
|
||||
|
||||
func (r *Record) Parent() cel.Activation { return nil }
|
||||
|
||||
func (r *Record) ResolveName(name string) (any, bool) {
|
||||
switch name {
|
||||
case "time":
|
||||
return ×tamp.Timestamp{Seconds: r.Time.Unix()}, true
|
||||
case "msg":
|
||||
return r.Message, true
|
||||
case "level":
|
||||
return r.Level.String(), true
|
||||
case "attrs":
|
||||
if r.attrs == nil {
|
||||
attrs := map[string]string{}
|
||||
|
||||
r.Attrs(func(attr slog.Attr) bool {
|
||||
attrs[attr.Key] = attr.Value.String()
|
||||
return true
|
||||
})
|
||||
|
||||
r.attrs = attrs
|
||||
return attrs, true
|
||||
}
|
||||
return r.attrs, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package expressions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func BenchmarkFilter(b *testing.B) {
|
||||
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
filter, err := NewFilter(log, "benchmark", `msg == "hello"`)
|
||||
if err != nil {
|
||||
b.Fatalf("NewFilter() error = %v", err)
|
||||
}
|
||||
|
||||
record := slog.NewRecord(time.Now(), slog.LevelInfo, "hello", 0)
|
||||
record.AddAttrs(slog.String("foo", "bar"))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
filter.Filter(ctx, record)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilterAttributes(b *testing.B) {
|
||||
for _, numAttrs := range []int{1, 2, 4, 8, 16, 32} {
|
||||
b.Run(fmt.Sprintf("%d_attributes", numAttrs), func(b *testing.B) {
|
||||
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`msg == "hello" && "foo" in attrs`)
|
||||
|
||||
attrs := make([]slog.Attr, numAttrs)
|
||||
for i := range numAttrs {
|
||||
key := fmt.Sprintf("foo%d", i)
|
||||
val := "bar"
|
||||
attrs[i] = slog.String(key, val)
|
||||
}
|
||||
|
||||
filter, err := NewFilter(log, "benchmark", sb.String())
|
||||
if err != nil {
|
||||
b.Fatalf("NewFilter() error = %v", err)
|
||||
}
|
||||
|
||||
record := slog.NewRecord(time.Now(), slog.LevelInfo, "hello", 0)
|
||||
record.AddAttrs(attrs...)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
filter.Filter(ctx, record)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,8 @@ import (
|
||||
"log/slog"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/logging"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||
"github.com/TecharoHQ/anubis/lib/policy/expressions"
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
"github.com/TecharoHQ/anubis/lib/thoth"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -37,42 +35,17 @@ type ParsedConfig struct {
|
||||
Thresholds []*Threshold
|
||||
DNSBL bool
|
||||
Impressum *config.Impressum
|
||||
Logging *config.Logging
|
||||
OpenGraph config.OpenGraph
|
||||
DefaultDifficulty int
|
||||
StatusCodes config.StatusCodes
|
||||
Store store.Interface
|
||||
}
|
||||
|
||||
func (pc *ParsedConfig) ApplyLogFilters(base *slog.Logger) (*slog.Logger, error) {
|
||||
var errs []error
|
||||
var filters []logging.Filterer
|
||||
|
||||
for _, f := range pc.Logging.Filters {
|
||||
filter, err := expressions.NewFilter(base, f.Name, f.Expression.String())
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("filter %s invalid: %w", f.Name, err))
|
||||
continue
|
||||
}
|
||||
filters = append(filters, filter)
|
||||
}
|
||||
|
||||
result := slog.New(logging.NewFilterHandler(base.Handler(), filters...))
|
||||
slog.SetDefault(result)
|
||||
|
||||
if len(errs) != 0 {
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func newParsedConfig(orig *config.Config) *ParsedConfig {
|
||||
return &ParsedConfig{
|
||||
orig: orig,
|
||||
OpenGraph: orig.OpenGraph,
|
||||
StatusCodes: orig.StatusCodes,
|
||||
Logging: orig.Logging,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,5 @@ package all
|
||||
import (
|
||||
_ "github.com/TecharoHQ/anubis/lib/store/bbolt"
|
||||
_ "github.com/TecharoHQ/anubis/lib/store/memory"
|
||||
_ "github.com/TecharoHQ/anubis/lib/store/s3api"
|
||||
_ "github.com/TecharoHQ/anubis/lib/store/valkey"
|
||||
)
|
||||
|
||||
@@ -11,9 +11,10 @@ import (
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// Sentinel error value used for testing and in admin-visible error messages.
|
||||
// Sentinel error values used for testing and in admin-visible error messages.
|
||||
var (
|
||||
ErrNotExists = errors.New("bbolt: value does not exist in store")
|
||||
ErrBucketDoesNotExist = errors.New("bbolt: bucket does not exist")
|
||||
ErrNotExists = errors.New("bbolt: value does not exist in store")
|
||||
)
|
||||
|
||||
// Store implements store.Interface backed by bbolt[1].
|
||||
@@ -149,10 +150,6 @@ func (s *Store) cleanup(ctx context.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) IsPersistent() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Store) cleanupThread(ctx context.Context) {
|
||||
t := time.NewTicker(time.Hour)
|
||||
defer t.Stop()
|
||||
|
||||
@@ -37,11 +37,6 @@ type Interface interface {
|
||||
|
||||
// Set puts a value into the store that expires according to its expiry.
|
||||
Set(ctx context.Context, key string, value []byte, expiry time.Duration) error
|
||||
|
||||
// IsPersistent returns true if this storage backend persists data across
|
||||
// service restarts (e.g., bbolt, valkey). Returns false for volatile storage
|
||||
// like in-memory backends.
|
||||
IsPersistent() bool
|
||||
}
|
||||
|
||||
func z[T any]() T { return *new(T) }
|
||||
@@ -93,7 +88,3 @@ func (j *JSON[T]) Set(ctx context.Context, key string, value T, expiry time.Dura
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JSON[T]) IsPersistent() bool {
|
||||
return j.Underlying.IsPersistent()
|
||||
}
|
||||
|
||||
@@ -48,10 +48,6 @@ func (i *impl) Set(_ context.Context, key string, value []byte, expiry time.Dura
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *impl) IsPersistent() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (i *impl) cleanupThread(ctx context.Context) {
|
||||
t := time.NewTicker(5 * time.Minute)
|
||||
defer t.Stop()
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
awsConfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoRegion = errors.New("s3api.Config: no region env var name defined")
|
||||
ErrNoAccessKeyID = errors.New("s3api.Config: no access key id env var name defined")
|
||||
ErrNoSecretAccessKey = errors.New("s3api.Config: no secret access key env var name defined")
|
||||
ErrNoBucketName = errors.New("s3api.Config: no bucket name env var name defined")
|
||||
)
|
||||
|
||||
func init() {
|
||||
store.Register("s3api", Factory{})
|
||||
}
|
||||
|
||||
// S3API is the subset of the AWS S3 client used by this store. It enables mocking in tests.
|
||||
type S3API interface {
|
||||
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
|
||||
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
|
||||
DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error)
|
||||
HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
|
||||
}
|
||||
|
||||
// Factory builds an S3-backed store. Tests can inject a Mock via Client.
|
||||
// Factory can optionally carry a preconstructed S3 client (e.g., a mock in tests).
|
||||
type Factory struct {
|
||||
Client S3API
|
||||
}
|
||||
|
||||
func (f Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface, error) {
|
||||
var config Config
|
||||
|
||||
if err := json.Unmarshal([]byte(data), &config); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||
}
|
||||
|
||||
if err := config.Valid(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||
}
|
||||
|
||||
if config.BucketName == "" {
|
||||
return nil, fmt.Errorf("%w: %s", store.ErrBadConfig, ErrNoBucketName)
|
||||
}
|
||||
|
||||
// If a client was injected (e.g., tests), use it directly.
|
||||
if f.Client != nil {
|
||||
return &Store{
|
||||
s3: f.Client,
|
||||
bucket: config.BucketName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
cfg, err := awsConfig.LoadDefaultConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load AWS config from environment: %w", err)
|
||||
}
|
||||
|
||||
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
||||
o.UsePathStyle = config.PathStyle
|
||||
})
|
||||
|
||||
return &Store{
|
||||
s3: client,
|
||||
bucket: config.BucketName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (Factory) Valid(data json.RawMessage) error {
|
||||
var config Config
|
||||
if err := json.Unmarshal([]byte(data), &config); err != nil {
|
||||
return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||
}
|
||||
|
||||
if err := config.Valid(); err != nil {
|
||||
return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
PathStyle bool `json:"pathStyle"`
|
||||
BucketName string `json:"bucketName"`
|
||||
}
|
||||
|
||||
func (c Config) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if c.BucketName == "" {
|
||||
errs = append(errs, ErrNoBucketName)
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("s3api.Config: invalid config: %w", errors.Join(errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/store"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
s3 S3API
|
||||
bucket string
|
||||
}
|
||||
|
||||
func (s *Store) Delete(ctx context.Context, key string) error {
|
||||
normKey := strings.ReplaceAll(key, ":", "/")
|
||||
// Emulate not found by probing first.
|
||||
if _, err := s.s3.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &s.bucket, Key: &normKey}); err != nil {
|
||||
return fmt.Errorf("%w: %w", store.ErrNotFound, err)
|
||||
}
|
||||
if _, err := s.s3.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: &s.bucket, Key: &normKey}); err != nil {
|
||||
return fmt.Errorf("can't delete from s3: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
normKey := strings.ReplaceAll(key, ":", "/")
|
||||
out, err := s.s3.GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: &s.bucket,
|
||||
Key: &normKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", store.ErrNotFound, err)
|
||||
}
|
||||
defer out.Body.Close()
|
||||
if msStr, ok := out.Metadata["x-anubis-expiry-ms"]; ok && msStr != "" {
|
||||
if ms, err := strconv.ParseInt(msStr, 10, 64); err == nil {
|
||||
if time.Now().UnixMilli() >= ms {
|
||||
_, _ = s.s3.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: &s.bucket, Key: &normKey})
|
||||
return nil, store.ErrNotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
b, err := io.ReadAll(out.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't read s3 object: %w", err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (s *Store) Set(ctx context.Context, key string, value []byte, expiry time.Duration) error {
|
||||
normKey := strings.ReplaceAll(key, ":", "/")
|
||||
// S3 has no native TTL; we store object with metadata X-Anubis-Expiry as epoch seconds.
|
||||
var meta map[string]string
|
||||
if expiry > 0 {
|
||||
exp := time.Now().Add(expiry).UnixMilli()
|
||||
meta = map[string]string{"x-anubis-expiry-ms": fmt.Sprintf("%d", exp)}
|
||||
}
|
||||
_, err := s.s3.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: &s.bucket,
|
||||
Key: &normKey,
|
||||
Body: bytes.NewReader(value),
|
||||
Metadata: meta,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't put s3 object: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Store) IsPersistent() bool { return true }
|
||||
@@ -1,140 +0,0 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/store/storetest"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
)
|
||||
|
||||
// mockS3 is an in-memory mock of the methods we use.
|
||||
type mockS3 struct {
|
||||
mu sync.RWMutex
|
||||
bucket string
|
||||
data map[string][]byte
|
||||
meta map[string]map[string]string
|
||||
}
|
||||
|
||||
func (m *mockS3) PutObject(ctx context.Context, in *s3.PutObjectInput, _ ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.data == nil {
|
||||
m.data = map[string][]byte{}
|
||||
}
|
||||
if m.meta == nil {
|
||||
m.meta = map[string]map[string]string{}
|
||||
}
|
||||
b, _ := io.ReadAll(in.Body)
|
||||
m.data[aws.ToString(in.Key)] = bytes.Clone(b)
|
||||
if in.Metadata != nil {
|
||||
m.meta[aws.ToString(in.Key)] = map[string]string{}
|
||||
for k, v := range in.Metadata {
|
||||
m.meta[aws.ToString(in.Key)][k] = v
|
||||
}
|
||||
}
|
||||
m.bucket = aws.ToString(in.Bucket)
|
||||
return &s3.PutObjectOutput{}, nil
|
||||
}
|
||||
|
||||
func (m *mockS3) GetObject(ctx context.Context, in *s3.GetObjectInput, _ ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
b, ok := m.data[aws.ToString(in.Key)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
out := &s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader(b))}
|
||||
if md, ok := m.meta[aws.ToString(in.Key)]; ok {
|
||||
out.Metadata = md
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *mockS3) DeleteObject(ctx context.Context, in *s3.DeleteObjectInput, _ ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.data, aws.ToString(in.Key))
|
||||
delete(m.meta, aws.ToString(in.Key))
|
||||
return &s3.DeleteObjectOutput{}, nil
|
||||
}
|
||||
|
||||
func (m *mockS3) HeadObject(ctx context.Context, in *s3.HeadObjectInput, _ ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if _, ok := m.data[aws.ToString(in.Key)]; !ok {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
return &s3.HeadObjectOutput{}, nil
|
||||
}
|
||||
|
||||
func TestImpl(t *testing.T) {
|
||||
mock := &mockS3{}
|
||||
f := Factory{Client: mock}
|
||||
|
||||
data, _ := json.Marshal(Config{
|
||||
BucketName: "bucket",
|
||||
})
|
||||
|
||||
storetest.Common(t, f, json.RawMessage(data))
|
||||
}
|
||||
|
||||
func TestKeyNormalization(t *testing.T) {
|
||||
mock := &mockS3{}
|
||||
f := Factory{Client: mock}
|
||||
|
||||
data, _ := json.Marshal(Config{
|
||||
BucketName: "anubis",
|
||||
})
|
||||
|
||||
s, err := f.Build(t.Context(), json.RawMessage(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
key := "a:b:c"
|
||||
val := []byte("value")
|
||||
if err := s.Set(t.Context(), key, val, 0); err != nil {
|
||||
t.Fatalf("Set failed: %v", err)
|
||||
}
|
||||
// Ensure mock saw normalized key
|
||||
mock.mu.RLock()
|
||||
_, hasRaw := mock.data["a:b:c"]
|
||||
got, hasNorm := mock.data["a/b/c"]
|
||||
mock.mu.RUnlock()
|
||||
if hasRaw {
|
||||
t.Fatalf("mock contains raw key with colon; normalization failed")
|
||||
}
|
||||
if !hasNorm || !bytes.Equal(got, val) {
|
||||
t.Fatalf("normalized key missing or wrong value: got=%q", string(got))
|
||||
}
|
||||
|
||||
// Get using colon key should work
|
||||
out, err := s.Get(t.Context(), key)
|
||||
if err != nil {
|
||||
t.Fatalf("Get failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(out, val) {
|
||||
t.Fatalf("Get returned wrong value: got=%q", string(out))
|
||||
}
|
||||
|
||||
// Delete using colon key should delete normalized object
|
||||
if err := s.Delete(t.Context(), key); err != nil {
|
||||
t.Fatalf("Delete failed: %v", err)
|
||||
}
|
||||
// Give any async cleanup in tests a tick (not needed for mock, but harmless)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
mock.mu.RLock()
|
||||
_, exists := mock.data["a/b/c"]
|
||||
mock.mu.RUnlock()
|
||||
if exists {
|
||||
t.Fatalf("normalized key still exists after Delete")
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,3 @@ func (s *Store) Set(ctx context.Context, key string, value []byte, expiry time.D
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) IsPersistent() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
710
package-lock.json
generated
710
package-lock.json
generated
@@ -19,7 +19,7 @@
|
||||
"playwright": "^1.52.0",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-import-url": "^1.0.0",
|
||||
"postcss-import-url": "^7.2.0",
|
||||
"postcss-url": "^10.1.3"
|
||||
}
|
||||
},
|
||||
@@ -553,13 +553,6 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
@@ -658,13 +651,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
@@ -788,18 +774,6 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cli": {
|
||||
"version": "0.4.4-2",
|
||||
"resolved": "https://registry.npmjs.org/cli/-/cli-0.4.4-2.tgz",
|
||||
"integrity": "sha512-zvFHTz+T8S4gejPHNVtdqc0mDnWmZcwd5juDF4ScZkPerNdl/9aiWcBv3l57v81jzq+n89eYLkRJdvc5aWJROA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"glob": ">= 3.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
@@ -859,17 +833,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/config-chain": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
||||
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ini": "^1.3.4",
|
||||
"proto-list": "~1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/css-declaration-sorter": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz",
|
||||
@@ -1084,23 +1047,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-equal": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.1.2.tgz",
|
||||
"integrity": "sha512-rUCt39nKM7s6qUyYgp/reJmtXjgkOS/JbLO24DioMZaBNkD3b7C7cD3zJjSyjclEElNTpetAIRD6fMIbBIbX1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dependency-graph": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz",
|
||||
@@ -1111,15 +1057,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz",
|
||||
"integrity": "sha512-0bTLzyr1S59cPsgAD/lR+ivvHTbgPb+k/mUR6WGqma1J6QDU+kUegI8uQFuH/cMUNK7JGN3Tk1Y5Jf2MO85WrA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
@@ -1258,16 +1195,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -1295,19 +1222,10 @@
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.1.0.tgz",
|
||||
"integrity": "sha512-ROG9M8tikYOuOJsvRBggh10WiQ/JebnldAwuCaQyFoiAUIE9XrYVnpznIjOQGZfCMzxzEBYHQr/LHJp3tcndzQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz",
|
||||
"integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==",
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
||||
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1319,13 +1237,6 @@
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
@@ -1361,28 +1272,6 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
@@ -1396,19 +1285,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -1416,35 +1292,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/growl": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz",
|
||||
"integrity": "sha512-VWv7s1EI41AG2LiCr7uAuxWikLDN1SQOuEUc37d/P34NAIIYgkvWYngNw0d9d9iCrDFL0SYCE9UQpxhIjjtuLg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/has-ansi": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
|
||||
"integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-ansi/node_modules/ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -1465,32 +1312,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
@@ -1570,68 +1391,10 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jade": {
|
||||
"version": "0.26.3",
|
||||
"resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
|
||||
"integrity": "sha512-mkk3vzUHFjzKjpCXeu+IjXeZD+QOTjUUdubgmHtHTDwvAO2ZTkMTTVrapts5CWz3JvJryh/4KWZpjeZrCepZ3A==",
|
||||
"deprecated": "Jade has been renamed to pug, please install the latest version of pug instead of jade",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"commander": "0.6.1",
|
||||
"mkdirp": "0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"jade": "bin/jade"
|
||||
}
|
||||
},
|
||||
"node_modules/jade/node_modules/commander": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz",
|
||||
"integrity": "sha512-0fLycpl1UMTGX257hRsu/arL/cUbcvQM4zMKwvLvzXtfdezIV4yotPS2dYtknF+NmEfWSoCEF6+hj9XLm/6hEw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/jade/node_modules/mkdirp": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
|
||||
"integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==",
|
||||
"deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
|
||||
"dev": true,
|
||||
"license": "MIT/X11",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/js-base64": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
|
||||
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/js-beautify": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.4.2.tgz",
|
||||
"integrity": "sha512-0o7oku1AcG66QoDIoSLCBENbyFgV6WHoqnZhC8oL4URTWYDzIXWo3tTGTLrLh6jR91miKS5YC+WBZeYC5iZMQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"config-chain": "~1.1.5",
|
||||
"mkdirp": "0.3.5",
|
||||
"nopt": "~2.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"css-beautify": "js/bin/css-beautify.js",
|
||||
"html-beautify": "js/bin/html-beautify.js",
|
||||
"js-beautify": "js/bin/js-beautify.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1654,6 +1417,13 @@
|
||||
"url": "https://github.com/sponsors/antonk52"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.assign": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
|
||||
"integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -1661,6 +1431,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.trim": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.trim/-/lodash.trim-4.5.1.tgz",
|
||||
"integrity": "sha512-nJAlRl/K+eiOehWKDzoBVrSMhK0K3A3YQsUNXHQa5yIrKBAhsZgSu3KoAFoFT+mEgiyBHddZ0pRk1ITpIp90Wg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
@@ -1668,13 +1445,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz",
|
||||
"integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
@@ -1724,95 +1494,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
|
||||
"integrity": "sha512-8OCq0De/h9ZxseqzCH8Kw/Filf5pF/vMI6+BH7Lu0jXz2pqYCjTAQRolSxRIi+Ax+oCCjlxoJMP0YQ4XlrQNHg==",
|
||||
"deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.0.tgz",
|
||||
"integrity": "sha512-Bmjo5ZIr+RcxCKRLFpE7tpGiYemqCkWNVBx31seyUv+c45MahZcBBcoRN33yMhvOBmiq0ABhpENk19WtM3BcOw==",
|
||||
"deprecated": "Mocha v1.x is no longer supported.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"commander": "2.0.0",
|
||||
"debug": "*",
|
||||
"diff": "1.0.7",
|
||||
"glob": "3.2.3",
|
||||
"growl": "1.7.x",
|
||||
"jade": "0.26.3",
|
||||
"mkdirp": "0.3.5"
|
||||
},
|
||||
"bin": {
|
||||
"_mocha": "bin/_mocha",
|
||||
"mocha": "bin/mocha"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/commander": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz",
|
||||
"integrity": "sha512-qebjpyeaA/nJ4w3EO2cV2++/zEkccPnjWogzA2rff+Lk8ILI75vULeTmyd4wPxWdKwtP3J+G39IXVZadh0UHyw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.x"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/glob": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz",
|
||||
"integrity": "sha512-WPaLsMHD1lYEqAmIQI6VOJSPwuBdGShDWnj1yUo0vQqEO809R8W3LM9OVU13CnnDhyv/EiNwOtxEW74SmrzS6w==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "BSD",
|
||||
"dependencies": {
|
||||
"graceful-fs": "~2.0.0",
|
||||
"inherits": "2",
|
||||
"minimatch": "~0.2.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/graceful-fs": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz",
|
||||
"integrity": "sha512-hcj/NTUWv+C3MbqrVb9F+aH6lvTwEHJdx2foBxlrVq5h6zE8Bfu4pv4CAAqbDcZrw/9Ak5lsRXlY9Ao8/F0Tuw==",
|
||||
"deprecated": "please upgrade to graceful-fs 4 for compatibility with current and future versions of Node.js",
|
||||
"dev": true,
|
||||
"license": "BSD",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/minimatch": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
|
||||
"integrity": "sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA==",
|
||||
"deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lru-cache": "2",
|
||||
"sigmund": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -1840,19 +1521,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.2.tgz",
|
||||
"integrity": "sha512-x8vXm7BZ2jE1Txrxh/hO74HTuYZQEbo8edoRcANgdZ4+PCV+pbjd/xdummkmjjC7LU5EjPzlu8zEq/oxWylnKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abbrev": "1"
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -1886,26 +1554,6 @@
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
@@ -1913,83 +1561,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/phpfn": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/phpfn/-/phpfn-1.0.0.tgz",
|
||||
"integrity": "sha512-hwl2fXpXDOHCOlFDkNYXwD4FUKsddgXooF7Cb8eyynt82Ej9DLVfL6P/2d6L0uQghJq1X6DUnTd0rKM3yC8oOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"phpjs": "latest"
|
||||
}
|
||||
},
|
||||
"node_modules/phpjs": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/phpjs/-/phpjs-1.3.2.tgz",
|
||||
"integrity": "sha512-S/V298ABWBDLsWgssVl91JmexMvTmmBR4oufeHvQU3W63+xOBluVtbVEoMyxv6ZdFuj/fx6BXe/WC6gWnO+lig==",
|
||||
"deprecated": "phpjs is no longer maintained. Please use Locutus instead: https://locutus.io",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cli": "0.4.4-2",
|
||||
"deep-equal": "0.1.2",
|
||||
"glob": "3.2.1",
|
||||
"js-beautify": "1.4.2",
|
||||
"mocha": "1.17.0",
|
||||
"send": "0.1.0",
|
||||
"underscore": "1.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/phpjs/node_modules/glob": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-3.2.1.tgz",
|
||||
"integrity": "sha512-wvxQZUqjkvW//FJMr/DCmPlAOFcrmf2ojnUddQTdgAQ5XkKL8ILfob0Rz+Ch/fSiols6EtiHRJS3i9W0kBRZmQ==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "BSD",
|
||||
"dependencies": {
|
||||
"graceful-fs": "~1.2.0",
|
||||
"inherits": "1",
|
||||
"minimatch": "~0.2.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/phpjs/node_modules/graceful-fs": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz",
|
||||
"integrity": "sha512-iiTUZ5vZ+2ZV+h71XAgwCSu6+NAizhFU3Yw8aC/hH5SQ3SnISqEqAek40imAFGtDcwJKNhXvSY+hzIolnLwcdQ==",
|
||||
"deprecated": "please upgrade to graceful-fs 4 for compatibility with current and future versions of Node.js",
|
||||
"dev": true,
|
||||
"license": "BSD",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/phpjs/node_modules/inherits": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz",
|
||||
"integrity": "sha512-Al67oatbRSo3RV5hRqIoln6Y5yMVbJSIn4jEJNL7VCImzq/kLr7vvb6sFRJXqr8rpHc/2kJOM+y0sPKN47VdzA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/phpjs/node_modules/minimatch": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
|
||||
"integrity": "sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA==",
|
||||
"deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lru-cache": "2",
|
||||
"sigmund": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -2254,116 +1825,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-import-url": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-import-url/-/postcss-import-url-1.0.0.tgz",
|
||||
"integrity": "sha512-sXZVBws7VJZDc3P60oTI/7hR5I5EZnjIrmm9QFQY6iwhdmRHi4o9deYoAcnV6jaKrPzzaqO8VGrxf6X2yxUfHQ==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-import-url/-/postcss-import-url-7.2.0.tgz",
|
||||
"integrity": "sha512-El61K/5+Rv753G9mBiHyQlOIN2mBfN0YHPMXLlgIo/m1+tPDLM32wd97WoUjc8FHUnC6EyyfVA8RDuKoyuVl0Q==",
|
||||
"dev": true,
|
||||
"license": "Beerware",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bluebird": "^3.0.2",
|
||||
"http-https": "^1.0.0",
|
||||
"is-url": "^1.2.1",
|
||||
"phpfn": "^1.0.0",
|
||||
"postcss": "^5.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-import-url/node_modules/ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-import-url/node_modules/ansi-styles": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
|
||||
"integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-import-url/node_modules/chalk": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
"integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^2.2.1",
|
||||
"escape-string-regexp": "^1.0.2",
|
||||
"has-ansi": "^2.0.0",
|
||||
"strip-ansi": "^3.0.0",
|
||||
"supports-color": "^2.0.0"
|
||||
"is-url": "^1.2.4",
|
||||
"lodash.assign": "^4.2.0",
|
||||
"lodash.trim": "^4.5.1",
|
||||
"resolve-relative-url": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-import-url/node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
|
||||
"integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-import-url/node_modules/has-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-import-url/node_modules/postcss": {
|
||||
"version": "5.2.18",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
|
||||
"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^1.1.3",
|
||||
"js-base64": "^2.1.9",
|
||||
"source-map": "^0.5.6",
|
||||
"supports-color": "^3.2.3"
|
||||
"node": ">=10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-import-url/node_modules/strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-import-url/node_modules/supports-color": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
|
||||
"integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-load-config": {
|
||||
@@ -2869,20 +2347,21 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/proto-list": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
|
||||
"node_modules/punycode": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||
"integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz",
|
||||
"integrity": "sha512-okJVEq9DbZyg+5lD8pr6ooQmeA0uu8DYIyAU7VK1WUUK7hctI1yw2ZHhKiKjB6RXaDrYRmTR4SsIHkyiQpaLMA==",
|
||||
"node_modules/querystring": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||
"integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==",
|
||||
"deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
@@ -2939,6 +2418,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-relative-url": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-relative-url/-/resolve-relative-url-1.0.0.tgz",
|
||||
"integrity": "sha512-zpcelQBAmrwckiyRmym9os1goECU3EzuTU/UrYkGzXV0i14n8FkyGUvwkOYA5klqVLq1Hz/EiFZMS7bZQdd+EA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"url": "0.10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
|
||||
@@ -2956,34 +2445,6 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.1.0.tgz",
|
||||
"integrity": "sha512-D/GaJQQYx7ICNq9Te5V4wHetfDQdFk3HJ4oBfDUBNW7XQmLbJ8sQDm/wFvVUUpKN8tluOnO1dFdM8KODn6D79w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "*",
|
||||
"fresh": "0.1.0",
|
||||
"mime": "1.2.6",
|
||||
"range-parser": "0.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/mime": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.2.6.tgz",
|
||||
"integrity": "sha512-S4yfg1ehMduQ5F3NeTUUWJesnut4RvymaRSatO4etOm68yZE98oCg2GtgG0coGYx03GCv240sezMvRwFk8DUKw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/sigmund": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
|
||||
"integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
|
||||
@@ -2997,16 +2458,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -3109,14 +2560,14 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -3126,14 +2577,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
@@ -3144,9 +2592,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3175,12 +2623,6 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz",
|
||||
"integrity": "sha512-yejOFsRnTJs0N9CK5Apzf6maDO2djxGoLLrlZlvGs2o9ZQuhIhDL18rtFyy4FBIbOkzA6+4hDgXbgz5EvDQCXQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
@@ -3222,6 +2664,17 @@
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
|
||||
"integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "1.3.2",
|
||||
"querystring": "0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -3247,13 +2700,6 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xxhashjs": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
|
||||
@@ -3275,9 +2721,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
||||
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"playwright": "^1.52.0",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-import-url": "^1.0.0",
|
||||
"postcss-import-url": "^7.2.0",
|
||||
"postcss-url": "^10.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
20
test/go.mod
20
test/go.mod
@@ -5,7 +5,7 @@ go 1.24.5
|
||||
replace github.com/TecharoHQ/anubis => ..
|
||||
|
||||
require (
|
||||
github.com/TecharoHQ/anubis v1.22.0
|
||||
github.com/TecharoHQ/anubis v1.21.3
|
||||
github.com/docker/docker v28.3.2+incompatible
|
||||
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -18,24 +18,6 @@ require (
|
||||
github.com/TecharoHQ/thoth-proto v0.4.0 // indirect
|
||||
github.com/a-h/templ v0.3.924 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
|
||||
36
test/go.sum
36
test/go.sum
@@ -16,42 +16,6 @@ github.com/a-h/templ v0.3.924 h1:t5gZqTneXqvehpNZsgtnlOscnBboNh9aASBH2MgV/0k=
|
||||
github.com/a-h/templ v0.3.924/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 h1:R0tNFJqfjHL3900cqhXuwQ+1K4G0xc9Yf8EDbFXCKEw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6/go.mod h1:y/7sDdu+aJvPtGXr4xYosdpq9a6T9Z0jkXfugmti0rI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 h1:hncKj/4gR+TPauZgTAsxOxNcvBayhUlYZ6LO/BYiQ30=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6/go.mod h1:OiIh45tp6HdJDDJGnja0mw8ihQGz3VGrUflLqSL0SmM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 h1:nEXUSAwyUfLTgnc9cxlDWy637qsq4UWwp3sNAfl0Z3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6/go.mod h1:HGzIULx4Ge3Do2V0FaiYKcyKzOqwrhUZgCI77NisswQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 h1:ETkfWcXP2KNPLecaDa++5bsQhCRa5M5sLUJa5DWYIIg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3/go.mod h1:+/3ZTqoYb3Ur7DObD00tarKMLMuKg8iqz5CHEanqTnw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
|
||||
19
web/build.sh
19
web/build.sh
@@ -39,18 +39,9 @@ for the JavaScript code in this page.
|
||||
mkdir -p static/locales
|
||||
cp ../lib/localization/locales/*.json static/locales/
|
||||
|
||||
shopt -s nullglob globstar
|
||||
|
||||
for file in js/**/*.ts js/**/*.mjs; do
|
||||
out="static/${file}"
|
||||
if [[ "$file" == *.ts ]]; then
|
||||
out="static/${file%.ts}.mjs"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$out")"
|
||||
|
||||
esbuild "$file" --sourcemap --bundle --minify --outfile="$out" --banner:js="$LICENSE"
|
||||
gzip -f -k -n "$out"
|
||||
zstd -f -k --ultra -22 "$out"
|
||||
brotli -fZk "$out"
|
||||
for file in js/*.mjs js/worker/*.mjs; do
|
||||
esbuild "${file}" --sourcemap --bundle --minify --outfile=static/"${file}" --banner:js="${LICENSE}"
|
||||
gzip -f -k -n static/${file}
|
||||
zstd -f -k --ultra -22 static/${file}
|
||||
brotli -fZk static/${file}
|
||||
done
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
type ProgressCallback = (nonce: number) => void;
|
||||
|
||||
interface ProcessOptions {
|
||||
basePrefix: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
const getHardwareConcurrency = () =>
|
||||
navigator.hardwareConcurrency !== undefined ? navigator.hardwareConcurrency : 1;
|
||||
|
||||
export default function process(
|
||||
options: ProcessOptions,
|
||||
data: string,
|
||||
difficulty: number = 5,
|
||||
signal: AbortSignal | null = null,
|
||||
progressCallback?: ProgressCallback,
|
||||
threads: number = Math.trunc(Math.max(getHardwareConcurrency() / 2, 1)),
|
||||
): Promise<string> {
|
||||
{ basePrefix, version },
|
||||
data,
|
||||
difficulty = 5,
|
||||
signal = null,
|
||||
progressCallback = null,
|
||||
threads = Math.trunc(Math.max(navigator.hardwareConcurrency / 2, 1)),
|
||||
) {
|
||||
console.debug("fast algo");
|
||||
|
||||
let workerMethod = window.crypto !== undefined ? "webcrypto" : "purejs";
|
||||
@@ -26,17 +16,13 @@ export default function process(
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let webWorkerURL = `${options.basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${options.version}`;
|
||||
let webWorkerURL = `${basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${version}`;
|
||||
|
||||
const workers: Worker[] = [];
|
||||
console.log(webWorkerURL);
|
||||
|
||||
const workers = [];
|
||||
let settled = false;
|
||||
|
||||
const onAbort = () => {
|
||||
console.log("PoW aborted");
|
||||
cleanup();
|
||||
reject(new DOMException("Aborted", "AbortError"));
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (settled) {
|
||||
return;
|
||||
@@ -48,6 +34,12 @@ export default function process(
|
||||
}
|
||||
};
|
||||
|
||||
const onAbort = () => {
|
||||
console.log("PoW aborted");
|
||||
cleanup();
|
||||
reject(new DOMException("Aborted", "AbortError"));
|
||||
};
|
||||
|
||||
if (signal != null) {
|
||||
if (signal.aborted) {
|
||||
return onAbort();
|
||||
@@ -1,4 +1,4 @@
|
||||
import fast from "./fast";
|
||||
import fast from "./fast.mjs";
|
||||
|
||||
export default {
|
||||
fast: fast,
|
||||
@@ -1,24 +1,20 @@
|
||||
import algorithms from "./algorithms";
|
||||
import algorithms from "./algorithms/index.mjs";
|
||||
|
||||
const defaultDifficulty = 4;
|
||||
|
||||
const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement;
|
||||
const difficultyInput: HTMLInputElement = document.getElementById("difficulty-input") as HTMLInputElement;
|
||||
const algorithmSelect: HTMLSelectElement = document.getElementById("algorithm-select") as HTMLSelectElement;
|
||||
const compareSelect: HTMLSelectElement = document.getElementById("compare-select") as HTMLSelectElement;
|
||||
const header: HTMLTableRowElement = document.getElementById("table-header") as HTMLTableRowElement;
|
||||
const headerCompare: HTMLTableSectionElement = document.getElementById("table-header-compare") as HTMLTableSectionElement;
|
||||
const results: HTMLTableRowElement = document.getElementById("results") as HTMLTableRowElement;
|
||||
const status = document.getElementById("status");
|
||||
const difficultyInput = document.getElementById("difficulty-input");
|
||||
const algorithmSelect = document.getElementById("algorithm-select");
|
||||
const compareSelect = document.getElementById("compare-select");
|
||||
const header = document.getElementById("table-header");
|
||||
const headerCompare = document.getElementById("table-header-compare");
|
||||
const results = document.getElementById("results");
|
||||
|
||||
const setupControls = () => {
|
||||
if (defaultDifficulty == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
difficultyInput.value = defaultDifficulty.toString();
|
||||
difficultyInput.value = defaultDifficulty;
|
||||
for (const alg of Object.keys(algorithms)) {
|
||||
const option1 = document.createElement("option");
|
||||
algorithmSelect?.append(option1);
|
||||
algorithmSelect.append(option1);
|
||||
const option2 = document.createElement("option");
|
||||
compareSelect.append(option2);
|
||||
option1.value = option1.innerText = option2.value = option2.innerText = alg;
|
||||
@@ -120,13 +116,13 @@ const benchmarkLoop = async (controller) => {
|
||||
await benchmarkLoop(controller);
|
||||
};
|
||||
|
||||
let controller: AbortController | null = null;
|
||||
let controller = null;
|
||||
const reset = () => {
|
||||
stats.time = stats.iters = 0;
|
||||
comparison.time = comparison.iters = 0;
|
||||
results.innerHTML = status.innerText = "";
|
||||
|
||||
const table = results.parentElement as HTMLElement;
|
||||
const table = results.parentElement;
|
||||
if (compareSelect.value !== "NONE") {
|
||||
table.style.gridTemplateColumns = "repeat(4,auto)";
|
||||
header.style.display = "none";
|
||||
@@ -1,21 +1,12 @@
|
||||
import algorithms from "./algorithms";
|
||||
import algorithms from "./algorithms/index.mjs";
|
||||
|
||||
// from Xeact
|
||||
const u = (url: string = "", params: Record<string, any> = {}) => {
|
||||
const u = (url = "", params = {}) => {
|
||||
let result = new URL(url, window.location.href);
|
||||
Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v));
|
||||
return result.toString();
|
||||
};
|
||||
|
||||
const j = (id: string): any | null => {
|
||||
const elem = document.getElementById(id);
|
||||
if (elem === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(elem.textContent);
|
||||
};
|
||||
|
||||
const imageURL = (mood, cacheBuster, basePrefix) =>
|
||||
u(`${basePrefix}/.within.website/x/cmd/anubis/static/img/${mood}.webp`, {
|
||||
cacheBuster,
|
||||
@@ -23,10 +14,9 @@ const imageURL = (mood, cacheBuster, basePrefix) =>
|
||||
|
||||
// Detect available languages by loading the manifest
|
||||
const getAvailableLanguages = async () => {
|
||||
const basePrefix = j("anubis_base_prefix");
|
||||
if (basePrefix === null) {
|
||||
return;
|
||||
}
|
||||
const basePrefix = JSON.parse(
|
||||
document.getElementById("anubis_base_prefix").textContent,
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/manifest.json`);
|
||||
@@ -48,11 +38,9 @@ const getBrowserLanguage = async () =>
|
||||
|
||||
// Load translations from JSON files
|
||||
const loadTranslations = async (lang) => {
|
||||
const basePrefix = j("anubis_base_prefix");
|
||||
if (basePrefix === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const basePrefix = JSON.parse(
|
||||
document.getElementById("anubis_base_prefix").textContent,
|
||||
);
|
||||
try {
|
||||
const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`);
|
||||
return await response.json();
|
||||
@@ -66,10 +54,9 @@ const loadTranslations = async (lang) => {
|
||||
};
|
||||
|
||||
const getRedirectUrl = () => {
|
||||
const publicUrl = j("anubis_public_url");
|
||||
if (publicUrl === null) {
|
||||
return;
|
||||
}
|
||||
const publicUrl = JSON.parse(
|
||||
document.getElementById("anubis_public_url").textContent,
|
||||
);
|
||||
if (publicUrl && window.location.href.startsWith(publicUrl)) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('redir');
|
||||
@@ -104,14 +91,16 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
||||
value: navigator.cookieEnabled,
|
||||
},
|
||||
];
|
||||
|
||||
const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement;
|
||||
const image: HTMLImageElement = document.getElementById("image") as HTMLImageElement;
|
||||
const title: HTMLHeadingElement = document.getElementById("title") as HTMLHeadingElement;
|
||||
const progress: HTMLDivElement = document.getElementById("progress") as HTMLDivElement;
|
||||
|
||||
const anubisVersion = j("anubis_version");
|
||||
const basePrefix = j("anubis_base_prefix");
|
||||
const status = document.getElementById("status");
|
||||
const image = document.getElementById("image");
|
||||
const title = document.getElementById("title");
|
||||
const progress = document.getElementById("progress");
|
||||
const anubisVersion = JSON.parse(
|
||||
document.getElementById("anubis_version").textContent,
|
||||
);
|
||||
const basePrefix = JSON.parse(
|
||||
document.getElementById("anubis_base_prefix").textContent,
|
||||
);
|
||||
const details = document.querySelector("details");
|
||||
let userReadDetails = false;
|
||||
|
||||
@@ -143,7 +132,9 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
||||
}
|
||||
}
|
||||
|
||||
const { challenge, rules } = j("anubis_challenge");
|
||||
const { challenge, rules } = JSON.parse(
|
||||
document.getElementById("anubis_challenge").textContent,
|
||||
);
|
||||
|
||||
const process = algorithms[rules.algorithm];
|
||||
if (!process) {
|
||||
@@ -191,9 +182,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
||||
const probability = Math.pow(1 - likelihood, iters);
|
||||
const distance = (1 - Math.pow(probability, 2)) * 100;
|
||||
progress["aria-valuenow"] = distance;
|
||||
if (progress.firstElementChild !== null) {
|
||||
(progress.firstElementChild as HTMLElement).style.width = `${distance}%`;
|
||||
}
|
||||
progress.firstElementChild.style.width = `${distance}%`;
|
||||
|
||||
if (probability < 0.1 && !showingApology) {
|
||||
status.append(
|
||||
@@ -208,7 +197,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
||||
console.log({ hash, nonce });
|
||||
|
||||
if (userReadDetails) {
|
||||
const container: HTMLDivElement = document.getElementById("progress") as HTMLDivElement;
|
||||
const container = document.getElementById("progress");
|
||||
|
||||
// Style progress bar as a continue button
|
||||
container.style.display = "flex";
|
||||
@@ -6,7 +6,7 @@ const calculateSHA256 = (text) => {
|
||||
return hash.digest();
|
||||
};
|
||||
|
||||
function toHexString(arr: Uint8Array): string {
|
||||
function toHexString(arr) {
|
||||
return Array.from(arr)
|
||||
.map((c) => c.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
@@ -1,11 +1,10 @@
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const calculateSHA256 = async (input: string) => {
|
||||
const calculateSHA256 = async (input) => {
|
||||
const data = encoder.encode(input);
|
||||
return await crypto.subtle.digest("SHA-256", data);
|
||||
};
|
||||
|
||||
const toHexString = (byteArray: Uint8Array) => {
|
||||
const toHexString = (byteArray) => {
|
||||
return byteArray.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user