-
Notifications
You must be signed in to change notification settings - Fork 1
/
captcha.go
91 lines (78 loc) · 2.94 KB
/
captcha.go
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
package main
import (
"errors"
"fmt"
"log"
"net/url"
"regexp"
)
const (
MaxCaptchaAttempts = 5
)
var solutionPattern = regexp.MustCompile(`^[a-zA-Z0-9]{4}$`)
// Response from GET GET_CAPTCHA_CLIENT_URL and request to POST VALIDATE_CAPTCHA_URL
// Format: {"captchaKey":"BASE64","captchaImage":"data:image/gif;base64,...BASE64...","userCode":null}
type CaptchaProtocol struct {
// This is set in the GET response, and should also be sent in the POST
CaptchaKey string `json:"captchaKey"`
// This doesn't need to be set in the POST
CaptchaImage string `json:"captchaImage"`
// UserCode is null in the GET response, set for POST
UserCode string `json:"userCode"`
}
// Success: {"captchaMatched": True, "captchaKey": "NEW\BASE64=="}
type CaptchaAttemptResults struct {
CaptchaMatched bool `json:"captchaMatched"`
CaptchaKey string `json:"captchaKey"`
}
// ProcessCaptcha retrieves and solves the captcha for the given jail, returning the captchaKey.
func ProcessCaptcha(jail *Jail) (string, error) {
// Referer should be the jail's URL; used for redirection in web client.
// May not affect us, but matches "normal" traffic.
headers := map[string][]string{
"Referer": {jail.getJailURL()},
}
// Yes, "captcha" and "Captcha", as seen in the application traffic
// Tempting to refactor this elsewhere to separate concerns.
getCaptchaClientURL, err := url.JoinPath(jail.BaseURL, "jtclientweb/captcha/getnewcaptchaclient")
if err != nil {
return "", fmt.Errorf("failed to join URL: %w", err)
}
validateCaptchaURL, err := url.JoinPath(jail.BaseURL, "jtclientweb/Captcha/validatecaptcha")
if err != nil {
return "", fmt.Errorf("failed to join URL: %w", err)
}
// Get the captcha key
challenge := &CaptchaProtocol{}
err = GetJSON[CaptchaProtocol](getCaptchaClientURL, headers, challenge)
if err != nil {
return "", fmt.Errorf("failed to GET captcha key: %w", err)
}
// Solve captcha
solution, err := solveCaptchaOpenAI(challenge.CaptchaImage)
if err != nil {
return "", fmt.Errorf("failed to get captcha solution: %w", err)
}
challenge.UserCode = solution
log.Printf("Received solution: %s", solution)
if !solutionFormatIsValid(solution) {
return "", fmt.Errorf(`solution "%s" seems invalid; skipping`, solution)
}
// Submit response
results := &CaptchaAttemptResults{}
err = PostJSON[CaptchaProtocol, CaptchaAttemptResults](validateCaptchaURL, headers, challenge, results)
if err != nil {
return "", fmt.Errorf("failed to submit captcha solution: %w", err)
}
if !results.CaptchaMatched {
return "", errors.New("captcha did not match")
}
log.Printf("Solution \"%s\" matched key \"%s\"", solution, results.CaptchaKey)
return results.CaptchaKey, nil
}
// Check if captcha solution format matches expected format.
// This lets us skip validating solutions we expect to fail.
// So far, I've only seen 4-character captchas matching ^[a-zA-Z0-9]{4}$
func solutionFormatIsValid(solution string) bool {
return solutionPattern.MatchString(solution)
}