diff options
| -rw-r--r-- | cmd/anubis/botPolicies.json | 12 | ||||
| -rw-r--r-- | cmd/anubis/internal/config/config.go | 68 | ||||
| -rw-r--r-- | cmd/anubis/internal/config/config_test.go | 42 | ||||
| -rw-r--r-- | cmd/anubis/js/main.mjs | 28 | ||||
| -rw-r--r-- | cmd/anubis/js/proof-of-work-slow.mjs | 63 | ||||
| -rw-r--r-- | cmd/anubis/js/proof-of-work.mjs | 5 | ||||
| -rw-r--r-- | cmd/anubis/main.go | 45 | ||||
| -rw-r--r-- | cmd/anubis/policy.go | 26 | ||||
| -rw-r--r-- | cmd/anubis/policy_test.go | 6 | ||||
| -rw-r--r-- | cmd/anubis/static/js/main.mjs | 2 | ||||
| -rw-r--r-- | cmd/anubis/static/js/main.mjs.br | bin | 992 -> 1216 bytes | |||
| -rw-r--r-- | cmd/anubis/static/js/main.mjs.gz | bin | 1194 -> 1451 bytes | |||
| -rw-r--r-- | cmd/anubis/static/js/main.mjs.map | 8 | ||||
| -rw-r--r-- | cmd/anubis/static/js/main.mjs.zst | bin | 1190 -> 1430 bytes | |||
| -rw-r--r-- | docs/docs/CHANGELOG.md | 17 | ||||
| -rw-r--r-- | docs/docs/admin/algorithm-selection.mdx | 12 | ||||
| -rw-r--r-- | docs/docs/admin/policies.md | 23 |
17 files changed, 311 insertions, 46 deletions
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 @@ -66,10 +66,20 @@ "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", "action": "CHALLENGE" } ], "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...<br/>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...<br/>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<e;d++){let i=new Worker(a);i.onmessage=c=>{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<l;o++){let f=Math.floor(o/2),p=o%2;if((c[f]>>(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...<br/>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<t;i++){let c=new Worker(s);c.onmessage=d=>{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<o;m++){let l=Math.floor(m/2),g=m%2;if((d[l]>>(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...<br/>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 Binary files differindex a7b35e9..6bfd987 100644 --- a/cmd/anubis/static/js/main.mjs.br +++ b/cmd/anubis/static/js/main.mjs.br diff --git a/cmd/anubis/static/js/main.mjs.gz b/cmd/anubis/static/js/main.mjs.gz Binary files differindex e0b648c..0e8d735 100644 --- a/cmd/anubis/static/js/main.mjs.gz +++ b/cmd/anubis/static/js/main.mjs.gz diff --git a/cmd/anubis/static/js/main.mjs.map b/cmd/anubis/static/js/main.mjs.map index 3a0d564..7cad28b 100644 --- a/cmd/anubis/static/js/main.mjs.map +++ b/cmd/anubis/static/js/main.mjs.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../../js/proof-of-work.mjs", "../../js/main.mjs"], - "sourcesContent": ["// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm\n\nexport function process(data, difficulty = 5, threads = navigator.hardwareConcurrency) {\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", "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...<br/>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": "MAEO,SAASA,EAAQC,EAAMC,EAAa,EAAGC,EAAU,UAAU,oBAAqB,CACrF,OAAO,IAAI,QAAQ,CAACC,EAASC,IAAW,CACtC,IAAIC,EAAe,IAAI,gBAAgB,IAAI,KAAK,CAC9C,IAAKC,EAAY,EAAG,KACtB,EAAG,CAAE,KAAM,wBAAyB,CAAC,CAAC,EAEhCC,EAAU,CAAC,EAEjB,QAASC,EAAI,EAAGA,EAAIN,EAASM,IAAK,CAChC,IAAIC,EAAS,IAAI,OAAOJ,CAAY,EAEpCI,EAAO,UAAaC,GAAU,CAC5BH,EAAQ,QAAQE,GAAUA,EAAO,UAAU,CAAC,EAC5CA,EAAO,UAAU,EACjBN,EAAQO,EAAM,IAAI,CACpB,EAEAD,EAAO,QAAWC,GAAU,CAC1BD,EAAO,UAAU,EACjBL,EAAO,CACT,EAEAK,EAAO,YAAY,CACjB,KAAAT,EACA,WAAAC,EACA,MAAOO,EACP,QAAAN,CACF,CAAC,EAEDK,EAAQ,KAAKE,CAAM,CACrB,CAEA,IAAI,gBAAgBJ,CAAY,CAClC,CAAC,CACH,CAEA,SAASC,GAAc,CACrB,OAAO,UAAY,CACjB,IAAMK,EAAUC,GAAS,CACvB,IAAMC,EAAU,IAAI,YAAY,EAAE,OAAOD,CAAI,EAC7C,OAAO,OAAO,OAAO,OAAO,UAAWC,EAAQ,MAAM,CACvD,EAEA,SAASC,EAAsBC,EAAK,CAClC,OAAO,MAAM,KAAKA,CAAG,EAClB,IAAKC,GAAMA,EAAE,SAAS,EAAE,EAAE,SAAS,EAAG,GAAG,CAAC,EAC1C,KAAK,EAAE,CACZ,CAEA,iBAAiB,UAAW,MAAON,GAAU,CAC3C,IAAIV,EAAOU,EAAM,KAAK,KAClBT,EAAaS,EAAM,KAAK,WACxBO,EACAC,EAAQR,EAAM,KAAK,MACnBR,EAAUQ,EAAM,KAAK,QAEzB,OAAa,CACX,IAAMS,EAAc,MAAMR,EAAOX,EAAOkB,CAAK,EACvCE,EAAW,IAAI,WAAWD,CAAW,EACvCE,EAAQ,GAEZ,QAASC,EAAI,EAAGA,EAAIrB,EAAYqB,IAAK,CACnC,IAAMC,EAAY,KAAK,MAAMD,EAAI,CAAC,EAC5BE,EAAcF,EAAI,EAIxB,IAFcF,EAASG,CAAS,IAAMC,IAAgB,EAAI,EAAI,GAAM,MAErD,EAAG,CAChBH,EAAQ,GACR,KACF,CACF,CAEA,GAAIA,EAAO,CACTJ,EAAOH,EAAsBM,CAAQ,EACrC,QAAQ,IAAIH,CAAI,EAChB,KACF,CAEAC,GAAShB,CACX,CAEA,YAAY,CACV,KAAAe,EACA,KAAAjB,EACA,WAAAC,EACA,MAAAiB,CACF,CAAC,CACH,CAAC,CACH,EAAE,SAAS,CACb,CCxFA,IAAMO,EAAI,CAACC,EAAM,GAAIC,EAAS,CAAC,IAAM,CACnC,IAAIC,EAAS,IAAI,IAAIF,EAAK,OAAO,SAAS,IAAI,EAC9C,cAAO,QAAQC,CAAM,EAAE,QAASE,GAAO,CACrC,GAAI,CAACC,EAAGC,CAAC,EAAIF,EACbD,EAAO,aAAa,IAAIE,EAAGC,CAAC,CAC9B,CAAC,EACMH,EAAO,SAAS,CACzB,EAEMI,EAAW,CAACC,EAAMC,IACtBT,EAAE,4CAA4CQ,CAAI,QAAS,CAAE,YAAAC,CAAY,CAAC,GAE3E,SAAY,CACX,IAAMC,EAAS,SAAS,eAAe,QAAQ,EACzCC,EAAQ,SAAS,eAAe,OAAO,EACvCC,EAAQ,SAAS,eAAe,OAAO,EACvCC,EAAU,SAAS,eAAe,SAAS,EAC3CC,EAAgB,KAAK,MAAM,SAAS,eAAe,gBAAgB,EAAE,WAAW,EAgBtFJ,EAAO,UAAY,iBAEnB,GAAM,CAAE,UAAAK,EAAW,WAAAC,CAAW,EAAI,MAAM,MAAM,mDAAoD,CAAE,OAAQ,MAAO,CAAC,EACjH,KAAKC,GAAK,CACT,GAAI,CAACA,EAAE,GACL,MAAM,IAAI,MAAM,wBAAwB,EAE1C,OAAOA,EAAE,KAAK,CAChB,CAAC,EACA,MAAMC,GAAO,CACZ,MAAAN,EAAM,UAAY,SAClBF,EAAO,UAAY,2BAA2BQ,EAAI,OAAO,GACzDP,EAAM,IAAMJ,EAAS,KAAK,EAC1BM,EAAQ,UAAY,GACpBA,EAAQ,MAAM,QAAU,OAClBK,CACR,CAAC,EAEHR,EAAO,UAAY,kCAAkCM,CAAU,GAE/D,IAAMG,EAAK,KAAK,IAAI,EACd,CAAE,KAAAC,EAAM,MAAAC,CAAM,EAAI,MAAMC,EAAQP,EAAWC,CAAU,EACrDO,EAAK,KAAK,IAAI,EACpB,QAAQ,IAAI,CAAE,KAAAH,EAAM,MAAAC,CAAM,CAAC,EAE3BT,EAAM,UAAY,WAClBF,EAAO,UAAY,cAAca,EAAKJ,CAAE,OAAOE,CAAK,cACpDV,EAAM,IAAMJ,EAAS,QAASO,CAAa,EAC3CD,EAAQ,UAAY,GACpBA,EAAQ,MAAM,QAAU,OAExB,WAAW,IAAM,CACf,IAAMW,EAAQ,OAAO,SAAS,KAC9B,OAAO,SAAS,KAAOxB,EAAE,mDAAoD,CAAE,SAAUoB,EAAM,MAAAC,EAAO,MAAAG,EAAO,YAAaD,EAAKJ,CAAG,CAAC,CACrI,EAAG,GAAG,CACR,GAAG", - "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 |
