package anubis import ( "encoding/json" "fmt" "regexp" ) // Anubis embeds several JSON blobs in its interstitial as // elements. We read two: // // - anubis_challenge: the canonical challenge (id, randomData, rules) // - preact_info: the preact frontend's view, crucially the `redir` // (the fully-formed pass-challenge URL) and `challenge` string. // // root-me.org uses the "preact" challenge: the answer is simply // SHA-256(randomData) (no proof-of-work nonce), gated by a minimum wait of // difficulty*80ms server-side. type anubisChallenge struct { Challenge struct { ID string `json:"id"` Method string `json:"method"` RandomData string `json:"randomData"` IssuedAt string `json:"issuedAt"` } `json:"challenge"` Rules struct { Difficulty int `json:"difficulty"` Algorithm string `json:"algorithm"` } `json:"rules"` } type preactInfo struct { Challenge string `json:"challenge"` Difficulty int `json:"difficulty"` Redir string `json:"redir"` // relative pass-challenge URL with id+redir baked in } // challenge is the normalized view the transport acts on. type challenge struct { algorithm string id string randomData string difficulty int redir string // preact: server-provided relative pass-challenge URL } func scriptRe(id string) *regexp.Regexp { return regexp.MustCompile( `(?is)]*\bid=["']` + regexp.QuoteMeta(id) + `["'][^>]*>(.*?)`) } var ( anubisChallengeRe = scriptRe("anubis_challenge") preactInfoRe = scriptRe("preact_info") ) // isInterstitial reports whether an HTML body is an Anubis challenge page. func isInterstitial(body []byte) bool { return anubisChallengeRe.Match(body) } func extractJSON(re *regexp.Regexp, body []byte, into any) error { m := re.FindSubmatch(body) if m == nil { return fmt.Errorf("anubis: script element not found") } if err := json.Unmarshal(m[1], into); err != nil { return fmt.Errorf("anubis: decode embedded JSON: %w", err) } return nil } // parseChallenge extracts and normalizes the Anubis challenge from // interstitial HTML. func parseChallenge(body []byte) (*challenge, error) { var ac anubisChallenge if err := extractJSON(anubisChallengeRe, body, &ac); err != nil { return nil, fmt.Errorf("anubis: parse anubis_challenge: %w", err) } if ac.Challenge.RandomData == "" || ac.Rules.Difficulty <= 0 { return nil, fmt.Errorf("anubis: incomplete challenge (randomData=%q difficulty=%d)", ac.Challenge.RandomData, ac.Rules.Difficulty) } c := &challenge{ algorithm: ac.Rules.Algorithm, id: ac.Challenge.ID, randomData: ac.Challenge.RandomData, difficulty: ac.Rules.Difficulty, } // The preact challenge carries the fully-formed pass-challenge URL in // preact_info.redir; grab it when present. if c.algorithm == "preact" { var pi preactInfo if err := extractJSON(preactInfoRe, body, &pi); err != nil { return nil, fmt.Errorf("anubis: parse preact_info: %w", err) } if pi.Redir == "" { return nil, fmt.Errorf("anubis: preact_info missing redir") } c.redir = pi.Redir if pi.Challenge != "" { c.randomData = pi.Challenge } if pi.Difficulty > 0 { c.difficulty = pi.Difficulty } } return c, nil }