From d3e509517c12ddf82adf8ab29a36da9da9bd2bd2 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Fri, 21 Mar 2025 13:48:00 -0400 Subject: [PATCH] cmd/anubis: configurable difficulty per-bot rule (#53) Closes #30 Introduces the "challenge" field in bot rule definitions: ```json { "name": "generic-bot-catchall", "user_agent_regex": "(?i:bot|crawler)", "action": "CHALLENGE", "challenge": { "difficulty": 16, "report_as": 4, "algorithm": "slow" } } ``` This makes Anubis return a challenge page for every user agent with "bot" or "crawler" in it (case-insensitively) with difficulty 16 using the old "slow" algorithm but reporting in the client as difficulty 4. This is useful when you want to make certain clients in particular suffer. Additional validation and testing logic has been added to make sure that users do not define "impossible" challenge settings. If no algorithm is specified, Anubis defaults to the "fast" algorithm. Signed-off-by: Xe Iaso --- cmd/anubis/botPolicies.json | 12 +++- cmd/anubis/internal/config/config.go | 68 +++++++++++++++++++--- cmd/anubis/internal/config/config_test.go | 42 +++++++++++++ cmd/anubis/js/main.mjs | 28 +++++++-- cmd/anubis/js/proof-of-work-slow.mjs | 63 ++++++++++++++++++++ cmd/anubis/js/proof-of-work.mjs | 5 +- cmd/anubis/main.go | 45 ++++++++------ cmd/anubis/policy.go | 26 ++++++++- cmd/anubis/policy_test.go | 6 +- cmd/anubis/static/js/main.mjs | 2 +- cmd/anubis/static/js/main.mjs.br | Bin 992 -> 1216 bytes cmd/anubis/static/js/main.mjs.gz | Bin 1194 -> 1451 bytes cmd/anubis/static/js/main.mjs.map | 8 +-- cmd/anubis/static/js/main.mjs.zst | Bin 1190 -> 1430 bytes docs/docs/CHANGELOG.md | 17 ++++++ docs/docs/admin/algorithm-selection.mdx | 12 ++++ docs/docs/admin/policies.md | 23 ++++++++ 17 files changed, 311 insertions(+), 46 deletions(-) create mode 100644 cmd/anubis/js/proof-of-work-slow.mjs create mode 100644 docs/docs/admin/algorithm-selection.mdx diff --git a/cmd/anubis/botPolicies.json b/cmd/anubis/botPolicies.json index 4a6321b..b602ef7 100644 --- a/cmd/anubis/botPolicies.json +++ b/cmd/anubis/botPolicies.json @@ -65,6 +65,16 @@ "user_agent_regex": "HeadlessChromium", "action": "DENY" }, + { + "name": "generic-bot-catchall", + "user_agent_regex": "(?i:bot|crawler)", + "action": "CHALLENGE", + "challenge": { + "difficulty": 16, + "report_as": 4, + "algorithm": "slow" + } + }, { "name": "generic-browser", "user_agent_regex": "Mozilla", @@ -72,4 +82,4 @@ } ], "dnsbl": true -} +} \ No newline at end of file diff --git a/cmd/anubis/internal/config/config.go b/cmd/anubis/internal/config/config.go index ad338ef..efd8496 100644 --- a/cmd/anubis/internal/config/config.go +++ b/cmd/anubis/internal/config/config.go @@ -9,17 +9,26 @@ import ( type Rule string const ( - RuleUnknown = "" - RuleAllow = "ALLOW" - RuleDeny = "DENY" - RuleChallenge = "CHALLENGE" + RuleUnknown Rule = "" + RuleAllow Rule = "ALLOW" + RuleDeny Rule = "DENY" + RuleChallenge Rule = "CHALLENGE" +) + +type Algorithm string + +const ( + AlgorithmUnknown Algorithm = "" + AlgorithmFast Algorithm = "fast" + AlgorithmSlow Algorithm = "slow" ) type Bot struct { - Name string `json:"name"` - UserAgentRegex *string `json:"user_agent_regex"` - PathRegex *string `json:"path_regex"` - Action Rule `json:"action"` + Name string `json:"name"` + UserAgentRegex *string `json:"user_agent_regex"` + PathRegex *string `json:"path_regex"` + Action Rule `json:"action"` + Challenge *ChallengeRules `json:"challenge,omitempty"` } var ( @@ -66,6 +75,12 @@ func (b Bot) Valid() error { errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownAction, b.Action)) } + if b.Action == RuleChallenge && b.Challenge != nil { + if err := b.Challenge.Valid(); err != nil { + errs = append(errs, err) + } + } + if len(errs) != 0 { return fmt.Errorf("config: bot entry for %q is not valid:\n%w", b.Name, errors.Join(errs...)) } @@ -73,6 +88,43 @@ func (b Bot) Valid() error { return nil } +type ChallengeRules struct { + Difficulty int `json:"difficulty"` + ReportAs int `json:"report_as"` + Algorithm Algorithm `json:"algorithm"` +} + +var ( + ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid") + ErrChallengeDifficultyTooLow = errors.New("config.Bot.ChallengeRules: difficulty is too low (must be >= 1)") + ErrChallengeDifficultyTooHigh = errors.New("config.Bot.ChallengeRules: difficulty is too high (must be <= 64)") +) + +func (cr ChallengeRules) Valid() error { + var errs []error + + if cr.Difficulty < 1 { + errs = append(errs, fmt.Errorf("%w, got: %d", ErrChallengeDifficultyTooLow, cr.Difficulty)) + } + + if cr.Difficulty > 64 { + errs = append(errs, fmt.Errorf("%w, got: %d", ErrChallengeDifficultyTooHigh, cr.Difficulty)) + } + + switch cr.Algorithm { + case AlgorithmFast, AlgorithmSlow, AlgorithmUnknown: + // do nothing, it's all good + default: + errs = append(errs, fmt.Errorf("%w: %q", ErrChallengeRuleHasWrongAlgorithm, cr.Algorithm)) + } + + if len(errs) != 0 { + return fmt.Errorf("config: challenge rules entry is not valid:\n%w", errors.Join(errs...)) + } + + return nil +} + type Config struct { Bots []Bot `json:"bots"` DNSBL bool `json:"dnsbl"` diff --git a/cmd/anubis/internal/config/config_test.go b/cmd/anubis/internal/config/config_test.go index f362a76..0903fbb 100644 --- a/cmd/anubis/internal/config/config_test.go +++ b/cmd/anubis/internal/config/config_test.go @@ -87,6 +87,48 @@ func TestBotValid(t *testing.T) { }, err: ErrInvalidPathRegex, }, + { + name: "challenge difficulty too low", + bot: Bot{ + Name: "mozilla-ua", + Action: RuleChallenge, + PathRegex: p("Mozilla"), + Challenge: &ChallengeRules{ + Difficulty: 0, + ReportAs: 4, + Algorithm: "fast", + }, + }, + err: ErrChallengeDifficultyTooLow, + }, + { + name: "challenge difficulty too high", + bot: Bot{ + Name: "mozilla-ua", + Action: RuleChallenge, + PathRegex: p("Mozilla"), + Challenge: &ChallengeRules{ + Difficulty: 420, + ReportAs: 4, + Algorithm: "fast", + }, + }, + err: ErrChallengeDifficultyTooHigh, + }, + { + name: "challenge wrong algorithm", + bot: Bot{ + Name: "mozilla-ua", + Action: RuleChallenge, + PathRegex: p("Mozilla"), + Challenge: &ChallengeRules{ + Difficulty: 420, + ReportAs: 4, + Algorithm: "high quality rips", + }, + }, + err: ErrChallengeRuleHasWrongAlgorithm, + }, } for _, cs := range tests { diff --git a/cmd/anubis/js/main.mjs b/cmd/anubis/js/main.mjs index fc85a44..297f16f 100644 --- a/cmd/anubis/js/main.mjs +++ b/cmd/anubis/js/main.mjs @@ -1,5 +1,11 @@ -import { process } from './proof-of-work.mjs'; -import { testVideo } from './video.mjs'; +import processFast from "./proof-of-work.mjs"; +import processSlow from "./proof-of-work-slow.mjs"; +import { testVideo } from "./video.mjs"; + +const algorithms = { + "fast": processFast, + "slow": processSlow, +} // from Xeact const u = (url = "", params = {}) => { @@ -37,7 +43,7 @@ const imageURL = (mood, cacheBuster) => status.innerHTML = 'Calculating...'; - const { challenge, difficulty } = await fetch("/.within.website/x/cmd/anubis/api/make-challenge", { method: "POST" }) + const { challenge, rules } = await fetch("/.within.website/x/cmd/anubis/api/make-challenge", { method: "POST" }) .then(r => { if (!r.ok) { throw new Error("Failed to fetch config"); @@ -47,16 +53,26 @@ const imageURL = (mood, cacheBuster) => .catch(err => { title.innerHTML = "Oh no!"; status.innerHTML = `Failed to fetch config: ${err.message}`; - image.src = imageURL("sad"); + image.src = imageURL("sad", anubisVersion); spinner.innerHTML = ""; spinner.style.display = "none"; throw err; }); - status.innerHTML = `Calculating...
Difficulty: ${difficulty}`; + const process = algorithms[rules.algorithm]; + if (!process) { + title.innerHTML = "Oh no!"; + status.innerHTML = `Failed to resolve check algorithm. You may want to reload the page.`; + image.src = imageURL("sad", anubisVersion); + spinner.innerHTML = ""; + spinner.style.display = "none"; + return; + } + + status.innerHTML = `Calculating...
Difficulty: ${rules.report_as}`; const t0 = Date.now(); - const { hash, nonce } = await process(challenge, difficulty); + const { hash, nonce } = await process(challenge, rules.difficulty); const t1 = Date.now(); console.log({ hash, nonce }); diff --git a/cmd/anubis/js/proof-of-work-slow.mjs b/cmd/anubis/js/proof-of-work-slow.mjs new file mode 100644 index 0000000..e30dc21 --- /dev/null +++ b/cmd/anubis/js/proof-of-work-slow.mjs @@ -0,0 +1,63 @@ +// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm + +export default function process(data, difficulty = 5, _threads = 1) { + console.debug("slow algo"); + return new Promise((resolve, reject) => { + let webWorkerURL = URL.createObjectURL(new Blob([ + '(', processTask(), ')()' + ], { type: 'application/javascript' })); + + let worker = new Worker(webWorkerURL); + + worker.onmessage = (event) => { + worker.terminate(); + resolve(event.data); + }; + + worker.onerror = (event) => { + worker.terminate(); + reject(); + }; + + worker.postMessage({ + data, + difficulty + }); + + URL.revokeObjectURL(webWorkerURL); + }); +} + +function processTask() { + return function () { + const sha256 = (text) => { + const encoded = new TextEncoder().encode(text); + return crypto.subtle.digest("SHA-256", encoded.buffer) + .then((result) => + Array.from(new Uint8Array(result)) + .map((c) => c.toString(16).padStart(2, "0")) + .join(""), + ); + }; + + addEventListener('message', async (event) => { + let data = event.data.data; + let difficulty = event.data.difficulty; + + let hash; + let nonce = 0; + do { + hash = await sha256(data + nonce++); + } while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')); + + nonce -= 1; // last nonce was post-incremented + + postMessage({ + hash, + data, + difficulty, + nonce, + }); + }); + }.toString(); +} \ No newline at end of file diff --git a/cmd/anubis/js/proof-of-work.mjs b/cmd/anubis/js/proof-of-work.mjs index 3125ae6..b4f9c53 100644 --- a/cmd/anubis/js/proof-of-work.mjs +++ b/cmd/anubis/js/proof-of-work.mjs @@ -1,6 +1,5 @@ -// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm - -export function process(data, difficulty = 5, threads = (navigator.hardwareConcurrency || 1)) { +export default function process(data, difficulty = 5, threads = (navigator.hardwareConcurrency || 1)) { + console.debug("fast algo"); return new Promise((resolve, reject) => { let webWorkerURL = URL.createObjectURL(new Blob([ '(', processTask(), ')()' diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index f2f7255..b92b591 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -44,7 +44,7 @@ import ( var ( bind = flag.String("bind", ":8923", "network address to bind HTTP to") bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp") - challengeDifficulty = flag.Int("difficulty", 4, "difficulty of the challenge") + challengeDifficulty = flag.Int("difficulty", defaultDifficulty, "difficulty of the challenge") metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to") metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to") socketMode = flag.String("socket-mode", "0770", "socket mode (permissions) for unix domain sockets.") @@ -85,8 +85,9 @@ var ( ) const ( - cookieName = "within.website-x-cmd-anubis-auth" - staticPath = "/.within.website/x/cmd/anubis/" + cookieName = "within.website-x-cmd-anubis-auth" + staticPath = "/.within.website/x/cmd/anubis/" + defaultDifficulty = 4 ) //go:generate go tool github.com/a-h/templ/cmd/templ generate @@ -261,7 +262,7 @@ func sha256sum(text string) (string, error) { return hex.EncodeToString(hash.Sum(nil)), nil } -func (s *Server) challengeFor(r *http.Request) string { +func (s *Server) challengeFor(r *http.Request, difficulty int) string { fp := sha256.Sum256(s.priv.Seed()) data := fmt.Sprintf( @@ -271,7 +272,7 @@ func (s *Server) challengeFor(r *http.Request) string { r.UserAgent(), time.Now().UTC().Round(24*7*time.Hour).Format(time.RFC3339), fp, - *challengeDifficulty, + difficulty, ) result, _ := sha256sum(data) return result @@ -324,7 +325,7 @@ func New(target, policyFname string) (*Server, error) { defer fin.Close() - policy, err := parseConfig(fin, policyFname) + policy, err := parseConfig(fin, policyFname, *challengeDifficulty) if err != nil { return nil, err // parseConfig sets a fancy error for us } @@ -485,7 +486,9 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request) { return } - if claims["challenge"] != s.challengeFor(r) { + challenge := s.challengeFor(r, rule.Challenge.Difficulty) + + if claims["challenge"] != challenge { lg.Debug("invalid challenge", "path", r.URL.Path) clearCookie(w) s.renderIndex(w, r) @@ -498,7 +501,7 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request) { nonce = int(v) } - calcString := fmt.Sprintf("%s%d", s.challengeFor(r), nonce) + calcString := fmt.Sprintf("%s%d", challenge, nonce) calculated, err := sha256sum(calcString) if err != nil { lg.Error("failed to calculate sha256sum", "path", r.URL.Path, "err", err) @@ -527,24 +530,32 @@ func (s *Server) renderIndex(w http.ResponseWriter, r *http.Request) { } func (s *Server) makeChallenge(w http.ResponseWriter, r *http.Request) { - challenge := s.challengeFor(r) - difficulty := *challengeDifficulty + cr, rule := s.check(r) + challenge := s.challengeFor(r, rule.Challenge.Difficulty) lg := slog.With("user_agent", r.UserAgent(), "accept_language", r.Header.Get("Accept-Language"), "priority", r.Header.Get("Priority"), "x-forwarded-for", r.Header.Get("X-Forwarded-For"), "x-real-ip", r.Header.Get("X-Real-Ip")) json.NewEncoder(w).Encode(struct { - Challenge string `json:"challenge"` - Difficulty int `json:"difficulty"` + Challenge string `json:"challenge"` + Rules *config.ChallengeRules `json:"rules"` }{ - Challenge: challenge, - Difficulty: difficulty, + Challenge: challenge, + Rules: rule.Challenge, }) - lg.Debug("made challenge", "challenge", challenge, "difficulty", difficulty) + lg.Debug("made challenge", "challenge", challenge, "rules", rule.Challenge, "cr", cr) challengesIssued.Inc() } func (s *Server) passChallenge(w http.ResponseWriter, r *http.Request) { - lg := slog.With("user_agent", r.UserAgent(), "accept_language", r.Header.Get("Accept-Language"), "priority", r.Header.Get("Priority"), "x-forwarded-for", r.Header.Get("X-Forwarded-For"), "x-real-ip", r.Header.Get("X-Real-Ip")) + cr, rule := s.check(r) + lg := slog.With( + "user_agent", r.UserAgent(), + "accept_language", r.Header.Get("Accept-Language"), + "priority", r.Header.Get("Priority"), + "x-forwarded-for", r.Header.Get("X-Forwarded-For"), + "x-real-ip", r.Header.Get("X-Real-Ip"), + "cr", cr, + ) nonceStr := r.FormValue("nonce") if nonceStr == "" { @@ -576,7 +587,7 @@ func (s *Server) passChallenge(w http.ResponseWriter, r *http.Request) { response := r.FormValue("response") redir := r.FormValue("redir") - challenge := s.challengeFor(r) + challenge := s.challengeFor(r, rule.Challenge.Difficulty) nonce, err := strconv.Atoi(nonceStr) if err != nil { diff --git a/cmd/anubis/policy.go b/cmd/anubis/policy.go index f636349..a637f09 100644 --- a/cmd/anubis/policy.go +++ b/cmd/anubis/policy.go @@ -32,7 +32,8 @@ type Bot struct { Name string UserAgent *regexp.Regexp Path *regexp.Regexp - Action config.Rule `json:"action"` + Action config.Rule + Challenge *config.ChallengeRules } func (b Bot) Hash() (string, error) { @@ -48,7 +49,7 @@ func (b Bot) Hash() (string, error) { return sha256sum(fmt.Sprintf("%s::%s::%s", b.Name, pathRex, userAgentRex)) } -func parseConfig(fin io.Reader, fname string) (*ParsedConfig, error) { +func parseConfig(fin io.Reader, fname string, defaultDifficulty int) (*ParsedConfig, error) { var c config.Config if err := json.NewDecoder(fin).Decode(&c); err != nil { return nil, fmt.Errorf("can't parse policy config JSON %s: %w", fname, err) @@ -96,6 +97,19 @@ func parseConfig(fin io.Reader, fname string) (*ParsedConfig, error) { } } + if b.Challenge == nil { + parsedBot.Challenge = &config.ChallengeRules{ + Difficulty: defaultDifficulty, + ReportAs: defaultDifficulty, + Algorithm: config.AlgorithmFast, + } + } else { + parsedBot.Challenge = b.Challenge + if parsedBot.Challenge.Algorithm == config.AlgorithmUnknown { + parsedBot.Challenge.Algorithm = config.AlgorithmFast + } + } + result.Bots = append(result.Bots, parsedBot) } @@ -142,5 +156,11 @@ func (s *Server) check(r *http.Request) (CheckResult, *Bot) { } } - return cr("default/allow", config.RuleAllow), nil + return cr("default/allow", config.RuleAllow), &Bot{ + Challenge: &config.ChallengeRules{ + Difficulty: defaultDifficulty, + ReportAs: defaultDifficulty, + Algorithm: config.AlgorithmFast, + }, + } } diff --git a/cmd/anubis/policy_test.go b/cmd/anubis/policy_test.go index 9a5699e..cf8ef2e 100644 --- a/cmd/anubis/policy_test.go +++ b/cmd/anubis/policy_test.go @@ -13,7 +13,7 @@ func TestDefaultPolicyMustParse(t *testing.T) { } defer fin.Close() - if _, err := parseConfig(fin, "botPolicies.json"); err != nil { + if _, err := parseConfig(fin, "botPolicies.json", defaultDifficulty); err != nil { t.Fatalf("can't parse config: %v", err) } } @@ -33,7 +33,7 @@ func TestGoodConfigs(t *testing.T) { } defer fin.Close() - if _, err := parseConfig(fin, fin.Name()); err != nil { + if _, err := parseConfig(fin, fin.Name(), defaultDifficulty); err != nil { t.Fatal(err) } }) @@ -55,7 +55,7 @@ func TestBadConfigs(t *testing.T) { } defer fin.Close() - if _, err := parseConfig(fin, fin.Name()); err == nil { + if _, err := parseConfig(fin, fin.Name(), defaultDifficulty); err == nil { t.Fatal(err) } else { t.Log(err) diff --git a/cmd/anubis/static/js/main.mjs b/cmd/anubis/static/js/main.mjs index d1e1b86..a934289 100644 --- a/cmd/anubis/static/js/main.mjs +++ b/cmd/anubis/static/js/main.mjs @@ -1,2 +1,2 @@ -(()=>{function m(n,s=5,e=(navigator.hardwareConcurrency||1)){return new Promise((t,l)=>{let a=URL.createObjectURL(new Blob(["(",g(),")()"],{type:"application/javascript"})),r=[];for(let d=0;d{r.forEach(u=>u.terminate()),i.terminate(),t(c.data)},i.onerror=c=>{i.terminate(),l()},i.postMessage({data:n,difficulty:s,nonce:d,threads:e}),r.push(i)}URL.revokeObjectURL(a)})}function g(){return function(){let n=e=>{let t=new TextEncoder().encode(e);return crypto.subtle.digest("SHA-256",t.buffer)};function s(e){return Array.from(e).map(t=>t.toString(16).padStart(2,"0")).join("")}addEventListener("message",async e=>{let t=e.data.data,l=e.data.difficulty,a,r=e.data.nonce,d=e.data.threads;for(;;){let i=await n(t+r),c=new Uint8Array(i),u=!0;for(let o=0;o>(p===0?4:0)&15)!==0){u=!1;break}}if(u){a=s(c),console.log(a);break}r+=d}postMessage({hash:a,data:t,difficulty:l,nonce:r})})}.toString()}var w=(n="",s={})=>{let e=new URL(n,window.location.href);return Object.entries(s).forEach(t=>{let[l,a]=t;e.searchParams.set(l,a)}),e.toString()},h=(n,s)=>w(`/.within.website/x/cmd/anubis/static/img/${n}.webp`,{cacheBuster:s});(async()=>{let n=document.getElementById("status"),s=document.getElementById("image"),e=document.getElementById("title"),t=document.getElementById("spinner"),l=JSON.parse(document.getElementById("anubis_version").textContent);n.innerHTML="Calculating...";let{challenge:a,difficulty:r}=await fetch("/.within.website/x/cmd/anubis/api/make-challenge",{method:"POST"}).then(o=>{if(!o.ok)throw new Error("Failed to fetch config");return o.json()}).catch(o=>{throw e.innerHTML="Oh no!",n.innerHTML=`Failed to fetch config: ${o.message}`,s.src=h("sad"),t.innerHTML="",t.style.display="none",o});n.innerHTML=`Calculating...
Difficulty: ${r}`;let d=Date.now(),{hash:i,nonce:c}=await m(a,r),u=Date.now();console.log({hash:i,nonce:c}),e.innerHTML="Success!",n.innerHTML=`Done! Took ${u-d}ms, ${c} iterations`,s.src=h("happy",l),t.innerHTML="",t.style.display="none",setTimeout(()=>{let o=window.location.href;window.location.href=w("/.within.website/x/cmd/anubis/api/pass-challenge",{response:i,nonce:c,redir:o,elapsedTime:u-d})},250)})();})(); +(()=>{function p(r,n=5,t=navigator.hardwareConcurrency||1){return console.debug("fast algo"),new Promise((e,o)=>{let s=URL.createObjectURL(new Blob(["(",y(),")()"],{type:"application/javascript"})),a=[];for(let i=0;i{a.forEach(u=>u.terminate()),c.terminate(),e(d.data)},c.onerror=d=>{c.terminate(),o()},c.postMessage({data:r,difficulty:n,nonce:i,threads:t}),a.push(c)}URL.revokeObjectURL(s)})}function y(){return function(){let r=t=>{let e=new TextEncoder().encode(t);return crypto.subtle.digest("SHA-256",e.buffer)};function n(t){return Array.from(t).map(e=>e.toString(16).padStart(2,"0")).join("")}addEventListener("message",async t=>{let e=t.data.data,o=t.data.difficulty,s,a=t.data.nonce,i=t.data.threads;for(;;){let c=await r(e+a),d=new Uint8Array(c),u=!0;for(let m=0;m>(g===0?4:0)&15)!==0){u=!1;break}}if(u){s=n(d),console.log(s);break}a+=i}postMessage({hash:s,data:e,difficulty:o,nonce:a})})}.toString()}function f(r,n=5,t=1){return console.debug("slow algo"),new Promise((e,o)=>{let s=URL.createObjectURL(new Blob(["(",b(),")()"],{type:"application/javascript"})),a=new Worker(s);a.onmessage=i=>{a.terminate(),e(i.data)},a.onerror=i=>{a.terminate(),o()},a.postMessage({data:r,difficulty:n}),URL.revokeObjectURL(s)})}function b(){return function(){let r=n=>{let t=new TextEncoder().encode(n);return crypto.subtle.digest("SHA-256",t.buffer).then(e=>Array.from(new Uint8Array(e)).map(o=>o.toString(16).padStart(2,"0")).join(""))};addEventListener("message",async n=>{let t=n.data.data,e=n.data.difficulty,o,s=0;do o=await r(t+s++);while(o.substring(0,e)!==Array(e+1).join("0"));s-=1,postMessage({hash:o,data:t,difficulty:e,nonce:s})})}.toString()}var L={fast:p,slow:f},w=(r="",n={})=>{let t=new URL(r,window.location.href);return Object.entries(n).forEach(e=>{let[o,s]=e;t.searchParams.set(o,s)}),t.toString()},h=(r,n)=>w(`/.within.website/x/cmd/anubis/static/img/${r}.webp`,{cacheBuster:n});(async()=>{let r=document.getElementById("status"),n=document.getElementById("image"),t=document.getElementById("title"),e=document.getElementById("spinner"),o=JSON.parse(document.getElementById("anubis_version").textContent);r.innerHTML="Calculating...";let{challenge:s,rules:a}=await fetch("/.within.website/x/cmd/anubis/api/make-challenge",{method:"POST"}).then(l=>{if(!l.ok)throw new Error("Failed to fetch config");return l.json()}).catch(l=>{throw t.innerHTML="Oh no!",r.innerHTML=`Failed to fetch config: ${l.message}`,n.src=h("sad",o),e.innerHTML="",e.style.display="none",l}),i=L[a.algorithm];if(!i){t.innerHTML="Oh no!",r.innerHTML="Failed to resolve check algorithm. You may want to reload the page.",n.src=h("sad",o),e.innerHTML="",e.style.display="none";return}r.innerHTML=`Calculating...
Difficulty: ${a.report_as}`;let c=Date.now(),{hash:d,nonce:u}=await i(s,a.difficulty),m=Date.now();console.log({hash:d,nonce:u}),t.innerHTML="Success!",r.innerHTML=`Done! Took ${m-c}ms, ${u} iterations`,n.src=h("happy",o),e.innerHTML="",e.style.display="none",setTimeout(()=>{let l=window.location.href;window.location.href=w("/.within.website/x/cmd/anubis/api/pass-challenge",{response:d,nonce:u,redir:l,elapsedTime:m-c})},250)})();})(); //# sourceMappingURL=main.mjs.map diff --git a/cmd/anubis/static/js/main.mjs.br b/cmd/anubis/static/js/main.mjs.br index a7b35e9e4029a4a8d08823b08bae3630b1ad9aa1..6bfd987ad3d4cb0c8eb13812030805ae18226232 100644 GIT binary patch literal 1216 zcmV;x1V8)1_+O3yvcSu?SO$iGYdL>?g4tyU1Ko>;L*bWQdz?xs-3lP070&4j?0hxV&v5v2O z4mu6X+=_18y`&84cb=$iZ7UMJ99AodSWN9j9`Zf9%n$hPUdX6B+k^0-xD=%AVjFdD z8=>q4>!jui(WxCLDureFxU9$%Q8a3Ez=#?D_$xu-e=kxiDUl6F?8eCjP$VOp#Esz$ z!Kwj$BFM}lHScwQ8+jL`Ay6X?Je{+ay36big6WpFESo>mRqIIi+d=OJNSNrfov|y-1XdSC67iIk z=tI~RM&T&HGfiM6;$xFx=u*w@y5&exYH2lYBuO+lqg6XQ);YV+f+TH@F7@8Q>=JLt zaK0HJO1ylH#G+9_c~b>fg#BSLk32y9b=JcEfk27prU{cEOl|Wl&1PsUVx1aBH8v-a z0NVEWY|$!c;2BI>;{~y~E?8x~*iJBTvoFDjr*Kb}LH&DG=$Rq1Qsj9oNGc%ns*%P+ zFS!Um&`Wek!sB2eNnryo1Ep&gll6FV*#}H3NrpfP3sP<*qX!nje#nQO<_TCZiqs9`gm3a(| z_Dw|+*+mtzg(ELwCAoe8h3it=yGol1tQD27Xwmsxeu&{7 zSP3017*LC?uwt@3X+W`)&^qy@3=3*S8Ve-lOv_N-@O_gHQ$?W9;l+2@pRiU-a9Oz{ zujl7}q@j{+6a^%?+d+|>92RKO4=QR4lH)JS>O)@%2GspI92;jGU9^eQuinxGeM%1n zoqb(6N4Cd-{Zuc8YCA>l^n$DrxCEzGxXlCrXHlzF>*TFXkCIUaz;}-Ae0T<=-bZeF z8fD<7V-6Dae_N5deACSEJz^Qlk?=@&z&Rq3VVePAy8ob_A9QPV=@LD)9m=G$#Qte& zy1_}7iF)KG2fQ7|sx5bbvzp*_^Rpyn?`=w+2^S6@&r+1e#r>9~wSAJtE;0c44lK03 eHOGQ*O8@!we%ocb2i>y2^@XRGS@$mue!@E`+e~Nx literal 992 zcmV<610Vds*hT-Pz11L>HA%V^R zy+8h=ERc>TzC^MF@7;fnasO+KNm<7yk4=F^iMB@P?4HU8n>7I-?@v(w*n+3TA4oV3 zmY={$@C9SK*6-zEc8@avnOuC&ILR~1|9*ef*&(@yUuht~5070~!?^-Q8$ePBs4i)m z|C)6wk3t6PR6(RT=Kzr|L`dMtmY1g86Vomhn4+cU8|u921&LtM^g8%bmUF}VQ`Di6 zTjUBO?~l5F6n7Rlt?R$ZP=-ch$BgASsseE=5|CC0FbMIK&nz&7JX3pO$w-~Zlh{|c zByqK=mVy)+&(mW;)50kC;jU=0U?GWf1K1YT1S+-Pvz)cL{HWtD%bBNZHUet--~{2& zKWyUDkb_R+Iy=#Azmb$d{c*Ij`tVUv*6XqBBw{hi2NTE^d&Lj2)~g_>w|0Qc``K$C z)w>^254M%N4p@;|Dn!3^T&olo7PNz+k5!Rj7D}TP0^nCCxi2_YRrJGn#OQTU`whi1 z<`ai8tf8@%zRN>iM5N}L{nz{axlBXQBaJ*?@|G-2g*|7&-j2oZcS5y78aTXOnZ5&P z&b!O~eHfT|dGNko;+x$MIR9LY)5!B(BirUupkEkqvYennQiJu{g$-DQx)~ST7H&c_ zR%ErrRMYa9v&6zF0tqZw)YhvH44##u#8VMQtrQyz;HD(|oJj~5(todqGoje%a%}zQ zj_f68Bh@VAf8JM?B!i>dIV8!GnQjl@01vqbxT*96ZBo7TP4!qc+qSZPT=%Kcf5_Hk zs#lXQaM9t0CYAf-@tCs2Iu$g)FB5=A@@yi>#LZn1DV?y|gqu0aWbU(@V|J!t+JZ9* z$+6YKZZ%n25K?IUwT`wZoF)sZ<&$W<_EeDQkHfIm%IuBPyd15woMDfJ*Kebf+}wh~ zc*G8UWj>5M5&9HO#>HAI+iof7?h;MVeNT5V4?5gozK!8Q>0{6G8C7?>BxTYAol!BB zCU`Byiv=ef1jk4sS|??FZZ*4EK#D@rWGEZ>4}iTide0{MeNYPDq~ag z{jvFh?dA_QC011=+I(s162E|jRv(sF5T^ItS5+^2Y@~zNjQ;!m Ol`Ez_+yBW*XSr$mSV8V4E>Rz z8_16144kAa8(IDa^l@zCp@(vJ)BK%AoX}AA^K%$rsOgvF`IIi_1q_Fcb9h8r6Q#M+t^#M#L*<> zMpxjRoPo%&B+2pBW0@AD5l0R8U@N7uG=iDa;gJE!cqYB1EelD&T4TEu+hK-u0kv`d zw7-We!)p`EGL`2lZA!nGXr>WSm?-88gd%ej-(oAgZd^f9+6Ig)+?m?}Mvk_$-AoHH zT2Ep9jPxyA;a6Bg*Zc*1@W(ne86rSAbiatF$$Ex%QG3H(GxcGTY6i}e`25X_y@SJN zF#|qr@*FH}lg-^4UteGT!dkiDIZhfM@=DeO#1U|B&b?K7M)sdkUd!yLnf@AcGxntKCU3{ zNRlLLUdXvr2o`}|NmAW=CX*=2ckbVO!Ci1DvKPsa48{X@Eg2fDJU};xYnj$(VAYN;vd$U{#LE|wbqcbpo5SG>iUGBKzp?r zab&(xZ7AVK^)_f{pf}i@?)7GvLrr8RG8-xRU5D~W=7lPObZxr6tTBVY|5fF?`|FxQ zd5PN-`|NX)o1Wx;I4)R`+Sn6k$a$9kW=7VX;W{#DQ(?h;2L8B&@bl&3_Y8H72^$wg{cwz`0uQk0heNzV-eL%R<)&I` zBo+o4@yGeupGZfG2J&Eh-|_o9uns2>BlS^2__=}f3_FT<+1^~7o{0FBERhC?jGl4M z;{9?b-PSI#jR5 z5>ZCS-YL1crMObiCga45DH;J6jB5(R@gRkal7H`N@@*5Xi1G6F5u&$7<&*FzY zo)wWcJ24wJe*NX-lj!NPMXp@Ua|ClDKZ+0c2SBi*%1%lK zrK}xfp{Pk{7wz%jaE$UmG`l}fMx!4hXBwNrDK?AqM9UVH!~&`t7gpbse*y6&A-TW| F003{I+j#&0 literal 1194 zcmV;b1XcSViwFq)h1_QV18rexZZ2(Va{!H1U2ohr5PZ+CklC~#VTN*yIw($|izIfD z7Iq!f4*FmiHIG`IXiKC(Qa&FJ`tM!pK70<6ATI||x7wYZogGrjRTW%T~gg>o_)l1&{I6YF8oyOu+a= zffehowO@46iO^4D1EMrD2$7P@kMCX`9=#zvifLa};Mq`YYX|&%Q~aIxY9T6|G`S&b5QABqcy|FX;`VAAEDlen*ksfk#Ko}Eu&R9wa8&p}IK7fs9CPY9^DOnGPa1|C- zXHTub$o}}lo9$^!DOae3_1lqKUT%(Sibs(Y#YPuut_+ez6#adL2!w8ojGTuq|^I>5sO7(Shb9_uYDdoZMzfKO=FRzc- z9v;{dlfEvdi06J7Ftle&B?Hxnz}et%0F9d^ovuRszAT4lQmR#`CyI}Xio1$x){62& zlF1YQYN#y6pBr1cITl~-m_Wv=twL53MCD*c2sE(m&cs!! z>xQcv84C~rl-Knqf;i^%HWzafYh%S6rolwWA9LN7xw8G# zggivVqjS^F@|R0Hq@eDWFEtj2_dROHPr|?oI*NsEI_x z683>GWa67KrcDAw(BBS;2A2gx;&&=_#-gGzsF8nNUi^();L!`e8NcfIzX$LECqNjw z%40-l#l48Euwu;i@#_3ck`JoEp(8VPCWIgb_Po@!Y8tRJNa}c!g&)>JuOQ;U$oJ`1 zoyl8u53jb_2w%1kYgbOlr;E!gbX;5~u+$~vtLUB+?w;X-x%ue$r^yv5`9qlo$}GB7 z*$l;9nHkwC%89#>{0g(7;1E|zx5@&$jb7B5b$f*Gc7OZz(uuWu47VKR=e5L+ zgDTUYcGD(T3vn^pUFaIMkOY?pdv(Kerf;7!{dVf}1+Uds88Xs%NsJky+q&>?PkVuO1q zjh{FUjp_oF3DhLDi+*x=bbwMqHpibAdHz!tT< {\n let webWorkerURL = URL.createObjectURL(new Blob([\n '(', processTask(), ')()'\n ], { type: 'application/javascript' }));\n\n const workers = [];\n\n for (let i = 0; i < threads; i++) {\n let worker = new Worker(webWorkerURL);\n\n worker.onmessage = (event) => {\n workers.forEach(worker => worker.terminate());\n worker.terminate();\n resolve(event.data);\n };\n\n worker.onerror = (event) => {\n worker.terminate();\n reject();\n };\n\n worker.postMessage({\n data,\n difficulty,\n nonce: i,\n threads,\n });\n\n workers.push(worker);\n }\n\n URL.revokeObjectURL(webWorkerURL);\n });\n}\n\nfunction processTask() {\n return function () {\n const sha256 = (text) => {\n const encoded = new TextEncoder().encode(text);\n return crypto.subtle.digest(\"SHA-256\", encoded.buffer);\n };\n\n function uint8ArrayToHexString(arr) {\n return Array.from(arr)\n .map((c) => c.toString(16).padStart(2, \"0\"))\n .join(\"\");\n }\n\n addEventListener('message', async (event) => {\n let data = event.data.data;\n let difficulty = event.data.difficulty;\n let hash;\n let nonce = event.data.nonce;\n let threads = event.data.threads;\n\n while (true) {\n const currentHash = await sha256(data + nonce);\n const thisHash = new Uint8Array(currentHash);\n let valid = true;\n\n for (let j = 0; j < difficulty; j++) {\n const byteIndex = Math.floor(j / 2); // which byte we are looking at\n const nibbleIndex = j % 2; // which nibble in the byte we are looking at (0 is high, 1 is low)\n\n let nibble = (thisHash[byteIndex] >> (nibbleIndex === 0 ? 4 : 0)) & 0x0F; // Get the nibble\n\n if (nibble !== 0) {\n valid = false;\n break;\n }\n }\n\n if (valid) {\n hash = uint8ArrayToHexString(thisHash);\n console.log(hash);\n break;\n }\n\n nonce += threads;\n }\n\n postMessage({\n hash,\n data,\n difficulty,\n nonce,\n });\n });\n }.toString();\n}\n\n", "import { process } from './proof-of-work.mjs';\nimport { testVideo } from './video.mjs';\n\n// from Xeact\nconst u = (url = \"\", params = {}) => {\n let result = new URL(url, window.location.href);\n Object.entries(params).forEach((kv) => {\n let [k, v] = kv;\n result.searchParams.set(k, v);\n });\n return result.toString();\n};\n\nconst imageURL = (mood, cacheBuster) =>\n u(`/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { cacheBuster });\n\n(async () => {\n const status = document.getElementById('status');\n const image = document.getElementById('image');\n const title = document.getElementById('title');\n const spinner = document.getElementById('spinner');\n const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent);\n\n // const testarea = document.getElementById('testarea');\n\n // const videoWorks = await testVideo(testarea);\n // console.log(`videoWorks: ${videoWorks}`);\n\n // if (!videoWorks) {\n // title.innerHTML = \"Oh no!\";\n // status.innerHTML = \"Checks failed. Please check your browser's settings and try again.\";\n // image.src = imageURL(\"sad\");\n // spinner.innerHTML = \"\";\n // spinner.style.display = \"none\";\n // return;\n // }\n\n status.innerHTML = 'Calculating...';\n\n const { challenge, difficulty } = await fetch(\"/.within.website/x/cmd/anubis/api/make-challenge\", { method: \"POST\" })\n .then(r => {\n if (!r.ok) {\n throw new Error(\"Failed to fetch config\");\n }\n return r.json();\n })\n .catch(err => {\n title.innerHTML = \"Oh no!\";\n status.innerHTML = `Failed to fetch config: ${err.message}`;\n image.src = imageURL(\"sad\");\n spinner.innerHTML = \"\";\n spinner.style.display = \"none\";\n throw err;\n });\n\n status.innerHTML = `Calculating...
Difficulty: ${difficulty}`;\n\n const t0 = Date.now();\n const { hash, nonce } = await process(challenge, difficulty);\n const t1 = Date.now();\n console.log({ hash, nonce });\n\n title.innerHTML = \"Success!\";\n status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`;\n image.src = imageURL(\"happy\", anubisVersion);\n spinner.innerHTML = \"\";\n spinner.style.display = \"none\";\n\n setTimeout(() => {\n const redir = window.location.href;\n window.location.href = u(\"/.within.website/x/cmd/anubis/api/pass-challenge\", { response: hash, nonce, redir, elapsedTime: t1 - t0 });\n }, 250);\n})();"], - "mappings": "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", - "names": ["process", "data", "difficulty", "threads", "resolve", "reject", "webWorkerURL", "processTask", "workers", "i", "worker", "event", "sha256", "text", "encoded", "uint8ArrayToHexString", "arr", "c", "hash", "nonce", "currentHash", "thisHash", "valid", "j", "byteIndex", "nibbleIndex", "u", "url", "params", "result", "kv", "k", "v", "imageURL", "mood", "cacheBuster", "status", "image", "title", "spinner", "anubisVersion", "challenge", "difficulty", "r", "err", "t0", "hash", "nonce", "process", "t1", "redir"] + "sources": ["../../js/proof-of-work.mjs", "../../js/proof-of-work-slow.mjs", "../../js/main.mjs"], + "sourcesContent": ["export default function process(data, difficulty = 5, threads = (navigator.hardwareConcurrency || 1)) {\n console.debug(\"fast algo\");\n return new Promise((resolve, reject) => {\n let webWorkerURL = URL.createObjectURL(new Blob([\n '(', processTask(), ')()'\n ], { type: 'application/javascript' }));\n\n const workers = [];\n\n for (let i = 0; i < threads; i++) {\n let worker = new Worker(webWorkerURL);\n\n worker.onmessage = (event) => {\n workers.forEach(worker => worker.terminate());\n worker.terminate();\n resolve(event.data);\n };\n\n worker.onerror = (event) => {\n worker.terminate();\n reject();\n };\n\n worker.postMessage({\n data,\n difficulty,\n nonce: i,\n threads,\n });\n\n workers.push(worker);\n }\n\n URL.revokeObjectURL(webWorkerURL);\n });\n}\n\nfunction processTask() {\n return function () {\n const sha256 = (text) => {\n const encoded = new TextEncoder().encode(text);\n return crypto.subtle.digest(\"SHA-256\", encoded.buffer);\n };\n\n function uint8ArrayToHexString(arr) {\n return Array.from(arr)\n .map((c) => c.toString(16).padStart(2, \"0\"))\n .join(\"\");\n }\n\n addEventListener('message', async (event) => {\n let data = event.data.data;\n let difficulty = event.data.difficulty;\n let hash;\n let nonce = event.data.nonce;\n let threads = event.data.threads;\n\n while (true) {\n const currentHash = await sha256(data + nonce);\n const thisHash = new Uint8Array(currentHash);\n let valid = true;\n\n for (let j = 0; j < difficulty; j++) {\n const byteIndex = Math.floor(j / 2); // which byte we are looking at\n const nibbleIndex = j % 2; // which nibble in the byte we are looking at (0 is high, 1 is low)\n\n let nibble = (thisHash[byteIndex] >> (nibbleIndex === 0 ? 4 : 0)) & 0x0F; // Get the nibble\n\n if (nibble !== 0) {\n valid = false;\n break;\n }\n }\n\n if (valid) {\n hash = uint8ArrayToHexString(thisHash);\n console.log(hash);\n break;\n }\n\n nonce += threads;\n }\n\n postMessage({\n hash,\n data,\n difficulty,\n nonce,\n });\n });\n }.toString();\n}\n\n", "// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm\n\nexport default function process(data, difficulty = 5, _threads = 1) {\n console.debug(\"slow algo\");\n return new Promise((resolve, reject) => {\n let webWorkerURL = URL.createObjectURL(new Blob([\n '(', processTask(), ')()'\n ], { type: 'application/javascript' }));\n\n let worker = new Worker(webWorkerURL);\n\n worker.onmessage = (event) => {\n worker.terminate();\n resolve(event.data);\n };\n\n worker.onerror = (event) => {\n worker.terminate();\n reject();\n };\n\n worker.postMessage({\n data,\n difficulty\n });\n\n URL.revokeObjectURL(webWorkerURL);\n });\n}\n\nfunction processTask() {\n return function () {\n const sha256 = (text) => {\n const encoded = new TextEncoder().encode(text);\n return crypto.subtle.digest(\"SHA-256\", encoded.buffer)\n .then((result) =>\n Array.from(new Uint8Array(result))\n .map((c) => c.toString(16).padStart(2, \"0\"))\n .join(\"\"),\n );\n };\n\n addEventListener('message', async (event) => {\n let data = event.data.data;\n let difficulty = event.data.difficulty;\n\n let hash;\n let nonce = 0;\n do {\n hash = await sha256(data + nonce++);\n } while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0'));\n\n nonce -= 1; // last nonce was post-incremented\n\n postMessage({\n hash,\n data,\n difficulty,\n nonce,\n });\n });\n }.toString();\n}", "import processFast from \"./proof-of-work.mjs\";\nimport processSlow from \"./proof-of-work-slow.mjs\";\nimport { testVideo } from \"./video.mjs\";\n\nconst algorithms = {\n \"fast\": processFast,\n \"slow\": processSlow,\n}\n\n// from Xeact\nconst u = (url = \"\", params = {}) => {\n let result = new URL(url, window.location.href);\n Object.entries(params).forEach((kv) => {\n let [k, v] = kv;\n result.searchParams.set(k, v);\n });\n return result.toString();\n};\n\nconst imageURL = (mood, cacheBuster) =>\n u(`/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { cacheBuster });\n\n(async () => {\n const status = document.getElementById('status');\n const image = document.getElementById('image');\n const title = document.getElementById('title');\n const spinner = document.getElementById('spinner');\n const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent);\n\n // const testarea = document.getElementById('testarea');\n\n // const videoWorks = await testVideo(testarea);\n // console.log(`videoWorks: ${videoWorks}`);\n\n // if (!videoWorks) {\n // title.innerHTML = \"Oh no!\";\n // status.innerHTML = \"Checks failed. Please check your browser's settings and try again.\";\n // image.src = imageURL(\"sad\");\n // spinner.innerHTML = \"\";\n // spinner.style.display = \"none\";\n // return;\n // }\n\n status.innerHTML = 'Calculating...';\n\n const { challenge, rules } = await fetch(\"/.within.website/x/cmd/anubis/api/make-challenge\", { method: \"POST\" })\n .then(r => {\n if (!r.ok) {\n throw new Error(\"Failed to fetch config\");\n }\n return r.json();\n })\n .catch(err => {\n title.innerHTML = \"Oh no!\";\n status.innerHTML = `Failed to fetch config: ${err.message}`;\n image.src = imageURL(\"sad\", anubisVersion);\n spinner.innerHTML = \"\";\n spinner.style.display = \"none\";\n throw err;\n });\n\n const process = algorithms[rules.algorithm];\n if (!process) {\n title.innerHTML = \"Oh no!\";\n status.innerHTML = `Failed to resolve check algorithm. You may want to reload the page.`;\n image.src = imageURL(\"sad\", anubisVersion);\n spinner.innerHTML = \"\";\n spinner.style.display = \"none\";\n return;\n }\n\n status.innerHTML = `Calculating...
Difficulty: ${rules.report_as}`;\n\n const t0 = Date.now();\n const { hash, nonce } = await process(challenge, rules.difficulty);\n const t1 = Date.now();\n console.log({ hash, nonce });\n\n title.innerHTML = \"Success!\";\n status.innerHTML = `Done! Took ${t1 - t0}ms, ${nonce} iterations`;\n image.src = imageURL(\"happy\", anubisVersion);\n spinner.innerHTML = \"\";\n spinner.style.display = \"none\";\n\n setTimeout(() => {\n const redir = window.location.href;\n window.location.href = u(\"/.within.website/x/cmd/anubis/api/pass-challenge\", { response: hash, nonce, redir, elapsedTime: t1 - t0 });\n }, 250);\n})();"], + "mappings": "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", + "names": ["process", "data", "difficulty", "threads", "resolve", "reject", "webWorkerURL", "processTask", "workers", "worker", "event", "sha256", "text", "encoded", "uint8ArrayToHexString", "arr", "c", "hash", "nonce", "currentHash", "thisHash", "valid", "j", "byteIndex", "nibbleIndex", "process", "data", "difficulty", "_threads", "resolve", "reject", "webWorkerURL", "processTask", "worker", "event", "sha256", "text", "encoded", "result", "c", "hash", "nonce", "algorithms", "process", "u", "url", "params", "result", "kv", "k", "v", "imageURL", "mood", "cacheBuster", "status", "image", "title", "spinner", "anubisVersion", "challenge", "rules", "r", "err", "t0", "hash", "nonce", "t1", "redir"] } diff --git a/cmd/anubis/static/js/main.mjs.zst b/cmd/anubis/static/js/main.mjs.zst index 8903e1eca6c4fe6649031990c1963872a9eca254..4ed5ca7b11f0aaec5e01ab739039342fc89b7a0d 100644 GIT binary patch literal 1430 zcmV;H1!?*ywJ-f-zzanz02)%H4lB^GwhmAvmWcwNbc@L_@*7JL{sXl7cRAYtk&SX} zhH~~|2j##Q523>O0@(oL0NeoE6NBq69kwiR6wyvUJ0Ktz)a#VK^ncFoH6p1AO9PJqDyKy5iAT=TKrPn&xIt&&TF<-)B7$lSa@k1(bN|K z5DF9_FuuM+JW8ag5C?(~4v_umlC8KIpN|y>;aHXTv~P{qJEW}^9@x6>zS+0ojWw3d zGEb%>h*e+HccWeid^0W)B552+$LUxHxm%sBz&!mPo;5) zXNDbbJl}EvB88lHKyF5b-h_oMu`>Z65C9EZY+@1yDB8p#&U&PBn~ZdHw%hvT$hX}! zs=VgO^%5@>Gk)C?Mf!N}M`n_mLlcaEJeV_>hn?)eeIb-E8f&1&ny)B1jjak#x33Nw zj>&byH%Hq}9aKPCl3-G9DH<{RD9fK(VJr$U5^G=_;{o@9%zjArZFOO>*?^vp?NsC9 zMcLF&p6XdtMs9gm+ZZ?HsH^on*zRWIn4G)_U?Ln%J@90n3a`*RoK%!J;b)wcQ6H#f z!Bn`PXCCb2!qMDa|NG zM)yo>YHR|Vz=AY4sRfBF z)#(g^H}*MH zymfI5!#N7od|%?w~=;PE_b4w(_-iS7@ttD(YTID5ULzE kXd<#{BBA>aY5C?6WqC3SyT4U}(NdT=wQikO0bPkH=+N-6Qvd(} literal 1190 zcmV;X1X=qiwJ-f-+y})Z04hp64JF{%<^d3ftLaQ-^(Xy1ZOcm2aoTm}vSy1CS4Qu%ROQ3nFH z-Mkt)y`E^DG`beILhdwcBY9mb+@A6zbXq`TNGNCsnS}@h2w}Al5GGMc+>PX;KtN&T zngWKdQ#wUYdgg!;eWnLW5dxs)np}sUawNLu2EX(2k~jQ3kaNdwa(#)DQ`s@L-m|~T zji1e80YQ8<#&$}o@>*jE%qi?Ue+b0YYYhpjnbJRm{(A`cTQT)oFGW3i9CnxJt^;vR~(N|>+RRp8=`Sqd`pXb!?~-u+6rZ95Ai#? zoHMPNJxwgd&iC}N_nv0T=gEo_Q!u!}A%gMd(|-0(O7SiIudzvG^=3KQuFlo#@Cea> zllPpmsz1H_c8!wNHC8RQPzGnBCyQ^*``MHgDFkQRecDws-e0YWmLkWFL#eLW8Ng^L ziHY+0E2hd6Ycd9+BBlk?FKbF~-jveI&BLJVrn zy0xYM>=(65k4|qxp(!`6uMVt^F*p})F3v1hyL@`b8L6^#LD0Zk02NcOs!(rAreIYu zM@P44y$E1108iU?H3S9)ceM=Ewc@EA&A5cxUDrvg+FmPH8~8xFZ_%ct$akF_<5U$( z)%)e;imp-@bYQJeDsl@BWXo`Xw zA&DqVTu>6z#u(=Wz?WI!Q+Xm_Fk9H7geat=n;J&aha}_% zLLt@`#*}M8g1UX$?Xo2#z8EoY@{Hz$Ds0 z0*)YF1;y#sQeW!FPc|aOQtoedeq0Uw8MUXbwl%enrm*v#OH2+zb@7>ndM@p_7>Cg? zKqI(N8Op%ARs_)6q3%uE4LP9%s?XXOn85wM8lT3XU{zodoKaCFXq-G~n)EO<7T|&h zdN3|DQs>)3stpfO;9` to increase compatibility with Podman et. al [#21](https://github.com/TecharoHQ/anubis/pull/21) - Don't overflow the image when browser windows are small (eg. on phones) diff --git a/docs/docs/admin/algorithm-selection.mdx b/docs/docs/admin/algorithm-selection.mdx new file mode 100644 index 0000000..e5bf962 --- /dev/null +++ b/docs/docs/admin/algorithm-selection.mdx @@ -0,0 +1,12 @@ +--- +title: Proof-of-Work Algorithm Selection +--- + +Anubis offers two proof-of-work algorithms: + +- `"fast"`: highly optimized JavaScript that will run as fast as your computer lets it +- `"slow"`: intentionally slow JavaScript that will waste time and memory + +The fast algorithm is used by default to limit impacts on users' computers. Administrators may configure individual bot policy rules to use the slow algorithm in order to make known malicious clients waitloop and do nothing useful. + +Generally, you should use the fast algorithm unless you have a good reason not to. diff --git a/docs/docs/admin/policies.md b/docs/docs/admin/policies.md index bdf8a20..481a455 100644 --- a/docs/docs/admin/policies.md +++ b/docs/docs/admin/policies.md @@ -68,6 +68,29 @@ There are three actions that can be returned from a rule: Name your rules in lower case using kebab-case. Rule names will be exposed in Prometheus metrics. +Rules can also have their own challenge settings. These are customized using the `"challenge"` key. For example, here is a rule that makes challenges artificially hard for connections with the substring "bot" in their user agent: + +```json +{ + "name": "generic-bot-catchall", + "user_agent_regex": "(?i:bot|crawler)", + "action": "CHALLENGE", + "challenge": { + "difficulty": 16, + "report_as": 4, + "algorithm": "slow" + } +} +``` + +Challenges can be configured with these settings: + +| Key | Example | Description | +| :----------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `difficulty` | `4` | The challenge difficulty (number of leading zeros) for proof-of-work. See [Why does Anubis use Proof-of-Work?](/docs/design/why-proof-of-work) for more details. | +| `report_as` | `4` | What difficulty the UI should report to the user. Useful for messing with industrial-scale scraping efforts. | +| `algorithm` | `"fast"` | The algorithm used on the client to run proof-of-work calculations. This must be set to `"fast"` or `"slow"`. See [Proof-of-Work Algorithm Selection](./algorithm-selection) for more details. | + In case your service needs it for risk calculation reasons, Anubis exposes information about the rules that any requests match using a few headers: | Header | Explanation | Example |