mirror of
https://gitgud.io/yats/cerberus.git
synced 2026-05-02 02:32:18 -04:00
Lay out groundwork for multiple iterations/steps
Add checking for bad solutions. Do http requests with contexts Minor fixes with managing steps counter.
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A fast [Tartarus](https://usips.org/products/tartarus/) solver lib.
|
A fast [Tartarus](https://usips.org/products/tartarus/) solver lib.
|
||||||
|
|
||||||
For technical details, you can try asking Josh (not me) or reverse engineer the *wonderful* solver solutions offered on Tartarus' no-js page. Good luck.
|
For technical details, you can try asking Josh (not me) or reverse engineer the solver solutions offered on Tartarus' no-js page. Good luck.
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
|||||||
141
cerberus.go
141
cerberus.go
@@ -13,63 +13,35 @@ import (
|
|||||||
type Challenge struct {
|
type Challenge struct {
|
||||||
Salt string // Challenge salt from server.
|
Salt string // Challenge salt from server.
|
||||||
Diff uint32 // Difficulty level.
|
Diff uint32 // Difficulty level.
|
||||||
Steps uint32 // Not 100% sure how this is used yet.
|
Steps int8 // Each step consists of a Challenge and a Solution. More than 1 may be required.
|
||||||
host *url.URL
|
// Stored as a signed int to get past underflow issues later on.
|
||||||
}
|
|
||||||
|
|
||||||
type Solution struct {
|
|
||||||
Salt string
|
|
||||||
Nonce uint32
|
|
||||||
Redirect string // To be set manually by caller.
|
|
||||||
|
|
||||||
Hash []byte
|
|
||||||
host *url.URL
|
host *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Brute force nonces until a valid solution is found.
|
// Convenience wrapper, equivalent to Solve(ctx, c).
|
||||||
func genHashes(ctx context.Context, c Challenge) <-chan Solution {
|
func (c Challenge) Solve(ctx context.Context) (Solution, error) {
|
||||||
var (
|
return Solve(ctx, c)
|
||||||
out = make(chan Solution, 1)
|
}
|
||||||
sha = sha256.New()
|
|
||||||
nonce = rand.Uint32()
|
|
||||||
)
|
|
||||||
|
|
||||||
go func() {
|
type Solution struct {
|
||||||
defer close(out)
|
Hash []byte // Not required in POST, but provided for reference.
|
||||||
|
Salt string // Challenge salt.
|
||||||
|
Nonce uint32 // Solution nonce. This is the "answer" to the problem.
|
||||||
|
Redirect string // Relative path to redirect to after Solution is accepted.
|
||||||
|
|
||||||
for {
|
Steps int8 // Steps, as described in Challenge.
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
sha.Write(fmt.Append(nil, c.Salt, nonce))
|
// TODO: Maybe make the redirect shit auto, idk.
|
||||||
|
|
||||||
sol := Solution{
|
host *url.URL
|
||||||
Salt: c.Salt,
|
|
||||||
Nonce: nonce,
|
|
||||||
Redirect: "/", // Use sensible default for placeholder.
|
|
||||||
Hash: sha.Sum(nil),
|
|
||||||
}
|
|
||||||
// Ensure we don't hang if out channel is full on ctx close.
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case out <- sol:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset hasher input for next iteration.
|
|
||||||
sha.Reset()
|
|
||||||
nonce++
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given difficulty is measured in number of leading 0 bits.
|
// Given difficulty is measured in number of leading 0 bits.
|
||||||
func checkZeros(diff uint32, hash []byte) bool {
|
func checkZeros(diff uint32, hash []byte) bool {
|
||||||
|
// I am using this big ugly check to avoid needing to hardcode the max difficulty (32, by the looks of it).
|
||||||
|
// Otherwise, it's a simple < comparison to (1 << (32 - diff))
|
||||||
|
|
||||||
var (
|
var (
|
||||||
rem = diff % 8 // Remainder after dividing diff (given in bits) to bytes.
|
rem = diff % 8 // Remainder after dividing diff (given in bits) to bytes.
|
||||||
nbytes = (diff - rem) / 8 // Amount of 0x0 bytes we can divide difficulty bits into.
|
nbytes = (diff - rem) / 8 // Amount of 0x0 bytes we can divide difficulty bits into.
|
||||||
@@ -77,9 +49,8 @@ func checkZeros(diff uint32, hash []byte) bool {
|
|||||||
mask uint8 // Mask to check remaining bits.
|
mask uint8 // Mask to check remaining bits.
|
||||||
)
|
)
|
||||||
|
|
||||||
lh := uint32(len(hash))
|
|
||||||
// Check bounds for the loops found below.
|
// Check bounds for the loops found below.
|
||||||
if lh < nbytes || (rem > 0 && lh < nbytes+1) {
|
if lh := uint32(len(hash)); lh < nbytes || (rem > 0 && lh < nbytes+1) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,28 +77,76 @@ func checkZeros(diff uint32, hash []byte) bool {
|
|||||||
return hash[nbytes]&mask == 0x0
|
return hash[nbytes]&mask == 0x0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Brute force nonces until a valid solution is found.
|
||||||
|
func genHashes(ctx context.Context, c Challenge) <-chan Solution {
|
||||||
|
var (
|
||||||
|
out = make(chan Solution, 1) // Probably unnecessary buffering for questionable efficiency.
|
||||||
|
sha = sha256.New()
|
||||||
|
nonce = rand.Uint32()
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
sha.Write(fmt.Append(nil, c.Salt, nonce))
|
||||||
|
|
||||||
|
sol := Solution{
|
||||||
|
Hash: sha.Sum(nil),
|
||||||
|
Salt: c.Salt,
|
||||||
|
Nonce: nonce,
|
||||||
|
Redirect: "/", // Use sensible default for placeholder, for now.
|
||||||
|
Steps: c.Steps - 1,
|
||||||
|
}
|
||||||
|
// Ensure we don't hang if out channel is full on ctx close.
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case out <- sol:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset hasher input for next iteration.
|
||||||
|
sha.Reset()
|
||||||
|
nonce++
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// Solve Challenge c. Returns Solution that can be submitted.
|
// Solve Challenge c. Returns Solution that can be submitted.
|
||||||
func Solve(ctx context.Context, c Challenge) (Solution, error) {
|
func Solve(ctx context.Context, c Challenge) (Solution, error) {
|
||||||
sol := make(chan Solution, 1)
|
sol := make(chan Solution, 1)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
worker := func() {
|
||||||
|
hf := genHashes(ctx, c)
|
||||||
|
// Loop until answer has been found.
|
||||||
|
// Should break when hash worker terminates.
|
||||||
|
for h := range hf {
|
||||||
|
if checkZeros(c.Diff, h.Hash) {
|
||||||
|
h.Salt = c.Salt
|
||||||
|
// Set host url to submit this to.
|
||||||
|
h.host = c.host
|
||||||
|
sol <- h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// A reasonable hardware-based job limiter.
|
// A reasonable hardware-based job limiter.
|
||||||
// Use 1 worker per thread at most.
|
// Use 1 worker per thread at most.
|
||||||
threads := runtime.NumCPU()
|
threads := runtime.NumCPU()
|
||||||
for i := 0; i < threads; i++ {
|
for range threads {
|
||||||
go func() {
|
go worker()
|
||||||
hf := genHashes(ctx, c)
|
|
||||||
// Loop until answer has been found.
|
|
||||||
// Should break when hash worker terminates.
|
|
||||||
for h := range hf {
|
|
||||||
if checkZeros(c.Diff, h.Hash) {
|
|
||||||
h.Salt = c.Salt
|
|
||||||
// Set host url to submit this to.
|
|
||||||
h.host = c.host
|
|
||||||
sol <- h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func solveTest(ctx context.Context, hc http.Client, host string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Fetching new %s challenge...", connType)
|
log.Printf("Fetching new %s challenge...", connType)
|
||||||
c, err := NewChallenge(hc, host)
|
c, err := NewChallenge(ctx, hc, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -42,13 +42,15 @@ func solveTest(ctx context.Context, hc http.Client, host string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("Solution hash: %x, nonce: %d\n", s.Hash, s.Nonce)
|
log.Printf("Solution hash: %x, nonce: %d, remaining steps: %d\n", s.Hash, s.Nonce, s.Steps)
|
||||||
|
|
||||||
a, err := Submit(hc, s)
|
resp, err := Submit(ctx, hc, s, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("Response: %s\n\n", a)
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
log.Printf("Response: %s\n\n", resp.Header.Get("Set-Cookie"))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -65,17 +67,13 @@ func newProxyTransport() *http.Transport {
|
|||||||
func TestSubmit(t *testing.T) {
|
func TestSubmit(t *testing.T) {
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
|
|
||||||
hc := http.Client{}
|
err := solveTest(ctx, http.Client{}, _TEST_HOST)
|
||||||
|
|
||||||
err := solveTest(ctx, hc, _TEST_HOST)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hc.Transport = newProxyTransport()
|
|
||||||
|
|
||||||
var dnsErr *net.DNSError
|
var dnsErr *net.DNSError
|
||||||
err = solveTest(ctx, hc, _TEST_ONION)
|
err = solveTest(ctx, http.Client{Transport: newProxyTransport()}, _TEST_ONION)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.As(err, &dnsErr) {
|
if errors.As(err, &dnsErr) {
|
||||||
log.Println("Unable to resolve .onion domain. Make sure ALL_PROXY is set and tor is running.")
|
log.Println("Unable to resolve .onion domain. Make sure ALL_PROXY is set and tor is running.")
|
||||||
|
|||||||
93
http.go
93
http.go
@@ -1,6 +1,7 @@
|
|||||||
package cerberus
|
package cerberus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -18,7 +19,16 @@ var (
|
|||||||
ErrParseFailed = errors.New("Failed to parse challenge from HTML data tags.")
|
ErrParseFailed = errors.New("Failed to parse challenge from HTML data tags.")
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewChallenge(hc http.Client, host string) (Challenge, error) {
|
type ErrInvalidSolution struct {
|
||||||
|
s Solution
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrInvalidSolution) Error() string {
|
||||||
|
return fmt.Sprintf("Received 400 status when submitting solution: %+v", e.s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request new Tartarus challenge from provided host.
|
||||||
|
func NewChallenge(ctx context.Context, hc http.Client, host string) (Challenge, error) {
|
||||||
u, err := parseHost(host)
|
u, err := parseHost(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Challenge{}, err
|
return Challenge{}, err
|
||||||
@@ -34,13 +44,18 @@ func NewChallenge(hc http.Client, host string) (Challenge, error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := hc.Get(u.String())
|
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return Challenge{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := hc.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Challenge{}, err
|
return Challenge{}, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Check for 203 status
|
// Check for 203 status. A 203 indicates a redirect to a challenge page.
|
||||||
if resp.StatusCode != 203 {
|
if resp.StatusCode != 203 {
|
||||||
return Challenge{}, ErrNoRedirect
|
return Challenge{}, ErrNoRedirect
|
||||||
}
|
}
|
||||||
@@ -55,28 +70,39 @@ func NewChallenge(hc http.Client, host string) (Challenge, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Submit(hc http.Client, s Solution) (string, error) {
|
// Submit a Solution for a Challenge.
|
||||||
resp, err := postSolution(hc, s)
|
//
|
||||||
if err != nil {
|
// If redirect is empty, "/" is used as a sensible default.
|
||||||
return "", err
|
// Auth cookies get set automatically in http.Client's CookieJar.
|
||||||
|
// The *http.Response is provided to support more advanced setups. The auth token can also be found in its Body.
|
||||||
|
func Submit(ctx context.Context, hc http.Client, s Solution, redirect string) (*http.Response, error) {
|
||||||
|
if redirect != "" {
|
||||||
|
s.Redirect = redirect
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
return resp.Header.Get("Set-Cookie"), nil
|
resp, err := postSolution(ctx, hc, s)
|
||||||
}
|
|
||||||
|
|
||||||
func postSolution(hc http.Client, s Solution) (*http.Response, error) {
|
|
||||||
// Ensure the POST url parses properly before passing the string.
|
|
||||||
u, err := url.Parse(fmt.Sprintf("%s://%s/.ttrs/challenge", s.host.Scheme, s.host.Hostname()))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return hc.PostForm(u.String(), url.Values{
|
if s.Steps > 0 {
|
||||||
"salt": []string{s.Salt},
|
c, err := NewChallenge(ctx, hc, s.host.String())
|
||||||
"redirect": []string{s.Redirect},
|
if err != nil {
|
||||||
"nonce": []string{fmt.Sprint(s.Nonce)},
|
return nil, err
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// maybe useful later. idk.
|
||||||
|
// c.Steps = s.Steps
|
||||||
|
|
||||||
|
s, err := Solve(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Submit(ctx, hc, s, redirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseHost(addr string) (*url.URL, error) {
|
func parseHost(addr string) (*url.URL, error) {
|
||||||
@@ -85,12 +111,35 @@ func parseHost(addr string) (*url.URL, error) {
|
|||||||
addr = "https://" + addr
|
addr = "https://" + addr
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(addr)
|
return url.Parse(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func postSolution(ctx context.Context, hc http.Client, s Solution) (*http.Response, error) {
|
||||||
|
// Ensure the POST url parses properly before passing the string.
|
||||||
|
u, err := url.Parse(fmt.Sprintf("%s://%s/.ttrs/challenge", s.host.Scheme, s.host.Hostname()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return u, nil
|
reqBody := strings.NewReader(fmt.Sprintf(`salt=%s&redirect=%s&nonce=%d`, s.Salt, url.PathEscape(s.Redirect), s.Nonce))
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", u.String(), reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := hc.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Additionally verify failure from response JSON. Maybe include resp body in err type.
|
||||||
|
// Rejected solution response: status=400 body={"success":false,"reason":"invalid_solution","action":"retry"}
|
||||||
|
if resp.StatusCode == 400 {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return nil, &ErrInvalidSolution{s}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseTags(r io.Reader) (Challenge, error) {
|
func parseTags(r io.Reader) (Challenge, error) {
|
||||||
@@ -115,7 +164,7 @@ func parseTags(r io.Reader) (Challenge, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return c, ErrParseFailed
|
return c, ErrParseFailed
|
||||||
}
|
}
|
||||||
c.Steps = uint32(steps)
|
c.Steps = int8(steps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user