mirror of
https://gitgud.io/yats/cerberus.git
synced 2026-04-30 09:42:06 -04:00
Initial commit
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.*
|
||||||
|
!.gitignore
|
||||||
|
|
||||||
|
*.json
|
||||||
|
*.log
|
||||||
|
|
||||||
|
build/
|
||||||
|
data-dir-*/
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
GPATH
|
||||||
|
GRTAGS
|
||||||
|
GTAGS
|
||||||
14
README.md
Normal file
14
README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Cerberus
|
||||||
|
|
||||||
|
A fast 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.
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
Donations are always appreciated but never expected nor required:
|
||||||
|
|
||||||
|
XMR: `8BjCARiV2uB2gZTbbiMUetfRxcAYZgVM5fXxjEbpmb2nAu8ND1grazZ1EhMGdRqVerAtvEJeiy7SzA3SLXpg2CtRDtCAFfn`
|
||||||
|
|
||||||
|
[Other crypto (anonpay)](https://trocador.app/anonpay/?ticker_to=xmr&network_to=Mainnet&address=8BjCARiV2uB2gZTbbiMUetfRxcAYZgVM5fXxjEbpmb2nAu8ND1grazZ1EhMGdRqVerAtvEJeiy7SzA3SLXpg2CtRDtCAFfn&donation=True&description=SockChat+Donation&bgcolor=) ([Tor version](http://trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion/anonpay/?ticker_to=xmr&network_to=Mainnet&address=8BjCARiV2uB2gZTbbiMUetfRxcAYZgVM5fXxjEbpmb2nAu8ND1grazZ1EhMGdRqVerAtvEJeiy7SzA3SLXpg2CtRDtCAFfn&donation=True&description=SockChat+Donation&bgcolor=))
|
||||||
24
UNLICENSE
Normal file
24
UNLICENSE
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <http://unlicense.org/>
|
||||||
141
cerberus.go
Normal file
141
cerberus.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package cerberus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net/url"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/minio/sha256-simd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Challenge struct {
|
||||||
|
Salt string // Challenge salt from server.
|
||||||
|
Diff uint32 // Difficulty level.
|
||||||
|
Steps uint32 // Time limit for answer in minutes.
|
||||||
|
host *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
type Solution struct {
|
||||||
|
Salt string
|
||||||
|
Nonce uint32
|
||||||
|
Redirect string
|
||||||
|
|
||||||
|
Hash []byte
|
||||||
|
host *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brute force nonces until a valid solution is found.
|
||||||
|
func genHashes(ctx context.Context, c Challenge) <-chan Solution {
|
||||||
|
var (
|
||||||
|
out = make(chan Solution, 1)
|
||||||
|
sha = sha256.New()
|
||||||
|
nonce = rand.Uint32()
|
||||||
|
redirect = "/" // TODO: figure out non-homepage redirects.
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
sha.Write(fmt.Append(nil, c.Salt, nonce))
|
||||||
|
|
||||||
|
sol := Solution{
|
||||||
|
Salt: c.Salt,
|
||||||
|
Nonce: nonce,
|
||||||
|
Redirect: redirect,
|
||||||
|
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.
|
||||||
|
func checkZeros(diff uint32, hash []byte) bool {
|
||||||
|
var (
|
||||||
|
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.
|
||||||
|
|
||||||
|
mask uint8 // Mask to check remaining bits.
|
||||||
|
)
|
||||||
|
|
||||||
|
lh := uint32(len(hash))
|
||||||
|
// Check bounds for the loops found below.
|
||||||
|
if lh < nbytes || (rem > 0 && lh < nbytes+1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, we count the number of leading 0x0 bytes.
|
||||||
|
for i := range nbytes {
|
||||||
|
if b := hash[i]; b != 0x0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we don't have any more bits to check, return.
|
||||||
|
if rem == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create bitmask by setting a bit to 1 for each remaining bit to check.
|
||||||
|
// The mask is built from right-to-left and then shifted to the LHS of the octet.
|
||||||
|
for range rem {
|
||||||
|
mask <<= 1
|
||||||
|
mask += 1
|
||||||
|
}
|
||||||
|
// Shift 1s we just added to the LHS of the octet.
|
||||||
|
mask <<= 8 - rem
|
||||||
|
|
||||||
|
return hash[nbytes]&mask == 0x0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solve Challenge c. Returns Solution that can be submitted.
|
||||||
|
func Solve(ctx context.Context, c Challenge) (Solution, error) {
|
||||||
|
sol := make(chan Solution, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// A reasonable hardware-based job limiter.
|
||||||
|
// Use 1 worker per thread at most.
|
||||||
|
threads := runtime.NumCPU()
|
||||||
|
for i := 0; i < threads; i++ {
|
||||||
|
go 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return Solution{}, ctx.Err()
|
||||||
|
case s := <-sol:
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
100
cerberus_test.go
Normal file
100
cerberus_test.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package cerberus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
const _TEST_HOST = "kiwifarms.jp"
|
||||||
|
const _TEST_ONION = "kiwifarmsaaf4t2h7gc3dfc5ojhmqruw2nit3uejrpiagrxeuxiyxcyd.onion"
|
||||||
|
|
||||||
|
type errBadZeroCheck struct {
|
||||||
|
Diff uint32
|
||||||
|
Hash []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *errBadZeroCheck) Error() string {
|
||||||
|
return fmt.Sprintf("Zero check failed. Diff: %d, Hash: %+v\n", e.Diff, e.Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func solveTest(ctx context.Context, hc http.Client, host string) error {
|
||||||
|
connType := "clearnet"
|
||||||
|
if strings.HasSuffix(host, ".onion") {
|
||||||
|
connType = "tor"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Fetching new %s challenge...", connType)
|
||||||
|
c, err := NewChallenge(hc, host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("Challenge: %s, Difficulty: %d, Steps: %d\n", c.Salt, c.Diff, c.Steps)
|
||||||
|
|
||||||
|
s, err := Solve(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("Solution hash: %x, nonce: %d\n", s.Hash, s.Nonce)
|
||||||
|
|
||||||
|
a, err := Submit(hc, s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("Response: %s\n\n", a)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProxyTransport() *http.Transport {
|
||||||
|
pcd := proxy.FromEnvironment().(proxy.ContextDialer)
|
||||||
|
|
||||||
|
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
tr.DialContext = pcd.DialContext
|
||||||
|
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubmit(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
hc := http.Client{}
|
||||||
|
|
||||||
|
err := solveTest(ctx, hc, _TEST_HOST)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hc.Transport = newProxyTransport()
|
||||||
|
|
||||||
|
var dnsErr *net.DNSError
|
||||||
|
err = solveTest(ctx, hc, _TEST_ONION)
|
||||||
|
if err != nil {
|
||||||
|
if errors.As(err, &dnsErr) {
|
||||||
|
log.Println("Unable to resolve .onion domain. Make sure ALL_PROXY is set and tor is running.")
|
||||||
|
log.Println("Skipping...")
|
||||||
|
} else {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckZeros(t *testing.T) {
|
||||||
|
d, h := uint32(17), []byte{0, 0, 64, 128, 42}
|
||||||
|
if !checkZeros(d, h) {
|
||||||
|
t.Error(errBadZeroCheck{d, h})
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should fail (i.e. be false).
|
||||||
|
d, h = uint32(3), []byte{33, 130, 222, 88}
|
||||||
|
if checkZeros(d, h) {
|
||||||
|
t.Error(errBadZeroCheck{d, h})
|
||||||
|
}
|
||||||
|
}
|
||||||
13
go.mod
Normal file
13
go.mod
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module github.com/y-a-t-s/cerberus
|
||||||
|
|
||||||
|
go 1.25.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/minio/sha256-simd v1.0.1
|
||||||
|
golang.org/x/net v0.49.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
|
||||||
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
|
)
|
||||||
9
go.sum
Normal file
9
go.sum
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
|
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||||
|
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||||
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
129
http.go
Normal file
129
http.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package cerberus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
"golang.org/x/net/html/atom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoRedirect = errors.New("No redirect to challenge page.")
|
||||||
|
ErrParseFailed = errors.New("Failed to parse challenge from HTML data tags.")
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewChallenge(hc http.Client, host string) (Challenge, error) {
|
||||||
|
u, err := parseHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return Challenge{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update host url in case we get redirected across domains.
|
||||||
|
hc.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||||
|
rh := req.URL.Host
|
||||||
|
if rh != u.Host && strings.HasPrefix(rh, "kiwifarms") {
|
||||||
|
u.Host = rh
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := hc.Get(u.String())
|
||||||
|
if err != nil {
|
||||||
|
return Challenge{}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check for 203 status
|
||||||
|
if resp.StatusCode != 203 {
|
||||||
|
return Challenge{}, ErrNoRedirect
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kept separate from the return because of the defer.
|
||||||
|
c, err := parseTags(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return Challenge{}, err
|
||||||
|
}
|
||||||
|
c.host = u
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Submit(hc http.Client, s Solution) (string, error) {
|
||||||
|
resp, err := postSolution(hc, s)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return resp.Header.Get("Set-Cookie"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hc.PostForm(u.String(), url.Values{
|
||||||
|
"salt": []string{s.Salt},
|
||||||
|
"redirect": []string{s.Redirect},
|
||||||
|
"nonce": []string{fmt.Sprint(s.Nonce)},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHost(addr string) (*url.URL, error) {
|
||||||
|
// Guess https as protocol if one wasn't provided and hope it parses.
|
||||||
|
if !strings.Contains(addr, "://") {
|
||||||
|
addr = "https://" + addr
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTags(r io.Reader) (Challenge, error) {
|
||||||
|
c := Challenge{}
|
||||||
|
|
||||||
|
z := html.NewTokenizer(r)
|
||||||
|
for i := z.Next(); i != html.ErrorToken; i = z.Next() {
|
||||||
|
tk := z.Token()
|
||||||
|
if tk.DataAtom == atom.Html {
|
||||||
|
for _, a := range tk.Attr {
|
||||||
|
switch a.Key {
|
||||||
|
case "data-ttrs-challenge":
|
||||||
|
c.Salt = a.Val
|
||||||
|
case "data-ttrs-difficulty":
|
||||||
|
diff, err := strconv.Atoi(a.Val)
|
||||||
|
if err != nil {
|
||||||
|
return c, ErrParseFailed
|
||||||
|
}
|
||||||
|
c.Diff = uint32(diff)
|
||||||
|
case "data-ttrs-steps":
|
||||||
|
steps, err := strconv.Atoi(a.Val)
|
||||||
|
if err != nil {
|
||||||
|
return c, ErrParseFailed
|
||||||
|
}
|
||||||
|
c.Steps = uint32(steps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Salt == "" {
|
||||||
|
return c, ErrParseFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user