diff --git a/.gitignore b/.gitignore index 29b636a..c1bd969 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea -*.iml \ No newline at end of file +*.iml +*.sock diff --git a/DESIGN.md b/DESIGN.md index bdf64cd..a741632 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -61,4 +61,9 @@ Cookie https://challenges.cloudflare.com/turnstile/v0/g/313d8a27/api.js?onload=URXdVe4&render=explicit -https://challenges.cloudflare.com/cdn-cgi/challenge-platform/h/g/orchestrate/chl_api/v1?ray=7fb72b3cba9bc4a4 \ No newline at end of file +https://challenges.cloudflare.com/cdn-cgi/challenge-platform/h/g/orchestrate/chl_api/v1?ray=7fb72b3cba9bc4a4 + + +## HAProxy-Protection + +https://gitgud.io/fatchan/haproxy-protection/-/blob/a6f3613b6a4e41860f4916de508de80e47e2ee98/src/js/worker.js \ No newline at end of file diff --git a/README.md b/README.md index 7fb834a..081d3de 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,12 @@ the other side. With Berghain in charge, you can be confident that your backend is reserved for the true VIPs of the internet, keeping out any uninvited guests. It's like the bouncer of the web world, ensuring that your resources are reserved for the browsers that really know how to dance! + +## Supported CAPTCHAs + +- None (Simple JS execute) +- POW +- Simple Captcha (Including Sound) +- [hCaptcha](https://www.hcaptcha.com/) +- [reCatpcha](https://developers.google.com/recaptcha?hl=de) +- [Turnstile](https://developers.cloudflare.com/turnstile/) \ No newline at end of file diff --git a/berghain.go b/berghain.go index 6e9a80b..9304e96 100644 --- a/berghain.go +++ b/berghain.go @@ -1,6 +1,10 @@ package berghain import ( + "crypto/hmac" + "crypto/sha256" + "hash" + "sync" "time" ) @@ -10,8 +14,32 @@ type LevelConfig struct { } type Berghain struct { - Secret []byte Levels []*LevelConfig + + secret []byte + hmac sync.Pool +} + +var hashAlgo = sha256.New + +func NewBerghain(secret []byte) *Berghain { + return &Berghain{ + secret: secret, + hmac: sync.Pool{ + New: func() any { + return NewZeroHasher(hmac.New(hashAlgo, secret)) + }, + }, + } +} + +func (b *Berghain) acquireHMAC() hash.Hash { + return b.hmac.Get().(hash.Hash) +} + +func (b *Berghain) releaseHMAC(h hash.Hash) { + h.Reset() + b.hmac.Put(h) } func (b *Berghain) LevelConfig(level uint8) *LevelConfig { diff --git a/berghain_test.go b/berghain_test.go new file mode 100644 index 0000000..8acb742 --- /dev/null +++ b/berghain_test.go @@ -0,0 +1,47 @@ +package berghain + +import ( + "crypto/rand" + "net/netip" + "testing" + "time" +) + +func generateSecret(tb testing.TB) []byte { + tb.Helper() + b := make([]byte, 32) + _, err := rand.Read(b) + if err != nil { + tb.Fatal(err) + } + return b +} + +func TestBerghain(t *testing.T) { + bh := NewBerghain(generateSecret(t)) + bh.Levels = []*LevelConfig{ + { + Duration: time.Minute, + Type: ValidationTypeNone, + }, + } + + req := AcquireValidatorRequest() + req.Identifier = &RequestIdentifier{ + SrcAddr: netip.MustParseAddr("1.2.3.4"), + Host: []byte("example.com"), + Level: 1, + } + req.Method = "GET" + + resp := AcquireValidatorResponse() + err := bh.LevelConfig(req.Identifier.Level).Type.RunValidator(bh, req, resp) + if err != nil { + t.Errorf("validator failed: %v", err) + } + + err = bh.IsValidCookie(*req.Identifier, resp.Token.ReadBytes()) + if err != nil { + t.Errorf("invalid cookie: %v", err) + } +} diff --git a/cmd/spop/config.go b/cmd/spop/config.go index b2444f9..9d2b0c6 100644 --- a/cmd/spop/config.go +++ b/cmd/spop/config.go @@ -19,13 +19,8 @@ type Config struct { type Secret []byte -func (s *Secret) MarshalYAML() (interface{}, error) { - return base64.StdEncoding.EncodeToString(*s), nil -} - func (s *Secret) UnmarshalYAML(node *yaml.Node) error { - value := node.Value - ba, err := base64.StdEncoding.DecodeString(value) + ba, err := base64.StdEncoding.DecodeString(node.Value) if err != nil { return err } @@ -36,12 +31,11 @@ func (s *Secret) UnmarshalYAML(node *yaml.Node) error { type FrontendConfig []LevelConfig func (fc FrontendConfig) AsBerghain(s []byte) *berghain.Berghain { - var b berghain.Berghain - b.Secret = s + b := berghain.NewBerghain(s) for _, c := range fc { b.Levels = append(b.Levels, c.AsLevelConfig()) } - return &b + return b } type LevelConfig struct { diff --git a/cmd/spop/config.yaml b/cmd/spop/config.yaml index 962d3fd..942d23a 100644 --- a/cmd/spop/config.yaml +++ b/cmd/spop/config.yaml @@ -1,11 +1,11 @@ secret: JMal0XJRROOMsMdPqggG2tR56CTkpgN3r47GgUN/WSQ= default: - - duration: 30s + - duration: 24h type: none - - duration: 20s + - duration: 1h type: pow - - duration: 10s + - duration: 60s type: pow frontend: diff --git a/cmd/spop/frontend.go b/cmd/spop/frontend.go index c8ed5ef..e8c06ed 100644 --- a/cmd/spop/frontend.go +++ b/cmd/spop/frontend.go @@ -2,7 +2,6 @@ package main import ( "context" - "errors" "fmt" "log" "net/http" @@ -84,7 +83,7 @@ func (f *frontend) HandleSPOEValidate(_ context.Context, w *encoding.ActionWrite readExpectedKVEntry(m, k, "cookie") err := f.bh.IsValidCookie(ri, k.ValueBytes()) - if err != nil && !errors.Is(err, berghain.ErrInvalidCookieLength) { + if err != nil && debug { log.Println(fmt.Sprintf("IsValidCookie: %v", err)) } isValidCookie := err == nil @@ -152,7 +151,7 @@ func (f *frontend) HandleSPOEChallenge(_ context.Context, w *encoding.ActionWrit panic(fmt.Errorf("validator failed: %v", err)) } - _ = w.SetStringBytes(encoding.VarScopeTransaction, "response", resp.Body) + _ = w.SetStringBytes(encoding.VarScopeTransaction, "response", resp.Body.ReadBytes()) if resp.Token.Len() > 0 { _ = w.SetStringBytes(encoding.VarScopeTransaction, "token", resp.Token.ReadBytes()) } diff --git a/cmd/spop/main.go b/cmd/spop/main.go index 8a80ae9..7c03817 100644 --- a/cmd/spop/main.go +++ b/cmd/spop/main.go @@ -18,14 +18,19 @@ import ( ) var configPath string +var debug bool func main() { flag.StringVar(&configPath, "config", "config.yaml", "Config file to load") + flag.BoolVar(&debug, "debug", false, "Enable debug mode") flag.Parse() log.SetFlags(log.LstdFlags | log.Lshortfile) - go http.ListenAndServe(":9001", nil) + // In debug mode start a http server to serve the default pprof handlers. + if debug { + go http.ListenAndServe(":9001", nil) + } c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) diff --git a/go.mod b/go.mod index 354d45f..61d2b15 100644 --- a/go.mod +++ b/go.mod @@ -9,4 +9,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require github.com/adrianbrad/queue v1.2.1 // indirect +require ( + github.com/adrianbrad/queue v1.2.1 // indirect +) diff --git a/go.sum b/go.sum index 79a70e4..148a09a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,12 @@ github.com/adrianbrad/queue v1.2.1 h1:CEVsjFQyuR0s5Hty/HJGWBZHsJ3KMmii0kEgLeam/mk= github.com/adrianbrad/queue v1.2.1/go.mod h1:wYiPC/3MPbyT45QHLrPR4zcqJWPePubM1oEP/xTwhUs= +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/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/hasher.go b/hasher.go new file mode 100644 index 0000000..b532c55 --- /dev/null +++ b/hasher.go @@ -0,0 +1,43 @@ +package berghain + +import "hash" + +// zeroHasher provides a wrapper to create zero allocation hashes. +type zeroHasher struct { + h hash.Hash + buf []byte +} + +func NewZeroHasher(h hash.Hash) hash.Hash { + return &zeroHasher{ + h: h, + buf: make([]byte, 0, h.Size()), + } +} + +func (zh *zeroHasher) Sum(b []byte) []byte { + if b != nil { + panic("zeroHasher does not support any parameter for Sum()") + } + if len(zh.buf) != 0 { + panic("invalid buffer state") + } + return zh.h.Sum(zh.buf) +} + +func (zh *zeroHasher) Size() int { + return zh.h.Size() +} + +func (zh *zeroHasher) BlockSize() int { + return zh.h.BlockSize() +} + +func (zh *zeroHasher) Write(p []byte) (int, error) { + return zh.h.Write(p) +} + +func (zh *zeroHasher) Reset() { + zh.buf = zh.buf[:0] + zh.h.Reset() +} diff --git a/identity.go b/identity.go index 32e41e5..542b704 100644 --- a/identity.go +++ b/identity.go @@ -2,22 +2,20 @@ package berghain import ( "bytes" - "crypto/hmac" - "crypto/sha256" "encoding/binary" "encoding/hex" "fmt" "net/netip" "sync" - "time" "github.com/dropmorepackets/haproxy-go/pkg/buffer" ) // 03|3778206500000000|1edb6858c727c3519825ac8a8777d94282fe476c4d3e0b6a7247dc5fa2d4ed7f // uint8 + uint64 + sha256 (32byte) = 41 byte -// encoded into hex = 82 byte -// two spacers = 2 byte +// this encoded into hex = 82 byte +// adding two spacers = 2 byte +// total = 82 bytes const encodedCookieSize = 84 var cookieBufferPool = sync.Pool{ @@ -45,35 +43,55 @@ func (ri RequestIdentifier) ToCookie(b *Berghain, enc *buffer.SliceBuffer) error raw := AcquireCookieBuffer() defer ReleaseCookieBuffer(raw) - h := hmac.New(sha256.New, b.Secret) + h := b.acquireHMAC() + defer b.releaseHMAC(h) + + // Write Host to the hash if _, err := h.Write(ri.Host); err != nil { return err } - if _, err := h.Write(ri.SrcAddr.AsSlice()); err != nil { + + // Write SrcAddr first to the buffer and then to the hash. + // netip.AsSlice does an allocation we want to avoid. + addrSlice := raw.WriteBytes()[:0] // reset capacity to zero + addrSlice = ri.SrcAddr.AppendTo(addrSlice) + if _, err := h.Write(addrSlice); err != nil { return err } + raw.Reset() + // Write Level to the buffer and to the hash raw.WriteNBytes(1)[0] = ri.Level if _, err := h.Write(raw.ReadBytes()); err != nil { return err } - hex.Encode(enc.WriteNBytes(hex.EncodedLen(raw.Len())), raw.ReadBytes()) + + // Write the hex encoded level to the buffer and append this to the output. + levelArea := enc.WriteNBytes(hex.EncodedLen(raw.Len())) + hex.Encode(levelArea, raw.ReadBytes()) raw.Reset() + // Write a spacer to the output. enc.WriteNBytes(1)[0] = '|' - expireAt := time.Now().Add(b.LevelConfig(ri.Level).Duration) + // Calculate the expiration of the cookie, write it to the buffer and hash. + expireAt := tc.Now().Add(b.LevelConfig(ri.Level).Duration) binary.LittleEndian.PutUint64(raw.WriteNBytes(8), uint64(expireAt.Unix())) if _, err := h.Write(raw.ReadBytes()); err != nil { return err } - hex.Encode(enc.WriteNBytes(hex.EncodedLen(raw.Len())), raw.ReadBytes()) + + // Write the hex encoded expiration to the output. + expireArea := enc.WriteNBytes(hex.EncodedLen(raw.Len())) + hex.Encode(expireArea, raw.ReadBytes()) raw.Reset() + // Write another spacer to the output. enc.WriteNBytes(1)[0] = '|' - sum := h.Sum(nil) - hex.Encode(enc.WriteNBytes(hex.EncodedLen(len(sum))), sum) + // Finally generate the sum and write that with hex encoding to the output. + sumArea := enc.WriteNBytes(hex.EncodedLen(h.Size())) + hex.Encode(sumArea, h.Sum(nil)) return nil } @@ -93,19 +111,28 @@ func (b *Berghain) IsValidCookie(ri RequestIdentifier, cookie []byte) error { dec := AcquireCookieBuffer() defer ReleaseCookieBuffer(dec) - h := hmac.New(sha256.New, b.Secret) + h := b.acquireHMAC() + defer b.releaseHMAC(h) + if _, err := h.Write(ri.Host); err != nil { return err } - if _, err := h.Write(ri.SrcAddr.AsSlice()); err != nil { + + // Write SrcAddr first to the buffer and then to the hash. + // netip.AsSlice does an allocation we want to avoid. + addrSlice := dec.WriteBytes()[:0] // reset capacity to zero + addrSlice = ri.SrcAddr.AppendTo(addrSlice) + if _, err := h.Write(addrSlice); err != nil { return err } + dec.Reset() - coo := buffer.NewSliceBufferWithSlice(cookie) + cookieBuf := buffer.NewSliceBufferWithSlice(cookie) - cookieLevel := coo.ReadNBytes(hex.EncodedLen(1)) - coo.AdvanceR(1) // Separator - if _, err := hex.Decode(dec.WriteNBytes(hex.DecodedLen(len(cookieLevel))), cookieLevel); err != nil { + cookieLevel := cookieBuf.ReadNBytes(hex.EncodedLen(1)) + cookieBuf.AdvanceR(1) // Separator + levelArea := dec.WriteNBytes(hex.DecodedLen(len(cookieLevel))) + if _, err := hex.Decode(levelArea, cookieLevel); err != nil { return err } @@ -119,14 +146,15 @@ func (b *Berghain) IsValidCookie(ri RequestIdentifier, cookie []byte) error { } dec.Reset() - cookieExpiration := coo.ReadNBytes(hex.EncodedLen(8)) - coo.AdvanceR(1) // Separator - if _, err := hex.Decode(dec.WriteNBytes(hex.DecodedLen(len(cookieExpiration))), cookieExpiration); err != nil { + cookieExpiration := cookieBuf.ReadNBytes(hex.EncodedLen(8)) + cookieBuf.AdvanceR(1) // Separator + expirArea := dec.WriteNBytes(hex.DecodedLen(len(cookieExpiration))) + if _, err := hex.Decode(expirArea, cookieExpiration); err != nil { return err } // Untrusted input is decoded and compared! - if uint64(time.Now().Unix()) > binary.LittleEndian.Uint64(dec.ReadBytes()) { + if uint64(tc.Now().Unix()) > binary.LittleEndian.Uint64(dec.ReadBytes()) { return ErrCookieExpired } @@ -135,8 +163,9 @@ func (b *Berghain) IsValidCookie(ri RequestIdentifier, cookie []byte) error { } dec.Reset() - cookieSum := coo.ReadBytes() - if _, err := hex.Decode(dec.WriteNBytes(hex.DecodedLen(len(cookieSum))), cookieSum); err != nil { + cookieSum := cookieBuf.ReadBytes() + sumArea := dec.WriteNBytes(hex.DecodedLen(len(cookieSum))) + if _, err := hex.Decode(sumArea, cookieSum); err != nil { return err } diff --git a/identity_test.go b/identity_test.go new file mode 100644 index 0000000..3b2412e --- /dev/null +++ b/identity_test.go @@ -0,0 +1,63 @@ +package berghain + +import ( + "net/netip" + "testing" + "time" +) + +func BenchmarkRequestIdentifier_ToCookie(b *testing.B) { + bh := NewBerghain(generateSecret(b)) + + bh.Levels = []*LevelConfig{ + { + Duration: time.Minute, + Type: ValidationTypeNone, + }, + } + + var ri = RequestIdentifier{ + SrcAddr: netip.MustParseAddr("1.2.3.4"), + Host: []byte("example.com"), + Level: 1, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + cb := AcquireCookieBuffer() + if err := ri.ToCookie(bh, cb); err != nil { + b.Fatal(err) + } + ReleaseCookieBuffer(cb) + } +} + +func BenchmarkRequestIdentifier_IsValidCookie(b *testing.B) { + bh := NewBerghain(generateSecret(b)) + + bh.Levels = []*LevelConfig{ + { + Duration: time.Minute, + Type: ValidationTypeNone, + }, + } + + var ri = RequestIdentifier{ + SrcAddr: netip.MustParseAddr("1.2.3.4"), + Host: []byte("example.com"), + Level: 1, + } + + cb := AcquireCookieBuffer() + defer ReleaseCookieBuffer(cb) + if err := ri.ToCookie(bh, cb); err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := bh.IsValidCookie(ri, cb.ReadBytes()); err != nil { + b.Fatal(err) + } + } +} diff --git a/time.go b/time.go new file mode 100644 index 0000000..4ed19ef --- /dev/null +++ b/time.go @@ -0,0 +1,34 @@ +package berghain + +import ( + "sync/atomic" + realTime "time" +) + +// timeCache stores a pointer to a time instance +// that is accurate to about one second. +type timeCache struct { + p atomic.Pointer[realTime.Time] +} + +func (tc *timeCache) Now() realTime.Time { + return *tc.p.Load() +} + +var tc = func() (tc timeCache) { + refresh := func() { + n := realTime.Now() + tc.p.Store(&n) + } + + refresher := func() { + for { + refresh() + realTime.Sleep(realTime.Second) + } + } + + refresh() + go refresher() + return +}() diff --git a/validator_none.go b/validator_none.go new file mode 100644 index 0000000..27755c5 --- /dev/null +++ b/validator_none.go @@ -0,0 +1,9 @@ +package berghain + +const validatorNoneResponse = `{"t": 0}` + +func validatorNone(b *Berghain, req *ValidatorRequest, resp *ValidatorResponse) error { + copy(resp.Body.WriteNBytes(len(validatorNoneResponse)), validatorNoneResponse) + + return req.Identifier.ToCookie(b, resp.Token) +} diff --git a/validator_none_test.go b/validator_none_test.go new file mode 100644 index 0000000..82ecff9 --- /dev/null +++ b/validator_none_test.go @@ -0,0 +1,43 @@ +package berghain + +import ( + "bytes" + "net/http" + "net/netip" + "testing" + "time" +) + +func Test_validatorNone(t *testing.T) { + bh := NewBerghain(generateSecret(t)) + + bh.Levels = []*LevelConfig{ + { + Duration: time.Minute, + Type: ValidationTypeNone, + }, + } + + req, resp := AcquireValidatorRequest(), AcquireValidatorResponse() + req.Identifier = &RequestIdentifier{ + SrcAddr: netip.MustParseAddr("1.2.3.4"), + Host: []byte("example.com"), + Level: 1, + } + req.Method = http.MethodGet + + err := validatorNone(bh, req, resp) + if err != nil { + t.Errorf("validator failed: %v", err) + } + + if !bytes.Equal(resp.Body.ReadBytes(), []byte(validatorNoneResponse)) { + t.Errorf("invalid response: %s != %s", validatorNoneResponse, resp.Body.ReadBytes()) + } + + err = bh.IsValidCookie(*req.Identifier, resp.Token.ReadBytes()) + if err != nil { + t.Errorf("invalid cookie: %v", err) + } + +} diff --git a/validator_pow.go b/validator_pow.go new file mode 100644 index 0000000..925a24d --- /dev/null +++ b/validator_pow.go @@ -0,0 +1,148 @@ +package berghain + +import ( + "bytes" + "encoding/hex" + "fmt" + "hash" + "math/rand" + "net/http" + "sync" + + "github.com/dropmorepackets/haproxy-go/pkg/buffer" +) + +var randPool = sync.Pool{ + New: func() interface{} { + return rand.New(rand.NewSource(rand.Int63())) + }, +} + +func writeRandomASCIIBytes(b []byte) { + r := randPool.Get().(*rand.Rand) + defer randPool.Put(r) + + r.Read(b) + + for i := 0; i < len(b); i++ { + b[i] = 65 + (b[i] % 25) + } +} + +var sha256Pool = sync.Pool{ + New: func() any { + return NewZeroHasher(hashAlgo()) + }, +} + +func acquireSHA256() hash.Hash { + return sha256Pool.Get().(hash.Hash) +} + +func releaseSHA256(h hash.Hash) { + h.Reset() + sha256Pool.Put(h) +} + +type powValidator struct { +} + +const ( + validatorPOWRandom = "00000000" + validatorPOWHash = "0000000000000000000000000000000000000000000000000000000000000000" + validatorPOWChallengeTemplate = `{"t": 1, "r": "` + validatorPOWRandom + `", "s": "` + validatorPOWHash + `"}` + validatorPOWMinSolutionLength = len(validatorPOWRandom) + 1 + len(validatorPOWHash) + 1 + 1 +) + +// This prevents invalid template strings by validatoring them on start +var _ = func() bool { + h := hashAlgo() + if len(validatorPOWHash) != hex.EncodedLen(h.Size()) { + panic("invalid pow hash length") + } + return true +}() + +func (powValidator) onNew(b *Berghain, req *ValidatorRequest, resp *ValidatorResponse) error { + + h := b.acquireHMAC() + defer b.releaseHMAC(h) + + copy(resp.Body.WriteBytes(), validatorPOWChallengeTemplate) + resp.Body.AdvanceW(15) + randArea := resp.Body.WriteNBytes(len(validatorPOWRandom)) + resp.Body.AdvanceW(9) + hexArea := resp.Body.WriteNBytes(hex.EncodedLen(h.Size())) + resp.Body.AdvanceW(2) + + writeRandomASCIIBytes(randArea) + h.Write(randArea) + hex.Encode(hexArea, h.Sum(nil)) + + return nil +} + +func (powValidator) isValid(b *Berghain, req *ValidatorRequest, resp *ValidatorResponse) bool { + // req.Body should look like this: + // NCUEKLGC-5d2702c936458bf9b962617673f0825ee3b51a84a42fc9f591d8c67516442a2f-61764 + if len(req.Body) <= validatorPOWMinSolutionLength { + // invalid solution data + return false + } + + body := buffer.NewSliceBufferWithSlice(req.Body) + + randArea := body.ReadNBytes(len(validatorPOWRandom)) + body.AdvanceR(1) // Skip padding character + sumArea := body.ReadNBytes(len(validatorPOWHash)) + body.AdvanceR(1) // Skip padding character + solArea := body.ReadBytes() + + h := b.acquireHMAC() + defer b.releaseHMAC(h) + + h.Write(randArea) + + // we use the response body temporarily as a buffer + defer resp.Body.Reset() + + ourSum := resp.Body.WriteNBytes(hex.EncodedLen(h.Size())) + hex.Encode(ourSum, h.Sum(nil)) + + if !bytes.Equal(ourSum, sumArea) { + // invalid hash in solution + return false + } + + sha := acquireSHA256() + defer releaseSHA256(sha) + + sha.Write(randArea) + sha.Write(solArea) + sum := sha.Sum(nil) + + if !bytes.HasPrefix(sum, []byte{0x00, 0x00}) { + // invalid solution + return false + } + + return true +} + +var errInvalidSolution = fmt.Errorf("invalid solution") + +func validatorPOW(b *Berghain, req *ValidatorRequest, resp *ValidatorResponse) error { + var p powValidator + + switch req.Method { + case http.MethodPost: + if p.isValid(b, req, resp) { + return req.Identifier.ToCookie(b, resp.Token) + } + return errInvalidSolution + case http.MethodGet: + return p.onNew(b, req, resp) + } + + return errInvalidMethod +} diff --git a/validator_pow_test.go b/validator_pow_test.go new file mode 100644 index 0000000..8523ac7 --- /dev/null +++ b/validator_pow_test.go @@ -0,0 +1,174 @@ +package berghain + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/netip" + "strconv" + "testing" + "time" +) + +func solvePOW(tb testing.TB, b []byte) ([]byte, error) { + tb.Helper() + + type powChallenge struct { + T int `json:"t"` + R string `json:"r"` + S string `json:"s"` + } + + var p powChallenge + if err := json.NewDecoder(bytes.NewReader(b)).Decode(&p); err != nil { + return nil, err + } + + if p.T != 1 { + return nil, fmt.Errorf("invalid challenge type: %d", p.T) + } + + h := NewZeroHasher(hashAlgo()) + for i := 0; true; i++ { + h.Write([]byte(p.R)) + + is := strconv.Itoa(i) + h.Write([]byte(is)) + + if bytes.HasPrefix(h.Sum(nil), []byte{0x00, 0x00}) { + solution := p.R + "-" + p.S + "-" + is + return []byte(solution), nil + } + + h.Reset() + } + panic("unreachable") +} + +func Test_validatorPOW(t *testing.T) { + bh := NewBerghain(generateSecret(t)) + + bh.Levels = []*LevelConfig{ + { + Duration: time.Minute, + Type: ValidationTypePOW, + }, + } + + req, resp := AcquireValidatorRequest(), AcquireValidatorResponse() + req.Identifier = &RequestIdentifier{ + SrcAddr: netip.MustParseAddr("1.2.3.4"), + Host: []byte("example.com"), + Level: 1, + } + req.Method = http.MethodGet + + if err := validatorPOW(bh, req, resp); err != nil { + t.Errorf("validator failed: %v", err) + } + + if resp.Body.Len() != len(validatorPOWChallengeTemplate) { + t.Errorf("invalid challenge response length: %d != %d", len(validatorPOWChallengeTemplate), resp.Body.Len()) + } + + solution, err := solvePOW(t, resp.Body.ReadBytes()) + if err != nil { + t.Errorf("while solving pow: %v", err) + } + + // Do another request but this time as POST and with the solution. + req.Method = http.MethodPost + req.Body = solution + if err := validatorPOW(bh, req, resp); err != nil { + t.Errorf("validator failed: %v", err) + } + + if resp.Body.Len() != 0 { + t.Errorf("invalid response length: %d != %d", 0, resp.Body.Len()) + } + + err = bh.IsValidCookie(*req.Identifier, resp.Token.ReadBytes()) + if err != nil { + t.Errorf("invalid cookie: %v", err) + } +} + +func Benchmark_validatorPOW_GET(b *testing.B) { + bh := NewBerghain(generateSecret(b)) + + bh.Levels = []*LevelConfig{ + { + Duration: time.Minute, + Type: ValidationTypePOW, + }, + } + + ri := RequestIdentifier{ + SrcAddr: netip.MustParseAddr("1.2.3.4"), + Host: []byte("example.com"), + Level: 1, + } + + req := AcquireValidatorRequest() + defer ReleaseValidatorRequest(req) + + req.Identifier = &ri + req.Method = http.MethodGet + + b.ResetTimer() + for i := 0; i < b.N; i++ { + resp := AcquireValidatorResponse() + if err := validatorPOW(bh, req, resp); err != nil { + b.Errorf("validator failed: %v", err) + } + + ReleaseValidatorResponse(resp) + } +} + +func Benchmark_validatorPOW_POST(b *testing.B) { + bh := NewBerghain(generateSecret(b)) + + bh.Levels = []*LevelConfig{ + { + Duration: time.Minute, + Type: ValidationTypePOW, + }, + } + + ri := RequestIdentifier{ + SrcAddr: netip.MustParseAddr("1.2.3.4"), + Host: []byte("example.com"), + Level: 1, + } + + req, resp := AcquireValidatorRequest(), AcquireValidatorResponse() + defer ReleaseValidatorRequest(req) + req.Method = http.MethodGet + + if err := validatorPOW(bh, req, resp); err != nil { + b.Errorf("validator failed: %v", err) + } + + solution, err := solvePOW(b, resp.Body.ReadBytes()) + if err != nil { + b.Errorf("while solving pow: %v", err) + } + + ReleaseValidatorResponse(resp) + req.Identifier = &ri + req.Method = http.MethodPost + req.Body = solution + + b.ResetTimer() + for i := 0; i < b.N; i++ { + resp = AcquireValidatorResponse() + + if err := validatorPOW(bh, req, resp); err != nil { + b.Errorf("validator failed: %v", err) + } + + ReleaseValidatorResponse(resp) + } +} diff --git a/validators.go b/validators.go index fe3df61..77cc495 100644 --- a/validators.go +++ b/validators.go @@ -1,6 +1,7 @@ package berghain import ( + "fmt" "sync" "github.com/dropmorepackets/haproxy-go/pkg/buffer" @@ -15,14 +16,14 @@ const ( ) type ValidatorResponse struct { - Body []byte + Body *buffer.SliceBuffer Token *buffer.SliceBuffer } var validatorResponsePool = sync.Pool{ New: func() any { return &ValidatorResponse{ - Body: nil, + Body: buffer.NewSliceBuffer(1024), //TODO: use const for size Token: AcquireCookieBuffer(), } }, @@ -34,6 +35,7 @@ func AcquireValidatorResponse() *ValidatorResponse { func ReleaseValidatorResponse(v *ValidatorResponse) { v.Token.Reset() + v.Body.Reset() validatorResponsePool.Put(v) } @@ -65,13 +67,10 @@ func (v ValidationType) RunValidator(b *Berghain, req *ValidatorRequest, resp *V case ValidationTypeNone: return validatorNone(b, req, resp) case ValidationTypePOW: - return nil + return validatorPOW(b, req, resp) default: panic("unknown validation type") } } -func validatorNone(b *Berghain, req *ValidatorRequest, resp *ValidatorResponse) error { - resp.Body = []byte(`{"t": 0}`) - return req.Identifier.ToCookie(b, resp.Token) -} +var errInvalidMethod = fmt.Errorf("invalid method") diff --git a/web/src/main.js b/web/src/main.js index d868b30..e1336da 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -6,7 +6,33 @@ const fetchChallenge = async () => { } const challengeNone = async () => { - + +} + +const nativeHash = async (data, method) => { + const buffer = new TextEncoder().encode(data); + const hashBuffer = await crypto.subtle.digest(method, buffer) + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); +} + + +const challengePOW = async (challenge) => { + let hash, i; + for (i = 0; true; i++) { + hash = await nativeHash(challenge.r + i.toString(), 'sha-256') + if (hash.startsWith("0000")) { + console.log(challenge) + console.log(i) + console.log(hash) + break; + } + } + + return fetch("/cdn-cgi/challenge-platform/challenge", { + method: "POST", + body: challenge.r + "-" + challenge.s + "-" + i.toString() + }); } const reload = window.location.reload; @@ -20,14 +46,17 @@ const doChallenge = async () => { case 0: challengeFunc = challengeNone break; + case 1: + challengeFunc = challengePOW + break; default: challengeFunc = challengeUnknown } - await challengeNone(challenge) + return challengeFunc(challenge) } -const showCountdown = async(seconds) => { +const showCountdown = async (seconds) => { const secondsTmp = seconds; const countdown = document.querySelector(".captcha-container .countdown-number"); @@ -41,7 +70,7 @@ const showCountdown = async(seconds) => { setTimeout(() => { clearInterval(timer); - }, seconds*1000); + }, seconds * 1000); } const onContentLoaded = async () => { @@ -49,14 +78,14 @@ const onContentLoaded = async () => { document.getElementById('no-cookie').style.display = "block"; } - await showCountdown(5); - await doChallenge(); + + await showCountdown(5); } window.addEventListener("DOMContentLoaded", onContentLoaded) const cookieName = "berghain"; -if (getParameterByName(cookieName) !== null && getCookie(cookieName) === undefined) { - document.getElementById('loop-warning').style.display = "block"; -} +// if (getParameterByName(cookieName) !== null && getCookie(cookieName) === undefined) { +// document.getElementById('loop-warning').style.display = "block"; +// }