diff options
| author | Xe Iaso <me@xeiaso.net> | 2025-03-17 19:33:07 -0400 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2025-03-17 19:33:07 -0400 |
| commit | 9923878c5c8b68df7f132efd28f76ce5478a1f1a (patch) | |
| tree | c18dfc413495c09886b0d622a275f142f3e9c333 /cmd | |
| download | anubis-9923878c5c8b68df7f132efd28f76ce5478a1f1a.tar.xz anubis-9923878c5c8b68df7f132efd28f76ce5478a1f1a.zip | |
initial import from /x/ monorepo
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'cmd')
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 |
