aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2025-03-17 19:33:07 -0400
committerXe Iaso <me@xeiaso.net>2025-03-17 19:33:07 -0400
commit9923878c5c8b68df7f132efd28f76ce5478a1f1a (patch)
treec18dfc413495c09886b0d622a275f142f3e9c333 /cmd
downloadanubis-9923878c5c8b68df7f132efd28f76ce5478a1f1a.tar.xz
anubis-9923878c5c8b68df7f132efd28f76ce5478a1f1a.zip
initial import from /x/ monorepo
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/anubis/.gitignore2
-rw-r--r--cmd/anubis/CHANGELOG.md5
-rw-r--r--cmd/anubis/botPolicies.json70
-rw-r--r--cmd/anubis/decaymap.go87
-rw-r--r--cmd/anubis/decaymap_test.go31
-rw-r--r--cmd/anubis/index.templ159
-rw-r--r--cmd/anubis/index_templ.go215
-rw-r--r--cmd/anubis/internal/config/config.go99
-rw-r--r--cmd/anubis/internal/config/config_test.go168
-rw-r--r--cmd/anubis/internal/config/testdata/bad/badregexes.json14
-rw-r--r--cmd/anubis/internal/config/testdata/bad/invalid.json5
-rw-r--r--cmd/anubis/internal/config/testdata/bad/nobots.json1
-rw-r--r--cmd/anubis/internal/config/testdata/good/challengemozilla.json9
-rw-r--r--cmd/anubis/internal/config/testdata/good/everything_blocked.json10
-rw-r--r--cmd/anubis/internal/dnsbl/dnsbl.go95
-rw-r--r--cmd/anubis/internal/dnsbl/dnsbl_test.go55
-rw-r--r--cmd/anubis/internal/dnsbl/droneblresponse_string.go54
-rw-r--r--cmd/anubis/js/main.mjs71
-rw-r--r--cmd/anubis/js/proof-of-work.mjs62
-rw-r--r--cmd/anubis/js/video.mjs16
-rw-r--r--cmd/anubis/main.go574
-rw-r--r--cmd/anubis/policy.go146
-rw-r--r--cmd/anubis/policy_test.go65
-rw-r--r--cmd/anubis/static/img/happy.webpbin0 -> 60572 bytes
-rw-r--r--cmd/anubis/static/img/pensive.webpbin0 -> 49148 bytes
-rw-r--r--cmd/anubis/static/img/sad.webpbin0 -> 50802 bytes
-rw-r--r--cmd/anubis/static/js/main.mjs2
-rw-r--r--cmd/anubis/static/js/main.mjs.brbin0 -> 802 bytes
-rw-r--r--cmd/anubis/static/js/main.mjs.gzbin0 -> 985 bytes
-rw-r--r--cmd/anubis/static/js/main.mjs.map7
-rw-r--r--cmd/anubis/static/js/main.mjs.zstbin0 -> 982 bytes
-rw-r--r--cmd/anubis/static/robots.txt47
-rw-r--r--cmd/anubis/static/testdata/black.mp4bin0 -> 1667 bytes
33 files changed, 2069 insertions, 0 deletions
diff --git a/cmd/anubis/.gitignore b/cmd/anubis/.gitignore
new file mode 100644
index 0000000..061bf12
--- /dev/null
+++ b/cmd/anubis/.gitignore
@@ -0,0 +1,2 @@
+*.rpm
+anubis
diff --git a/cmd/anubis/CHANGELOG.md b/cmd/anubis/CHANGELOG.md
new file mode 100644
index 0000000..612bec1
--- /dev/null
+++ b/cmd/anubis/CHANGELOG.md
@@ -0,0 +1,5 @@
+# CHANGELOG
+
+## 2025-01-24
+
+- Added support for custom bot policy documentation, allowing administrators to change how Anubis works to meet their needs.
diff --git a/cmd/anubis/botPolicies.json b/cmd/anubis/botPolicies.json
new file mode 100644
index 0000000..6e04a11
--- /dev/null
+++ b/cmd/anubis/botPolicies.json
@@ -0,0 +1,70 @@
+{
+ "bots": [
+ {
+ "name": "amazonbot",
+ "user_agent_regex": "Amazonbot",
+ "action": "DENY"
+ },
+ {
+ "name": "googlebot",
+ "user_agent_regex": "\\+http\\:\\/\\/www\\.google\\.com/bot\\.html",
+ "action": "ALLOW"
+ },
+ {
+ "name": "bingbot",
+ "user_agent_regex": "\\+http\\:\\/\\/www\\.bing\\.com/bingbot\\.htm",
+ "action": "ALLOW"
+ },
+ {
+ "name": "qwantbot",
+ "user_agent_regex": "\\+https\\:\\/\\/help\\.qwant\\.com/bot/",
+ "action": "ALLOW"
+ },
+ {
+ "name": "us-artificial-intelligence-scraper",
+ "user_agent_regex": "\\+https\\:\\/\\/github\\.com\\/US-Artificial-Intelligence\\/scraper",
+ "action": "DENY"
+ },
+ {
+ "name": "well-known",
+ "path_regex": "^/.well-known/.*$",
+ "action": "ALLOW"
+ },
+ {
+ "name": "favicon",
+ "path_regex": "^/favicon.ico$",
+ "action": "ALLOW"
+ },
+ {
+ "name": "robots-txt",
+ "path_regex": "^/robots.txt$",
+ "action": "ALLOW"
+ },
+ {
+ "name": "rss-readers",
+ "path_regex": ".*\\.(rss|xml|atom|json)$",
+ "action": "ALLOW"
+ },
+ {
+ "name": "lightpanda",
+ "user_agent_regex": "^Lightpanda/.*$",
+ "action": "DENY"
+ },
+ {
+ "name": "headless-chrome",
+ "user_agent_regex": "HeadlessChrome",
+ "action": "DENY"
+ },
+ {
+ "name": "headless-chromium",
+ "user_agent_regex": "HeadlessChromium",
+ "action": "DENY"
+ },
+ {
+ "name": "generic-browser",
+ "user_agent_regex": "Mozilla",
+ "action": "CHALLENGE"
+ }
+ ],
+ "dnsbl": true
+}
diff --git a/cmd/anubis/decaymap.go b/cmd/anubis/decaymap.go
new file mode 100644
index 0000000..dcd2952
--- /dev/null
+++ b/cmd/anubis/decaymap.go
@@ -0,0 +1,87 @@
+package main
+
+import (
+ "sync"
+ "time"
+)
+
+func zilch[T any]() T {
+ var zero T
+ return zero
+}
+
+// DecayMap is a lazy key->value map. It's a wrapper around a map and a mutex. If values exceed their time-to-live, they are pruned at Get time.
+type DecayMap[K comparable, V any] struct {
+ data map[K]decayMapEntry[V]
+ lock sync.RWMutex
+}
+
+type decayMapEntry[V any] struct {
+ Value V
+ expiry time.Time
+}
+
+// NewDecayMap creates a new DecayMap of key type K and value type V.
+//
+// Key types must be comparable to work with maps.
+func NewDecayMap[K comparable, V any]() *DecayMap[K, V] {
+ return &DecayMap[K, V]{
+ data: make(map[K]decayMapEntry[V]),
+ }
+}
+
+// expire forcibly expires a key by setting its time-to-live one second in the past.
+func (m *DecayMap[K, V]) expire(key K) bool {
+ 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
+}
+
+// Get gets a value from the DecayMap by key.
+//
+// If a value has expired, forcibly delete it if it was not updated.
+func (m *DecayMap[K, V]) Get(key K) (V, bool) {
+ m.lock.RLock()
+ value, ok := m.data[key]
+ m.lock.RUnlock()
+
+ if !ok {
+ return zilch[V](), false
+ }
+
+ if time.Now().After(value.expiry) {
+ 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 == value.expiry {
+ delete(m.data, key)
+ }
+ m.lock.Unlock()
+
+ return zilch[V](), false
+ }
+
+ return value.Value, true
+}
+
+// Set sets a key value pair in the map.
+func (m *DecayMap[K, V]) Set(key K, value V, ttl time.Duration) {
+ m.lock.Lock()
+ defer m.lock.Unlock()
+
+ m.data[key] = decayMapEntry[V]{
+ Value: value,
+ expiry: time.Now().Add(ttl),
+ }
+}
diff --git a/cmd/anubis/decaymap_test.go b/cmd/anubis/decaymap_test.go
new file mode 100644
index 0000000..73e0626
--- /dev/null
+++ b/cmd/anubis/decaymap_test.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+ "testing"
+ "time"
+)
+
+func TestDecayMap(t *testing.T) {
+ dm := NewDecayMap[string, string]()
+
+ dm.Set("test", "hi", 5*time.Minute)
+
+ val, ok := dm.Get("test")
+ if !ok {
+ t.Error("somehow the test key was not set")
+ }
+
+ if val != "hi" {
+ t.Errorf("wanted value %q, got: %q", "hi", val)
+ }
+
+ ok = dm.expire("test")
+ if !ok {
+ t.Error("somehow could not force-expire the test key")
+ }
+
+ _, ok = dm.Get("test")
+ if ok {
+ t.Error("got value even though it was supposed to be expired")
+ }
+}
diff --git a/cmd/anubis/index.templ b/cmd/anubis/index.templ
new file mode 100644
index 0000000..e2426d5
--- /dev/null
+++ b/cmd/anubis/index.templ
@@ -0,0 +1,159 @@
+package main
+
+import (
+ "github.com/TecharoHQ/anubis"
+ "github.com/TecharoHQ/anubis/xess"
+)
+
+templ base(title string, body templ.Component) {
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>{ title }</title>
+ <link rel="stylesheet" href={ xess.URL }/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+ <style>
+ body,
+ html {
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 65ch;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .centered-div {
+ text-align: center;
+ }
+
+ .lds-roller,
+ .lds-roller div,
+ .lds-roller div:after {
+ box-sizing: border-box;
+ }
+ .lds-roller {
+ display: inline-block;
+ position: relative;
+ width: 80px;
+ height: 80px;
+ }
+ .lds-roller div {
+ animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
+ transform-origin: 40px 40px;
+ }
+ .lds-roller div:after {
+ content: " ";
+ display: block;
+ position: absolute;
+ width: 7.2px;
+ height: 7.2px;
+ border-radius: 50%;
+ background: currentColor;
+ margin: -3.6px 0 0 -3.6px;
+ }
+ .lds-roller div:nth-child(1) {
+ animation-delay: -0.036s;
+ }
+ .lds-roller div:nth-child(1):after {
+ top: 62.62742px;
+ left: 62.62742px;
+ }
+ .lds-roller div:nth-child(2) {
+ animation-delay: -0.072s;
+ }
+ .lds-roller div:nth-child(2):after {
+ top: 67.71281px;
+ left: 56px;
+ }
+ .lds-roller div:nth-child(3) {
+ animation-delay: -0.108s;
+ }
+ .lds-roller div:nth-child(3):after {
+ top: 70.90963px;
+ left: 48.28221px;
+ }
+ .lds-roller div:nth-child(4) {
+ animation-delay: -0.144s;
+ }
+ .lds-roller div:nth-child(4):after {
+ top: 72px;
+ left: 40px;
+ }
+ .lds-roller div:nth-child(5) {
+ animation-delay: -0.18s;
+ }
+ .lds-roller div:nth-child(5):after {
+ top: 70.90963px;
+ left: 31.71779px;
+ }
+ .lds-roller div:nth-child(6) {
+ animation-delay: -0.216s;
+ }
+ .lds-roller div:nth-child(6):after {
+ top: 67.71281px;
+ left: 24px;
+ }
+ .lds-roller div:nth-child(7) {
+ animation-delay: -0.252s;
+ }
+ .lds-roller div:nth-child(7):after {
+ top: 62.62742px;
+ left: 17.37258px;
+ }
+ .lds-roller div:nth-child(8) {
+ animation-delay: -0.288s;
+ }
+ .lds-roller div:nth-child(8):after {
+ top: 56px;
+ left: 12.28719px;
+ }
+ @keyframes lds-roller {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+ </style>
+ </head>
+ <body id="top">
+ <main>
+ <center>
+ <h1 id="title" class=".centered-div">{ title }</h1>
+ </center>
+ @body
+ <footer>
+ <center>
+ <p>Protected by <a href="https://xeiaso.net/blog/2025/anubis">Anubis</a> from <a href="https://within.website">Within</a>.</p>
+ </center>
+ </footer>
+ </main>
+ </body>
+ </html>
+}
+
+templ index() {
+ <div class="centered-div">
+ <img id="image" width="256" src={ "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/>
+ <img style="display:none;" width="256" src={ "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/>
+ <p id="status">Loading...</p>
+ <script async type="module" src={ "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script>
+ <div id="spinner" class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
+ <noscript>
+ <p>Sadly, you must enable JavaScript to get past this challenge. I would love to not have to do this, but god is dead and AI scrapers have destroyed the common good.</p>
+ </noscript>
+ <div id="testarea"></div>
+ </div>
+}
+
+templ errorPage(message string) {
+ <div class="centered-div">
+ <img id="image" width="256" src={ "/.within.website/x/cmd/anubis/static/img/sad.webp?cacheBuster=" + anubis.Version }/>
+ <p>{ message }.</p>
+ <button onClick="window.location.reload();">Try again</button>
+ <p><a href="/">Go home</a></p>
+ </div>
+}
diff --git a/cmd/anubis/index_templ.go b/cmd/anubis/index_templ.go
new file mode 100644
index 0000000..37a9db3
--- /dev/null
+++ b/cmd/anubis/index_templ.go
@@ -0,0 +1,215 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.833
+package main
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "github.com/TecharoHQ/anubis"
+ "github.com/TecharoHQ/anubis/xess"
+)
+
+func base(title string, body templ.Component) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html><head><title>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var2 string
+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 12, Col: 17}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var3 string
+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(xess.URL)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 13, Col: 41}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><style>\n body,\n html {\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n width: 65ch;\n margin-left: auto;\n margin-right: auto;\n }\n\n .centered-div {\n text-align: center;\n }\n\n .lds-roller,\n .lds-roller div,\n .lds-roller div:after {\n box-sizing: border-box;\n }\n .lds-roller {\n display: inline-block;\n position: relative;\n width: 80px;\n height: 80px;\n }\n .lds-roller div {\n animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;\n transform-origin: 40px 40px;\n }\n .lds-roller div:after {\n content: \" \";\n display: block;\n position: absolute;\n width: 7.2px;\n height: 7.2px;\n border-radius: 50%;\n background: currentColor;\n margin: -3.6px 0 0 -3.6px;\n }\n .lds-roller div:nth-child(1) {\n animation-delay: -0.036s;\n }\n .lds-roller div:nth-child(1):after {\n top: 62.62742px;\n left: 62.62742px;\n }\n .lds-roller div:nth-child(2) {\n animation-delay: -0.072s;\n }\n .lds-roller div:nth-child(2):after {\n top: 67.71281px;\n left: 56px;\n }\n .lds-roller div:nth-child(3) {\n animation-delay: -0.108s;\n }\n .lds-roller div:nth-child(3):after {\n top: 70.90963px;\n left: 48.28221px;\n }\n .lds-roller div:nth-child(4) {\n animation-delay: -0.144s;\n }\n .lds-roller div:nth-child(4):after {\n top: 72px;\n left: 40px;\n }\n .lds-roller div:nth-child(5) {\n animation-delay: -0.18s;\n }\n .lds-roller div:nth-child(5):after {\n top: 70.90963px;\n left: 31.71779px;\n }\n .lds-roller div:nth-child(6) {\n animation-delay: -0.216s;\n }\n .lds-roller div:nth-child(6):after {\n top: 67.71281px;\n left: 24px;\n }\n .lds-roller div:nth-child(7) {\n animation-delay: -0.252s;\n }\n .lds-roller div:nth-child(7):after {\n top: 62.62742px;\n left: 17.37258px;\n }\n .lds-roller div:nth-child(8) {\n animation-delay: -0.288s;\n }\n .lds-roller div:nth-child(8):after {\n top: 56px;\n left: 12.28719px;\n }\n @keyframes lds-roller {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n }\n </style></head><body id=\"top\"><main><center><h1 id=\"title\" class=\".centered-div\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 125, Col: 49}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</h1></center>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = body.Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<footer><center><p>Protected by <a href=\"https://xeiaso.net/blog/2025/anubis\">Anubis</a> from <a href=\"https://within.website\">Within</a>.</p></center></footer></main></body></html>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func index() templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var5 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var5 == nil {
+ templ_7745c5c3_Var5 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"centered-div\"><img id=\"image\" width=\"256\" src=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 140, Col: 121}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"> <img style=\"display:none;\" width=\"256\" src=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 string
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 141, Col: 130}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"><p id=\"status\">Loading...</p><script async type=\"module\" src=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 143, Col: 116}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"></script><div id=\"spinner\" class=\"lds-roller\"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div><noscript><p>Sadly, you must enable JavaScript to get past this challenge. I would love to not have to do this, but god is dead and AI scrapers have destroyed the common good.</p></noscript><div id=\"testarea\"></div></div>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func errorPage(message string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var9 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var9 == nil {
+ templ_7745c5c3_Var9 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"centered-div\"><img id=\"image\" width=\"256\" src=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/sad.webp?cacheBuster=" + anubis.Version)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 154, Col: 117}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"><p>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var11 string
+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(message)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 155, Col: 14}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, ".</p><button onClick=\"window.location.reload();\">Try again</button><p><a href=\"/\">Go home</a></p></div>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/cmd/anubis/internal/config/config.go b/cmd/anubis/internal/config/config.go
new file mode 100644
index 0000000..ad338ef
--- /dev/null
+++ b/cmd/anubis/internal/config/config.go
@@ -0,0 +1,99 @@
+package config
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+)
+
+type Rule string
+
+const (
+ RuleUnknown = ""
+ RuleAllow = "ALLOW"
+ RuleDeny = "DENY"
+ RuleChallenge = "CHALLENGE"
+)
+
+type Bot struct {
+ Name string `json:"name"`
+ UserAgentRegex *string `json:"user_agent_regex"`
+ PathRegex *string `json:"path_regex"`
+ Action Rule `json:"action"`
+}
+
+var (
+ ErrNoBotRulesDefined = errors.New("config: must define at least one (1) bot rule")
+ ErrBotMustHaveName = errors.New("config.Bot: must set name")
+ ErrBotMustHaveUserAgentOrPath = errors.New("config.Bot: must set either user_agent_regex, path_regex")
+ ErrBotMustHaveUserAgentOrPathNotBoth = errors.New("config.Bot: must set either user_agent_regex, path_regex, and not both")
+ ErrUnknownAction = errors.New("config.Bot: unknown action")
+ ErrInvalidUserAgentRegex = errors.New("config.Bot: invalid user agent regex")
+ ErrInvalidPathRegex = errors.New("config.Bot: invalid path regex")
+)
+
+func (b Bot) Valid() error {
+ var errs []error
+
+ if b.Name == "" {
+ errs = append(errs, ErrBotMustHaveName)
+ }
+
+ if b.UserAgentRegex == nil && b.PathRegex == nil {
+ errs = append(errs, ErrBotMustHaveUserAgentOrPath)
+ }
+
+ if b.UserAgentRegex != nil && b.PathRegex != nil {
+ errs = append(errs, ErrBotMustHaveUserAgentOrPathNotBoth)
+ }
+
+ if b.UserAgentRegex != nil {
+ if _, err := regexp.Compile(*b.UserAgentRegex); err != nil {
+ errs = append(errs, ErrInvalidUserAgentRegex, err)
+ }
+ }
+
+ if b.PathRegex != nil {
+ if _, err := regexp.Compile(*b.PathRegex); err != nil {
+ errs = append(errs, ErrInvalidPathRegex, err)
+ }
+ }
+
+ switch b.Action {
+ case RuleAllow, RuleChallenge, RuleDeny:
+ // okay
+ default:
+ errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownAction, b.Action))
+ }
+
+ if len(errs) != 0 {
+ return fmt.Errorf("config: bot entry for %q is not valid:\n%w", b.Name, errors.Join(errs...))
+ }
+
+ return nil
+}
+
+type Config struct {
+ Bots []Bot `json:"bots"`
+ DNSBL bool `json:"dnsbl"`
+}
+
+func (c Config) Valid() error {
+ var errs []error
+
+ if len(c.Bots) == 0 {
+ errs = append(errs, ErrNoBotRulesDefined)
+ }
+
+ for _, b := range c.Bots {
+ if err := b.Valid(); err != nil {
+ errs = append(errs, err)
+ }
+ }
+
+ if len(errs) != 0 {
+ return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...))
+ }
+
+ return nil
+}
diff --git a/cmd/anubis/internal/config/config_test.go b/cmd/anubis/internal/config/config_test.go
new file mode 10