-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathvalidator_captcha.go
More file actions
154 lines (131 loc) · 4.41 KB
/
Copy pathvalidator_captcha.go
File metadata and controls
154 lines (131 loc) · 4.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package berghain
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type captchaValidator struct {
}
// Providers accept tokens of a few kilobytes; reCAPTCHA tokens are the
// largest at around two to three kilobytes.
const validatorCaptchaMaxTokenLength = 8 << 10
// The provider verdict is a small JSON document; limit reads defensively.
const validatorCaptchaMaxVerdictLength = 64 << 10
var (
errCaptchaRejected = fmt.Errorf("captcha token rejected")
errCaptchaHostMismatch = fmt.Errorf("captcha hostname mismatch")
errCaptchaUnavailable = fmt.Errorf("captcha provider unavailable")
)
// captchaChallengeBody returns the static challenge response for a captcha
// level. Unlike POW, the challenge embeds no per-request state: the security
// binding happens when the solved token is exchanged for a cookie.
func (lc *LevelConfig) captchaChallengeBody() []byte {
lc.captchaBodyOnce.Do(func() {
body, err := json.Marshal(struct {
Countdown int `json:"c"`
Type int `json:"t"`
Sitekey string `json:"k"`
}{
Countdown: lc.Countdown,
Type: int(lc.Type) - 1, // the web protocol counts types from zero
Sitekey: lc.CaptchaSitekey,
})
if err != nil {
panic(err)
}
lc.captchaBody = body
})
return lc.captchaBody
}
func (captchaValidator) onNew(b *Berghain, req *ValidatorRequest, resp *ValidatorResponse) error {
lc := b.LevelConfig(req.Identifier.Level)
body := lc.captchaChallengeBody()
if len(body) > len(resp.Body.WriteBytes()) {
return fmt.Errorf("captcha challenge body exceeds response buffer: %d bytes", len(body))
}
copy(resp.Body.WriteNBytes(len(body)), body)
return nil
}
func (captchaValidator) isValid(b *Berghain, req *ValidatorRequest, _ *ValidatorResponse) error {
if len(req.Body) == 0 {
return ErrEmpty
}
if len(req.Body) > validatorCaptchaMaxTokenLength {
return ErrInvalidLength
}
lc := b.LevelConfig(req.Identifier.Level)
// All three providers implement the same siteverify contract:
// hCaptcha and Turnstile clone the reCAPTCHA API on purpose.
verifyURL := lc.CaptchaVerifyURL
if verifyURL == "" {
switch lc.Type {
case ValidationTypeTurnstile:
verifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
case ValidationTypeHCaptcha:
verifyURL = "https://api.hcaptcha.com/siteverify"
case ValidationTypeReCaptcha:
verifyURL = "https://www.google.com/recaptcha/api/siteverify"
}
}
form := url.Values{
"secret": {lc.CaptchaSecret},
"response": {string(req.Body)},
"remoteip": {req.Identifier.SrcAddr.String()},
}
httpResp, err := b.httpClient().PostForm(verifyURL, form)
if err != nil {
// Fail closed: the client is told the challenge failed and can retry.
return fmt.Errorf("%w: %v", errCaptchaUnavailable, err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("%w: status %d", errCaptchaUnavailable, httpResp.StatusCode)
}
var verdict struct {
Success bool `json:"success"`
Hostname string `json:"hostname"`
ErrorCodes []string `json:"error-codes"`
}
if err := json.NewDecoder(io.LimitReader(httpResp.Body, validatorCaptchaMaxVerdictLength)).Decode(&verdict); err != nil {
return fmt.Errorf("%w: %v", errCaptchaUnavailable, err)
}
if !verdict.Success {
return fmt.Errorf("%w: %s", errCaptchaRejected, strings.Join(verdict.ErrorCodes, ", "))
}
if !lc.CaptchaSkipHostnameCheck && !captchaHostnameMatches(verdict.Hostname, req.Identifier.Host) {
return errCaptchaHostMismatch
}
return nil
}
// captchaHostnameMatches accepts the exact identity host or any of its
// subdomains: trusted_domains may have collapsed the identity host to a
// domain suffix while the provider reports the full page hostname.
func captchaHostnameMatches(hostname string, host []byte) bool {
if hostname == "" || len(host) == 0 {
return false
}
if len(hostname) == len(host) {
return strings.EqualFold(hostname, string(host))
}
prefixLen := len(hostname) - len(host)
if prefixLen < 1 || hostname[prefixLen-1] != '.' {
return false
}
return strings.EqualFold(hostname[prefixLen:], string(host))
}
func validatorCaptcha(b *Berghain, req *ValidatorRequest, resp *ValidatorResponse) error {
var c captchaValidator
switch req.Method {
case http.MethodPost:
if err := c.isValid(b, req, resp); err != nil {
return err
}
return req.Identifier.ToCookie(b, resp.Token)
case http.MethodGet:
return c.onNew(b, req, resp)
}
return errInvalidMethod
}