feat: initial commit
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user