112 lines
3.2 KiB
Go
112 lines
3.2 KiB
Go
package anubis
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
)
|
|
|
|
// Anubis embeds several JSON blobs in its interstitial as
|
|
// <script id="...">{...}</script> 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)<script[^>]*\bid=["']` + regexp.QuoteMeta(id) + `["'][^>]*>(.*?)</script>`)
|
|
}
|
|
|
|
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
|
|
}
|