├── .gitignore ├── README.md ├── agents ├── agent.go ├── chrome.go └── utils.go ├── algorithm ├── algorithm.go ├── algorithm_test.go ├── hsl.go ├── hsw.go └── register.go ├── captcha_test.go ├── challenge.go ├── events.go ├── go.mod ├── go.sum ├── images ├── example.png └── one_vs_one.png ├── screen ├── curve.go ├── curve_test.go ├── options.go └── utils.go ├── solver.go └── utils ├── data.go ├── rand.go └── widget.go /.gitignore: -------------------------------------------------------------------------------- 1 | /yolo/coco.names 2 | /yolo/yolov3.cfg 3 | /yolo/yolov3.weights 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-hcaptcha 2 | 3 | A Go library for solving hCaptchas with any image recognition API. 4 | 5 | ![1v1 me, bro!](images/one_vs_one.png) 6 | 7 | ## Basic Usage 8 | In order to solve, you need the site URL (not the domain!), and the site key, which can be found 9 | in the HTML of the website with the hCaptcha challenge. 10 | 11 | Below is a basic example of how to use the solver with the two using a simple guessing solver. 12 | ```go 13 | c, err := NewChallenge(siteUrl, siteKey) 14 | if err != nil { 15 | panic(err) 16 | } 17 | err = c.Solve(&GuessSolver{}) 18 | if err != nil { 19 | panic(err) 20 | } 21 | fmt.Println(c.Token()) // P0_eyJ0eXAiOiJKV1QiLC... 22 | ``` 23 | 24 | ## Custom Solvers 25 | Custom solvers can be implemented with ease. All solvers need to implement the `Solver` interface. You 26 | can link this with any image recognition API you want. 27 | 28 | You can see an example of this with the `GuessSolver` implementation in `solver.go`. 29 | 30 | ## Credits 31 | 32 | ### 2.0.1 33 | A few changes to support the modern API, and general code cleanup along with a switch to `mathgl` for 34 | Bézier curve creation. 35 | 36 | ### 2.0.0 37 | The motion data capturing required with hCaptcha would not be possible without the work of 38 | [@h0nde](https://github.com/h0nde) and his [py-hcaptcha](https://github.com/h0nde/py-hcaptcha) solver in Python. 39 | 40 | ### 1.0.2: 41 | There were quite a lot of changes with the hCaptcha API, so the solver was updated to reflect these changes, with 42 | the generous help of [@aw1875](https://github.com/aw1875) and his [puppeteer-hcaptcha](https://github.com/aw1875/puppeteer-hcaptcha) 43 | solver in JavaScript. 44 | 45 | ### 1.0.0 46 | This project was inspired by the work of [@JimmyLaurent](https://github.com/JimmyLaurent) and his [hcaptcha-solver](https://github.com/JimmyLaurent/hcaptcha-solver) 47 | also in JavaScript. I'd like to thank him for his work, and for being a motivation to create this library. 48 | -------------------------------------------------------------------------------- /agents/agent.go: -------------------------------------------------------------------------------- 1 | package agents 2 | 3 | import "github.com/iancoleman/orderedmap" 4 | 5 | // Agent is used to generate user-agent specific data. 6 | type Agent interface { 7 | // UserAgent returns the user-agent string for the agent. 8 | UserAgent() string 9 | // ScreenProperties returns the screen properties of the agent. 10 | ScreenProperties() *orderedmap.OrderedMap 11 | // NavigatorProperties returns the navigator properties of the agent. 12 | NavigatorProperties() *orderedmap.OrderedMap 13 | 14 | // Unix returns the current timestamp with any added offsets. 15 | Unix() int64 16 | // OffsetUnix offsets the Unix timestamp with the given offset. 17 | OffsetUnix(offset int64) 18 | // ResetUnix resets the Unix timestamp with offsets to the current time. 19 | ResetUnix() 20 | } 21 | 22 | // Compile time check to make sure that the Agent interface is implemented by Chrome. 23 | var _ Agent = (*Chrome)(nil) 24 | -------------------------------------------------------------------------------- /agents/chrome.go: -------------------------------------------------------------------------------- 1 | package agents 2 | 3 | import ( 4 | "github.com/iancoleman/orderedmap" 5 | "github.com/justtaldevelops/go-hcaptcha/utils" 6 | "time" 7 | ) 8 | 9 | // Chrome is the agent for Google Chrome. 10 | type Chrome struct { 11 | // screenSize is the screen size of the Chrome browser. 12 | screenSize [2]int 13 | // availableScreenSize is the available screen size of the Chrome browser. 14 | availableScreenSize [2]int 15 | // cpuCount is the number of CPUs allowed to the Chrome browser. 16 | cpuCount int 17 | // memorySize is the memory size in gigabytes allowed to the Chrome browser. 18 | memorySize int 19 | // unixOffset is the offset of the unix timestamps. 20 | unixOffset int64 21 | } 22 | 23 | // NewChrome creates a new Chrome agent. 24 | func NewChrome() *Chrome { 25 | possibleScreenSizes := [][][2]int{ 26 | {{1920, 1080}, {1920, 1040}}, 27 | {{2560, 1440}, {2560, 1400}}, 28 | } 29 | possibleCpuCounts := []int{2, 4, 8, 16} 30 | possibleMemorySizes := []int{2, 4, 8, 16} 31 | 32 | screenSizes := possibleScreenSizes[utils.Between(0, len(possibleScreenSizes)-1)] 33 | 34 | return &Chrome{ 35 | screenSize: screenSizes[0], 36 | availableScreenSize: screenSizes[1], 37 | cpuCount: possibleCpuCounts[utils.Between(0, len(possibleCpuCounts)-1)], 38 | memorySize: possibleMemorySizes[utils.Between(0, len(possibleMemorySizes)-1)], 39 | } 40 | } 41 | 42 | // UserAgent ... 43 | func (c *Chrome) UserAgent() string { 44 | return latestChromeAgent 45 | } 46 | 47 | // ScreenProperties ... 48 | func (c *Chrome) ScreenProperties() *orderedmap.OrderedMap { 49 | m := orderedmap.New() 50 | m.Set("availWidth", c.availableScreenSize[0]) 51 | m.Set("availHeight", c.availableScreenSize[1]) 52 | m.Set("width", c.screenSize[0]) 53 | m.Set("height", c.screenSize[1]) 54 | m.Set("colorDepth", 24) 55 | m.Set("pixelDepth", 24) 56 | m.Set("availLeft", 0) 57 | m.Set("availTop", 0) 58 | return m 59 | } 60 | 61 | // NavigatorProperties ... 62 | func (c *Chrome) NavigatorProperties() *orderedmap.OrderedMap { 63 | chromium := orderedmap.New() 64 | chromium.Set("brand", "Chromium") 65 | chromium.Set("version", shortChromeVersion) 66 | 67 | chrome := orderedmap.New() 68 | chrome.Set("brand", "Google Chrome") 69 | chrome.Set("version", shortChromeVersion) 70 | 71 | notAnyBrand := orderedmap.New() 72 | notAnyBrand.Set("brand", ";Not A Brand") 73 | notAnyBrand.Set("version", "99") 74 | 75 | userAgentData := orderedmap.New() 76 | userAgentData.Set("brands", []*orderedmap.OrderedMap{chromium, chrome, notAnyBrand}) 77 | userAgentData.Set("mobile", false) 78 | 79 | m := orderedmap.New() 80 | m.Set("vendorSub", "") 81 | m.Set("productSub", "20030107") 82 | m.Set("vendor", "Google Inc.") 83 | m.Set("maxTouchPoints", 0) 84 | m.Set("userActivation", struct{}{}) 85 | m.Set("doNotTrack", "1") 86 | m.Set("geolocation", struct{}{}) 87 | m.Set("connection", struct{}{}) 88 | m.Set("webkitTemporaryStorage", struct{}{}) 89 | m.Set("webkitPersistentStorage", struct{}{}) 90 | m.Set("hardwareConcurrency", c.cpuCount) 91 | m.Set("cookieEnabled", true) 92 | m.Set("appCodeName", "Mozilla") 93 | m.Set("appName", "Netscape") 94 | m.Set("appVersion", "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/"+chromeVersion+" Safari/537.36") 95 | m.Set("platform", "Win32") 96 | m.Set("product", "Gecko") 97 | m.Set("userAgent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/"+chromeVersion+" Safari/537.36") 98 | m.Set("language", "en-US") 99 | m.Set("languages", []string{"en-US"}) 100 | m.Set("onLine", true) 101 | m.Set("webdriver", false) 102 | m.Set("pdfViewerEnabled", true) 103 | m.Set("scheduling", struct{}{}) 104 | m.Set("bluetooth", struct{}{}) 105 | m.Set("clipboard", struct{}{}) 106 | m.Set("credentials", struct{}{}) 107 | m.Set("keyboard", struct{}{}) 108 | m.Set("managed", struct{}{}) 109 | m.Set("mediaDevices", struct{}{}) 110 | m.Set("storage", struct{}{}) 111 | m.Set("serviceWorker", struct{}{}) 112 | m.Set("wakeLock", struct{}{}) 113 | m.Set("deviceMemory", c.memorySize) 114 | m.Set("ink", struct{}{}) 115 | m.Set("hid", struct{}{}) 116 | m.Set("locks", struct{}{}) 117 | m.Set("mediaCapabilities", struct{}{}) 118 | m.Set("mediaSession", struct{}{}) 119 | m.Set("permissions", struct{}{}) 120 | m.Set("presentation", struct{}{}) 121 | m.Set("serial", struct{}{}) 122 | m.Set("virtualKeyboard", struct{}{}) 123 | m.Set("usb", struct{}{}) 124 | m.Set("xr", struct{}{}) 125 | m.Set("userAgentData", userAgentData) 126 | m.Set("plugins", []string{ 127 | "internal-pdf-viewer", 128 | "internal-pdf-viewer", 129 | "internal-pdf-viewer", 130 | "internal-pdf-viewer", 131 | "internal-pdf-viewer", 132 | }) 133 | 134 | return m 135 | } 136 | 137 | // Unix ... 138 | func (c *Chrome) Unix() int64 { 139 | t := time.Now().UnixNano() / int64(time.Millisecond) 140 | t += c.unixOffset 141 | 142 | return t 143 | } 144 | 145 | // OffsetUnix ... 146 | func (c *Chrome) OffsetUnix(offset int64) { 147 | c.unixOffset += offset 148 | } 149 | 150 | // ResetUnix ... 151 | func (c *Chrome) ResetUnix() { 152 | if c.unixOffset > 0 { 153 | time.Sleep(time.Duration(c.unixOffset) * time.Millisecond) 154 | } 155 | c.unixOffset = 0 156 | } 157 | -------------------------------------------------------------------------------- /agents/utils.go: -------------------------------------------------------------------------------- 1 | package agents 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | // latestChromeAgent is initialized by init. 11 | latestChromeAgent string 12 | // chromeVersion is initialized by init. 13 | chromeVersion string 14 | // shortChromeVersion is initialized by init. 15 | shortChromeVersion string 16 | ) 17 | 18 | // init updates the latest Chrome agent and Chrome version. 19 | func init() { 20 | resp, err := http.Get("https://jnrbsn.github.io/user-agents/user-agents.json") 21 | if err != nil { 22 | panic(err) 23 | } 24 | defer resp.Body.Close() 25 | 26 | var data []string 27 | if err = json.NewDecoder(resp.Body).Decode(&data); err != nil { 28 | panic(err) 29 | } 30 | 31 | latestChromeAgent = data[0] 32 | chromeVersion = strings.Split(strings.Split(latestChromeAgent, "Chrome/")[1], " ")[0] 33 | shortChromeVersion = strings.Split(chromeVersion, ".")[0] 34 | } 35 | -------------------------------------------------------------------------------- /algorithm/algorithm.go: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | import ( 4 | "github.com/justtaldevelops/go-hcaptcha/utils" 5 | "io/ioutil" 6 | "net/http" 7 | ) 8 | 9 | // Algorithm is the algorithm used to provide proof. 10 | type Algorithm interface { 11 | // Encode encodes the algorithm name as a string. 12 | Encode() string 13 | // Prove returns proof (N) of the request (C). 14 | Prove(request string) (string, error) 15 | } 16 | 17 | // Proof is the full proof of a request. 18 | type Proof struct { 19 | // Algorithm is the algorithm used to provide proof. 20 | Algorithm Algorithm 21 | // Request is the original request (C) which was used to calculate the proof (N). 22 | Request string 23 | // Proof is the calculated proof (N). 24 | Proof string 25 | } 26 | 27 | // Compile time checks to make sure HSL and HSW implement Algorithm. 28 | var _, _ Algorithm = (*HSL)(nil), (*HSW)(nil) 29 | 30 | // Solve solves for N given a specific algorithm and request. It returns the full proof. 31 | func Solve(algorithm, request string) (Proof, error) { 32 | algo := findAlgorithm(algorithm) 33 | proof, err := algo.Prove(request) 34 | if err != nil { 35 | return Proof{}, err 36 | } 37 | 38 | return Proof{Algorithm: algo, Request: `{"type":"` + algo.Encode() + `","req":"` + request + `"}`, Proof: proof}, nil 39 | } 40 | 41 | // script gets the script of the algorithm from hCaptcha. 42 | func script(script string) string { 43 | resp, err := http.Get("https://newassets.hcaptcha.com/c/" + utils.AssetVersion + "/" + script) 44 | if err != nil { 45 | panic(err) 46 | } 47 | defer resp.Body.Close() 48 | 49 | b, err := ioutil.ReadAll(resp.Body) 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | return string(b) 55 | } 56 | -------------------------------------------------------------------------------- /algorithm/algorithm_test.go: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestAlgorithms(t *testing.T) { 9 | fmt.Println(Solve("hsl", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzIjoyLCJ0IjoicCIsImQiOiJwMDFXWVZyVzZjRkl3VVNiV05paUdrQ015VmFnOFZkY2FWUzBPVGFaaDM1VnRnS2M1MlduWXM3SkdPajN5MGtHZHdNNi9ZMmQyN2I2c3BIWmlCSjVvdVZMMWNySHduN2RmMStzUjZTUDhDdU9DcEtpM3JLRFIwSlNDc25wSXVjamFVbGd1enpYTXkzakl4d0t3U0RucXQ0MG1EWEZoU0RiMjdLNDBWU3QvRXFrbXNEOVRzQWowRGJXTFE9PW5zdGVPcDlxdXdqY2xOcEwiLCJsIjoiaHR0cHM6Ly9hc3NldHMuaGNhcHRjaGEuY29tL2MvNTgyOTZiODAiLCJlIjoxNjE3NTU0Nzg0fQ.IHACH-J48Dhzd1Lz0STv5wjZKipMzRg5A-DD71n3WEM")) 10 | fmt.Println(Solve("hsw", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzIjoyLCJ0IjoidyIsImQiOiJmTkRxeWt2WUxRWkRNemlrd2Zwc1VMbHUrcklpMXlYejNnYjdNRUhDWEM2TGhFRFIzMFo1VzkwRExvcFhTWHFUcjFrT1E1bDQ1VHBaSzBDY2pnZlhEWHVmL3pIRjliQ3RIdjhiTEtsekM2TUlWbzB0a28zT052SlU2U1A3NkdCdVNwV0pCTU1jRVAyTzQ5aXpTM3VMc1MyK0NFQVRiTW1kczBNSHFyY3NWNXFUL0hWL3pjdUZmbWpXMkE9PTVFWXlVbHR2TlAzbmxFUFgiLCJsIjoiaHR0cHM6Ly9uZXdhc3NldHMuaGNhcHRjaGEuY29tL2MvOTYzZTQ2YjciLCJlIjoxNjM2Mjk5OTkxfQ.g_xb9oJD-antRtIXq-CW7Mf3lzOzin4CPvd8uU2O6Jg")) 11 | } 12 | -------------------------------------------------------------------------------- /algorithm/hsl.go: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/square/go-jose.v2/jwt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // HSL is one of a few proof algorithms for hCaptcha services. 11 | type HSL struct{} 12 | 13 | // Encode ... 14 | func (h *HSL) Encode() string { 15 | return "hsl" 16 | } 17 | 18 | // Prove ... 19 | func (h *HSL) Prove(request string) (string, error) { 20 | tok, err := jwt.ParseSigned(request) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | var claims map[string]interface{} 26 | if err = tok.UnsafeClaimsWithoutVerification(&claims); err != nil { 27 | panic(err) 28 | } 29 | 30 | now := time.Now().UTC().Format("2006-01-02T15:04:05.999Z") 31 | now = now[:len(now)-5] 32 | now = strings.ReplaceAll(now, "-", "") 33 | now = strings.ReplaceAll(now, ":", "") 34 | now = strings.ReplaceAll(now, "T", "") 35 | 36 | return strings.Join([]string{ 37 | "1", 38 | fmt.Sprint(int(claims["s"].(float64))), 39 | now, 40 | claims["d"].(string), 41 | "", 42 | "1", // TODO: Figure out if this is right? 43 | }, ":"), nil 44 | } 45 | -------------------------------------------------------------------------------- /algorithm/hsw.go: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | import ( 4 | "fmt" 5 | "github.com/mxschmitt/playwright-go" 6 | ) 7 | 8 | // page is used for generating the HSW. 9 | var page playwright.Page 10 | 11 | // init initializes Playwright for the HSW algorithm. 12 | func init() { 13 | pw, err := playwright.Run() 14 | if err != nil { 15 | panic(err) 16 | } 17 | browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{ 18 | Headless: playwright.Bool(true), 19 | }) 20 | if err != nil { 21 | panic(err) 22 | } 23 | page, err = browser.NewPage() 24 | if err != nil { 25 | panic(err) 26 | } 27 | _, err = page.AddScriptTag(playwright.PageAddScriptTagOptions{Content: playwright.String(script("hsw.js"))}) 28 | if err != nil { 29 | panic(err) 30 | } 31 | } 32 | 33 | // HSW is one of a few proof algorithms for hCaptcha services. 34 | type HSW struct{} 35 | 36 | // Encode ... 37 | func (h *HSW) Encode() string { 38 | return "hsw" 39 | } 40 | 41 | // Prove ... 42 | func (h HSW) Prove(request string) (string, error) { 43 | resp, err := page.Evaluate(fmt.Sprintf(`hsw("%v")`, request)) 44 | if err != nil { 45 | return "", err 46 | } 47 | return resp.(string), nil 48 | } 49 | -------------------------------------------------------------------------------- /algorithm/register.go: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | // algorithms is a map of all available algorithms. 4 | var algorithms = make(map[string]Algorithm) 5 | 6 | // init registers all available algorithms. 7 | func init() { 8 | registerAlgorithm(&HSL{}) 9 | registerAlgorithm(&HSW{}) 10 | } 11 | 12 | // findAlgorithm returns the algorithm with the given name. 13 | func findAlgorithm(name string) Algorithm { 14 | return algorithms[name] 15 | } 16 | 17 | // registerAlgorithm registers an algorithm. 18 | func registerAlgorithm(algorithm Algorithm) { 19 | algorithms[algorithm.Encode()] = algorithm 20 | } 21 | -------------------------------------------------------------------------------- /captcha_test.go: -------------------------------------------------------------------------------- 1 | package hcaptcha 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | // TestCaptcha ... 9 | func TestCaptcha(t *testing.T) { 10 | for { 11 | c, err := NewChallenge( 12 | "https://accounts.hcaptcha.com/demo", 13 | "a5f74b19-9e45-40e0-b45d-47ff91b7a6c2", 14 | ChallengeOptions{ 15 | Timeout: 10 * time.Second, 16 | }, 17 | ) 18 | if err != nil { 19 | panic(err) 20 | } 21 | err = c.Solve(&GuessSolver{}) 22 | if err != nil { 23 | c.Logger().Debugf("Error from hCaptcha API: %s", err) 24 | continue 25 | } 26 | c.Logger().Info(c.Token()) 27 | break 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /challenge.go: -------------------------------------------------------------------------------- 1 | package hcaptcha 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/go-gl/mathgl/mgl64" 8 | "github.com/iancoleman/orderedmap" 9 | "github.com/justtaldevelops/go-hcaptcha/agents" 10 | "github.com/justtaldevelops/go-hcaptcha/algorithm" 11 | "github.com/justtaldevelops/go-hcaptcha/screen" 12 | "github.com/justtaldevelops/go-hcaptcha/utils" 13 | "github.com/sirupsen/logrus" 14 | "github.com/tidwall/gjson" 15 | "io/ioutil" 16 | "math" 17 | "net/http" 18 | "net/url" 19 | "strconv" 20 | "strings" 21 | "time" 22 | ) 23 | 24 | // Challenge is an hCaptcha challenge. 25 | type Challenge struct { 26 | host, siteKey string 27 | url, widgetID string 28 | id, token string 29 | category, question string 30 | tasks []Task 31 | log *logrus.Logger 32 | agent agents.Agent 33 | proof algorithm.Proof 34 | top, frame *EventRecorder 35 | } 36 | 37 | // ChallengeOptions contains special options that can be applied to new solvers. 38 | type ChallengeOptions struct { 39 | // Logger is the logger to use for logging. 40 | Logger *logrus.Logger 41 | // Proxy is the proxy to use for requests to hCaptcha. 42 | Proxy string 43 | // Timeout is the timeout to use for requests to hCaptcha. 44 | Timeout time.Duration 45 | } 46 | 47 | // Task is a task assigned by hCaptcha. 48 | type Task struct { 49 | // Image is a link to the image URL to represent the task. 50 | Image string 51 | // Key is the task key, used when referencing answers. 52 | Key string 53 | // Index is the index of the task. 54 | Index int 55 | } 56 | 57 | // basicChallengeOptions is a set of default options for a basic solver. 58 | func basicChallengeOptions(options *ChallengeOptions) { 59 | if options.Logger == nil { 60 | options.Logger = logrus.New() 61 | options.Logger.Formatter = &logrus.TextFormatter{ForceColors: true} 62 | options.Logger.Level = logrus.DebugLevel 63 | } 64 | } 65 | 66 | // NewChallenge creates a new hCaptcha challenge. 67 | func NewChallenge(targetUrl, siteKey string, opts ...ChallengeOptions) (*Challenge, error) { 68 | if len(opts) == 0 { 69 | opts = append(opts, ChallengeOptions{}) 70 | } 71 | 72 | options := opts[0] 73 | basicChallengeOptions(&options) 74 | 75 | if len(options.Proxy) > 0 { 76 | proxyUrl, err := url.Parse(options.Proxy) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | http.DefaultClient.Transport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)} 82 | } 83 | 84 | if options.Timeout > 0 { 85 | http.DefaultClient.Timeout = options.Timeout 86 | } 87 | 88 | c := &Challenge{ 89 | host: strings.ToLower(strings.Split(strings.Split(targetUrl, "://")[1], "/")[0]), 90 | siteKey: siteKey, 91 | url: targetUrl, 92 | log: options.Logger, 93 | widgetID: utils.WidgetID(), 94 | agent: agents.NewChrome(), 95 | } 96 | c.agent.OffsetUnix(-10) 97 | c.setupFrames() 98 | 99 | c.log.Debug("Verifying site configuration...") 100 | err := c.siteConfig() 101 | if err != nil { 102 | return nil, err 103 | } 104 | c.log.Info("Requesting captcha...") 105 | err = c.requestCaptcha() 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | return c, nil 111 | } 112 | 113 | // Solve solves the challenge with the provided solver. 114 | func (c *Challenge) Solve(solver Solver) error { 115 | c.log.Debugf("Solving challenge with %T...", solver) 116 | if len(c.token) > 0 { 117 | return nil 118 | } 119 | 120 | split := strings.Split(c.question, " ") 121 | object := strings.Replace(strings.Replace(strings.Replace(split[len(split)-1], "motorbus", "bus", 1), "airplane", "aeroplane", 1), "motorcycle", "motorbike", 1) 122 | 123 | c.log.Debugf(`The type of challenge is "%v"`, c.category) 124 | c.log.Debugf(`The target object is "%v"`, object) 125 | 126 | answers := solver.Solve(c.category, object, c.tasks) 127 | 128 | c.log.Debugf("Decided on %v/%v of the tasks given!", len(answers), len(c.tasks)) 129 | c.log.Debug("Simulating mouse movements on tiles...") 130 | 131 | c.simulateMouseMovements(answers) 132 | c.agent.ResetUnix() 133 | 134 | answersAsMap := orderedmap.New() 135 | for _, answer := range c.tasks { 136 | answersAsMap.Set(answer.Key, strconv.FormatBool(c.answered(answer, answers))) 137 | } 138 | 139 | motionData := orderedmap.New() 140 | frameData := c.frame.Data() 141 | for _, key := range frameData.Keys() { 142 | value, _ := frameData.Get(key) 143 | motionData.Set(key, value) 144 | } 145 | motionData.Set("topLevel", c.top.Data()) 146 | motionData.Set("v", 1) 147 | 148 | encodedMotionData, err := json.Marshal(motionData) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | m := orderedmap.New() 154 | m.Set("v", utils.Version) 155 | m.Set("job_mode", c.category) 156 | m.Set("answers", answersAsMap) 157 | m.Set("serverdomain", c.host) 158 | m.Set("sitekey", c.siteKey) 159 | m.Set("motionData", string(encodedMotionData)) 160 | m.Set("n", c.proof.Proof) 161 | m.Set("c", c.proof.Request) 162 | 163 | b, err := json.Marshal(m) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | req, err := http.NewRequest("POST", "https://hcaptcha.com/checkcaptcha/"+c.id+"?s="+c.siteKey, bytes.NewReader(b)) 169 | if err != nil { 170 | return err 171 | } 172 | req.Header.Set("Authority", "hcaptcha.com") 173 | req.Header.Set("Accept", "*/*") 174 | req.Header.Set("User-Agent", c.agent.UserAgent()) 175 | req.Header.Set("Content-Type", "application/json") 176 | req.Header.Set("Origin", "https://newassets.hcaptcha.com") 177 | req.Header.Set("Sec-Fetch-Site", "same-site") 178 | req.Header.Set("Sec-Fetch-Mode", "cors") 179 | req.Header.Set("Sec-Fetch-Dest", "empty") 180 | req.Header.Set("Accept-Language", "en-US,en;q=0.9") 181 | 182 | resp, err := http.DefaultClient.Do(req) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | b, err = ioutil.ReadAll(resp.Body) 188 | if err != nil { 189 | return err 190 | } 191 | response := gjson.ParseBytes(b) 192 | if !response.Get("pass").Bool() { 193 | return fmt.Errorf("incorrect answers") 194 | } 195 | 196 | c.log.Info("Successfully completed challenge!") 197 | c.token = response.Get("generated_pass_UUID").String() 198 | return nil 199 | } 200 | 201 | // Tasks returns the tasks for the challenge. 202 | func (c *Challenge) Tasks() []Task { 203 | return c.tasks 204 | } 205 | 206 | // Logger returns the logger for the challenge. 207 | func (c *Challenge) Logger() *logrus.Logger { 208 | return c.log 209 | } 210 | 211 | // Category returns the category of the challenge. 212 | func (c *Challenge) Category() string { 213 | return c.category 214 | } 215 | 216 | // Question returns the question of the challenge. 217 | func (c *Challenge) Question() string { 218 | return c.question 219 | } 220 | 221 | // Token returns the response token. This is only valid once the challenge has been solved. 222 | func (c *Challenge) Token() string { 223 | return c.token 224 | } 225 | 226 | // setupFrames sets up the frames for the challenge. 227 | func (c *Challenge) setupFrames() { 228 | c.top = NewEventRecorder(c.agent) 229 | c.top.Record() 230 | c.top.SetData("dr", "") 231 | c.top.SetData("inv", false) 232 | c.top.SetData("sc", c.agent.ScreenProperties()) 233 | c.top.SetData("nv", c.agent.NavigatorProperties()) 234 | c.top.SetData("exec", false) 235 | c.agent.OffsetUnix(int64(utils.Between(200, 400))) 236 | c.frame = NewEventRecorder(c.agent) 237 | c.frame.Record() 238 | } 239 | 240 | // simulateMouseMovements simulates mouse movements for the hCaptcha API. 241 | func (c *Challenge) simulateMouseMovements(answers []Task) { 242 | totalPages := int(math.Max(1, float64(len(c.tasks)/utils.TilesPerPage))) 243 | cursorPos := mgl64.Vec2{float64(utils.Between(1, 5)), float64(utils.Between(300, 350))} 244 | 245 | rightBoundary := utils.FrameSize[0] 246 | upBoundary := utils.FrameSize[1] 247 | opts := &screen.CurveOpts{ 248 | RightBoundary: &rightBoundary, 249 | UpBoundary: &upBoundary, 250 | } 251 | 252 | for page := 0; page < totalPages; page++ { 253 | pageTiles := c.tasks[page*utils.TilesPerPage : (page+1)*utils.TilesPerPage] 254 | for _, tile := range pageTiles { 255 | if !c.answered(tile, answers) { 256 | continue 257 | } 258 | 259 | tilePos := mgl64.Vec2{ 260 | float64((utils.TileImageSize[0] * tile.Index % utils.TilesPerRow) + 261 | utils.TileImagePadding[0]*tile.Index%utils.TilesPerRow + 262 | utils.Between(10, utils.TileImageSize[0]) + 263 | utils.TileImageStartPosition[0]), 264 | float64((utils.TileImageSize[1] * tile.Index % utils.TilesPerRow) + 265 | utils.TileImagePadding[1]*tile.Index%utils.TilesPerRow + 266 | utils.Between(10, utils.TileImageSize[1]) + 267 | utils.TileImageStartPosition[1]), 268 | } 269 | 270 | movements := c.generateMouseMovements(cursorPos, tilePos, opts) 271 | lastMovement := movements[len(movements)-1] 272 | for _, move := range movements { 273 | c.frame.RecordEvent(Event{Type: "mm", Point: move.point, Timestamp: move.timestamp}) 274 | } 275 | // TODO: Add a delay for movement up and down. 276 | c.frame.RecordEvent(Event{Type: "md", Point: lastMovement.point, Timestamp: lastMovement.timestamp}) 277 | c.frame.RecordEvent(Event{Type: "mu", Point: lastMovement.point, Timestamp: lastMovement.timestamp}) 278 | cursorPos = tilePos 279 | } 280 | 281 | buttonPos := mgl64.Vec2{ 282 | float64(utils.VerifyButtonPosition[0] + utils.Between(5, 50)), 283 | float64(utils.VerifyButtonPosition[1] + utils.Between(5, 15)), 284 | } 285 | 286 | movements := c.generateMouseMovements(cursorPos, buttonPos, opts) 287 | lastMovement := movements[len(movements)-1] 288 | for _, move := range movements { 289 | c.frame.RecordEvent(Event{Type: "mm", Point: move.point, Timestamp: move.timestamp}) 290 | } 291 | c.frame.RecordEvent(Event{Type: "md", Point: lastMovement.point, Timestamp: lastMovement.timestamp}) 292 | c.frame.RecordEvent(Event{Type: "mu", Point: lastMovement.point, Timestamp: lastMovement.timestamp}) 293 | cursorPos = buttonPos 294 | } 295 | } 296 | 297 | // movement is a mouse movement. 298 | type movement struct { 299 | // point is the mouse's position at the timestamp. 300 | point mgl64.Vec2 301 | // timestamp is the timestamp of the movement. 302 | timestamp int64 303 | } 304 | 305 | // generateMouseMovements generates mouse movements for simulateMouseMovements. 306 | func (c *Challenge) generateMouseMovements(fromPoint, toPoint mgl64.Vec2, opts *screen.CurveOpts) []movement { 307 | curve := screen.NewCurve(fromPoint, toPoint, opts) 308 | points := curve.Points() 309 | 310 | resultMovements := make([]movement, len(points)) 311 | for _, point := range points { 312 | c.agent.OffsetUnix(int64(utils.Between(2, 5))) 313 | resultMovements = append(resultMovements, movement{point: point, timestamp: c.agent.Unix()}) 314 | } 315 | return resultMovements 316 | } 317 | 318 | // answered returns true if the task provided is in the answers slice. 319 | func (c *Challenge) answered(task Task, answers []Task) bool { 320 | for _, answer := range answers { 321 | if answer.Key == task.Key { 322 | return true 323 | } 324 | } 325 | return false 326 | } 327 | 328 | // requestCaptcha gets the captcha from the site. 329 | func (c *Challenge) requestCaptcha() error { 330 | prev := orderedmap.New() 331 | prev.Set("escaped", false) 332 | prev.Set("passed", false) 333 | prev.Set("expiredChallenge", false) 334 | prev.Set("expiredResponse", false) 335 | 336 | motionData := orderedmap.New() 337 | motionData.Set("v", 1) 338 | 339 | frameData := c.frame.Data() 340 | for _, key := range frameData.Keys() { 341 | value, _ := frameData.Get(key) 342 | motionData.Set(key, value) 343 | } 344 | 345 | motionData.Set("topLevel", c.top.Data()) 346 | motionData.Set("session", struct{}{}) 347 | motionData.Set("widgetList", []string{c.widgetID}) 348 | motionData.Set("widgetId", c.widgetID) 349 | motionData.Set("href", c.url) 350 | motionData.Set("prev", prev) 351 | 352 | encodedMotionData, err := json.Marshal(motionData) 353 | if err != nil { 354 | return err 355 | } 356 | 357 | form := url.Values{} 358 | form.Add("v", utils.Version) 359 | form.Add("sitekey", c.siteKey) 360 | form.Add("host", c.host) 361 | form.Add("hl", "en") 362 | form.Add("motionData", string(encodedMotionData)) 363 | form.Add("n", c.proof.Proof) 364 | form.Add("c", c.proof.Request) 365 | 366 | req, err := http.NewRequest("POST", "https://hcaptcha.com/getcaptcha?s="+c.siteKey, strings.NewReader(form.Encode())) 367 | if err != nil { 368 | return err 369 | } 370 | req.Header.Set("Authority", "hcaptcha.com") 371 | req.Header.Set("Accept", "application/json") 372 | req.Header.Set("User-Agent", c.agent.UserAgent()) 373 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 374 | req.Header.Set("Origin", "https://newassets.hcaptcha.com") 375 | req.Header.Set("Sec-Fetch-Site", "same-site") 376 | req.Header.Set("Sec-Fetch-Mode", "cors") 377 | req.Header.Set("Sec-Fetch-Dest", "empty") 378 | req.Header.Set("Accept-Language", "en-US,en;q=0.9") 379 | 380 | resp, err := http.DefaultClient.Do(req) 381 | if err != nil { 382 | return err 383 | } 384 | defer resp.Body.Close() 385 | 386 | b, err := ioutil.ReadAll(resp.Body) 387 | if err != nil { 388 | return err 389 | } 390 | 391 | response := gjson.ParseBytes(b) 392 | if response.Get("pass").Exists() { 393 | c.token = response.Get("generated_pass_UUID").String() 394 | return nil 395 | } 396 | 397 | success := response.Get("success") 398 | if success.Exists() && !success.Bool() { 399 | return fmt.Errorf("challenge creation request was rejected") 400 | } 401 | 402 | c.id = response.Get("key").String() 403 | c.category = response.Get("request_type").String() 404 | c.question = response.Get("requester_question").Get("en").String() 405 | 406 | tasks := response.Get("tasklist").Array() 407 | if len(tasks) == 0 { 408 | return fmt.Errorf("no tasks in challenge, most likely ratelimited") 409 | } 410 | 411 | for index, task := range tasks { 412 | c.tasks = append(c.tasks, Task{ 413 | Image: task.Get("datapoint_uri").String(), 414 | Key: task.Get("task_key").String(), 415 | Index: index, 416 | }) 417 | } 418 | 419 | request := response.Get("c") 420 | c.proof, err = algorithm.Solve(request.Get("type").String(), request.Get("req").String()) 421 | if err != nil { 422 | return err 423 | } 424 | return nil 425 | } 426 | 427 | // siteConfig verifies a site configuration and returns proof of work for the given challenge. 428 | func (c *Challenge) siteConfig() error { 429 | req, err := http.NewRequest("GET", "https://hcaptcha.com/checksiteconfig?v="+utils.Version+"&host="+c.host+"&sitekey="+c.siteKey+"&sc=1&swa=1", nil) 430 | if err != nil { 431 | return err 432 | } 433 | req.Header.Set("Content-Type", "application/json") 434 | req.Header.Set("User-Agent", c.agent.UserAgent()) 435 | 436 | resp, err := http.DefaultClient.Do(req) 437 | if err != nil { 438 | return err 439 | } 440 | 441 | b, err := ioutil.ReadAll(resp.Body) 442 | if err != nil { 443 | return err 444 | } 445 | 446 | response := gjson.ParseBytes(b) 447 | if !response.Get("pass").Bool() { 448 | return fmt.Errorf("site key is invalid") 449 | } 450 | 451 | request := response.Get("c") 452 | c.proof, err = algorithm.Solve(request.Get("type").String(), request.Get("req").String()) 453 | if err != nil { 454 | return err 455 | } 456 | return nil 457 | } 458 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package hcaptcha 2 | 3 | import ( 4 | "github.com/go-gl/mathgl/mgl64" 5 | "github.com/iancoleman/orderedmap" 6 | "github.com/justtaldevelops/go-hcaptcha/agents" 7 | ) 8 | 9 | // Event is a movement event. 10 | type Event struct { 11 | // Point is the position of the event. 12 | Point mgl64.Vec2 13 | // Type represents the type of event. For example, a "mouse up" event would be "mu". 14 | Type string 15 | // Timestamp is the time the event was recorded. 16 | Timestamp int64 17 | } 18 | 19 | // EventRecorder helps record and retrieve movement events. 20 | type EventRecorder struct { 21 | recording bool 22 | agent agents.Agent 23 | manifest *orderedmap.OrderedMap 24 | timeBuffers map[string]*EventContainer 25 | } 26 | 27 | // NewEventRecorder creates a new EventRecorder. 28 | func NewEventRecorder(agent agents.Agent) *EventRecorder { 29 | return &EventRecorder{ 30 | agent: agent, 31 | manifest: orderedmap.New(), 32 | timeBuffers: make(map[string]*EventContainer), 33 | } 34 | } 35 | 36 | // Record records a new event. 37 | func (e *EventRecorder) Record() { 38 | e.manifest.Set("st", e.agent.Unix()) 39 | e.recording = true 40 | } 41 | 42 | // Data records the events to the manifest and returns it. 43 | func (e *EventRecorder) Data() *orderedmap.OrderedMap { 44 | for event, container := range e.timeBuffers { 45 | e.manifest.Set(event, container.Data()) 46 | e.manifest.Set(event+"-mp", container.MeanPeriod()) 47 | } 48 | return e.manifest 49 | } 50 | 51 | // SetData sets data in the manifest of the EventRecorder. 52 | func (e *EventRecorder) SetData(name string, value interface{}) { 53 | e.manifest.Set(name, value) 54 | } 55 | 56 | // RecordEvent records a new event. 57 | func (e *EventRecorder) RecordEvent(event Event) { 58 | if !e.recording { 59 | return 60 | } 61 | 62 | if _, ok := e.timeBuffers[event.Type]; !ok { 63 | e.timeBuffers[event.Type] = NewEventContainer(e.agent, 16, 15e3) 64 | } 65 | e.timeBuffers[event.Type].Push(event) 66 | } 67 | 68 | // EventContainer is a container for event data. 69 | type EventContainer struct { 70 | agent agents.Agent 71 | period, interval int64 72 | date []int64 73 | data [][]int64 74 | previousTimestamp int64 75 | meanPeriod int64 76 | meanCounter int64 77 | } 78 | 79 | // NewEventContainer creates a new EventContainer. 80 | func NewEventContainer(agent agents.Agent, period, interval int64) *EventContainer { 81 | return &EventContainer{ 82 | agent: agent, 83 | period: period, 84 | interval: interval, 85 | } 86 | } 87 | 88 | // MeanPeriod returns the mean period of the event container. 89 | func (e *EventContainer) MeanPeriod() int64 { 90 | return e.meanPeriod 91 | } 92 | 93 | // Data returns the data of the event container. 94 | func (e *EventContainer) Data() [][]int64 { 95 | e.cleanStaleData() 96 | return e.data 97 | } 98 | 99 | // Push adds a new event to the event container. 100 | func (e *EventContainer) Push(event Event) { 101 | e.cleanStaleData() 102 | 103 | var timestamp int64 104 | 105 | notFirst := len(e.date) > 0 106 | if notFirst { 107 | timestamp = e.date[len(e.date)-1] 108 | } 109 | 110 | if event.Timestamp-timestamp >= e.period { 111 | e.date = append(e.date, event.Timestamp) 112 | e.data = append(e.data, []int64{int64(event.Point.X()), int64(event.Point.Y()), event.Timestamp}) 113 | 114 | if notFirst { 115 | delta := event.Timestamp - e.previousTimestamp 116 | e.meanPeriod = (e.meanPeriod*e.meanCounter + delta) / (e.meanCounter + 1) 117 | e.meanCounter++ 118 | } 119 | } 120 | 121 | e.previousTimestamp = event.Timestamp 122 | } 123 | 124 | // cleanStaleData removes stale data from the event container. 125 | func (e *EventContainer) cleanStaleData() { 126 | date := e.agent.Unix() 127 | t := len(e.date) - 1 128 | 129 | for t >= 0 { 130 | if date-e.date[t] >= e.interval { 131 | e.date = e.date[:t+1] 132 | e.date = e.date[:t+1] 133 | break 134 | } 135 | 136 | t -= 1 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/justtaldevelops/go-hcaptcha 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-gl/mathgl v1.0.0 7 | github.com/go-vgo/robotgo v0.100.3 8 | github.com/google/go-cmp v0.5.6 // indirect 9 | github.com/iancoleman/orderedmap v0.2.0 10 | github.com/mxschmitt/playwright-go v0.1400.0 11 | github.com/sirupsen/logrus v1.8.1 12 | github.com/tidwall/gjson v1.11.0 13 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 // indirect 14 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c // indirect 15 | gopkg.in/square/go-jose.v2 v2.6.0 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= 2 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= 3 | github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= 4 | github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= 5 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= 6 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68= 11 | github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= 12 | github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= 13 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 14 | github.com/go-vgo/robotgo v0.100.3 h1:nm69jC22o2E7ixfMfoSuBWQN4GM+D/GbxG+Wzt1HvBo= 15 | github.com/go-vgo/robotgo v0.100.3/go.mod h1:GCjwxRFoUkuekzm02WexYBV0paDLCbrrlKEfxt/CB10= 16 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 17 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 18 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 19 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 20 | github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= 21 | github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= 22 | github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= 23 | github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= 24 | github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= 25 | github.com/mxschmitt/playwright-go v0.1400.0 h1:HL8dbxcVEobE+pNjASeYGJJRmd4+9gyu/51XO7d3qF0= 26 | github.com/mxschmitt/playwright-go v0.1400.0/go.mod h1:kUvZFgMneRGknVLtC2DKQ42lhZiCmWzxgBdGwjC0vkw= 27 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 28 | github.com/otiai10/gosseract v2.2.1+incompatible h1:Ry5ltVdpdp4LAa2bMjsSJH34XHVOV7XMi41HtzL8X2I= 29 | github.com/otiai10/gosseract v2.2.1+incompatible/go.mod h1:XrzWItCzCpFRZ35n3YtVTgq5bLAhFIkascoRo8G32QE= 30 | github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc= 31 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 32 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 33 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 34 | github.com/robotn/gohook v0.31.2 h1:ADIppQ3T0Sd+kaDMb4Vnv6UeSmhNfK0H0HpPWCd8G5I= 35 | github.com/robotn/gohook v0.31.2/go.mod h1:0BQit8783ey63WXFau8TvoaTYfNtsAhqZ0RJaqlYi6E= 36 | github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934 h1:2lhSR8N3T6I30q096DT7/5AKEIcf1vvnnWAmS0wfnNY= 37 | github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ= 38 | github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770 h1:2uX8QRLkkxn2EpAQ6I3KhA79BkdRZfvugJUzJadiJwk= 39 | github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU= 40 | github.com/shirou/gopsutil v3.21.8+incompatible h1:sh0foI8tMRlCidUJR+KzqWYWxrkuuPIGiO6Vp+KXdCU= 41 | github.com/shirou/gopsutil v3.21.8+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 42 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 43 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 46 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 47 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 48 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 49 | github.com/tidwall/gjson v1.11.0 h1:C16pk7tQNiH6VlCrtIXL1w8GaOsi1X3W8KDkE1BuYd4= 50 | github.com/tidwall/gjson v1.11.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 51 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 52 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 53 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 54 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 55 | github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo= 56 | github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= 57 | github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ= 58 | github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= 59 | github.com/vcaesar/gops v0.21.2 h1:OwWoJR0zb+AK41TN2adhZUP9lAmaRMzkWwf7Ux5Mx00= 60 | github.com/vcaesar/gops v0.21.2/go.mod h1:BEJGigAc9GORbEegWX9rRon/qwibjDs8p50WYm2KlXw= 61 | github.com/vcaesar/imgo v0.30.0 h1:ODQVX0EFJEh+WkKahCBtE0SqcDCIjl/kjiOplR0Ouh8= 62 | github.com/vcaesar/imgo v0.30.0/go.mod h1:8TGnt5hjaMgwDByvMFIzUDSh5uSea4n1tAbSvnhvA6U= 63 | github.com/vcaesar/tt v0.20.0 h1:9t2Ycb9RNHcP0WgQgIaRKJBB+FrRdejuaL6uWIHuoBA= 64 | github.com/vcaesar/tt v0.20.0/go.mod h1:GHPxQYhn+7OgKakRusH7KJ0M5MhywoeLb8Fcffs/Gtg= 65 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 66 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 67 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 68 | golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 69 | golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= 70 | golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 71 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 72 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 73 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 74 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 75 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 76 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 80 | golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 82 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 84 | golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 85 | golang.org/x/sys v0.0.0-20210925032602-92d5a993a665/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 86 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8= 87 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 88 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 89 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 90 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 91 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 92 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 93 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 95 | gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= 96 | gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 97 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 98 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 99 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 100 | -------------------------------------------------------------------------------- /images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustTalDevelops/go-hcaptcha/acb9f17274f7482b940b5293162ea4052a7f7dfa/images/example.png -------------------------------------------------------------------------------- /images/one_vs_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustTalDevelops/go-hcaptcha/acb9f17274f7482b940b5293162ea4052a7f7dfa/images/one_vs_one.png -------------------------------------------------------------------------------- /screen/curve.go: -------------------------------------------------------------------------------- 1 | package screen 2 | 3 | import ( 4 | "github.com/go-gl/mathgl/mgl64" 5 | "github.com/justtaldevelops/go-hcaptcha/utils" 6 | "math" 7 | "math/rand" 8 | ) 9 | 10 | // Curve is used for generating a curve similar to what a human would make when moving a mouse cursor, 11 | // starting and ending at given positions. 12 | type Curve struct { 13 | fromPoint, toPoint mgl64.Vec2 14 | points []mgl64.Vec2 15 | } 16 | 17 | // NewCurve creates a new Curve. 18 | func NewCurve(fromPoint, toPoint mgl64.Vec2, opts *CurveOpts) *Curve { 19 | h := &Curve{ 20 | fromPoint: fromPoint, 21 | toPoint: toPoint, 22 | } 23 | h.points = h.generateCurve(opts) 24 | return h 25 | } 26 | 27 | // FromPoint returns the starting point of the curve. 28 | func (h *Curve) FromPoint() mgl64.Vec2 { 29 | return h.fromPoint 30 | } 31 | 32 | // ToPoint returns the ending point of the curve. 33 | func (h *Curve) ToPoint() mgl64.Vec2 { 34 | return h.toPoint 35 | } 36 | 37 | // Points returns the points of the curve. 38 | func (h *Curve) Points() []mgl64.Vec2 { 39 | return h.points 40 | } 41 | 42 | // generateCurve generates a curve according to the parameters in CurveOpts. 43 | func (h *Curve) generateCurve(opts *CurveOpts) []mgl64.Vec2 { 44 | h.defaultCurveOpts(opts) 45 | 46 | offsetBoundaryX := *opts.OffsetBoundaryX 47 | offsetBoundaryY := *opts.OffsetBoundaryY 48 | 49 | leftBoundary := *opts.LeftBoundary - offsetBoundaryX 50 | rightBoundary := *opts.RightBoundary + offsetBoundaryX 51 | downBoundary := *opts.DownBoundary - offsetBoundaryY 52 | upBoundary := *opts.UpBoundary + offsetBoundaryY 53 | count := *opts.KnotsCount 54 | distortionMean := *opts.DistortionMean 55 | distortionStdDev := *opts.DistortionStdDev 56 | distortionFrequency := *opts.DistortionFrequency 57 | targetPoints := *opts.TargetPoints 58 | 59 | internalKnots := h.generateInternalKnots(leftBoundary, rightBoundary, downBoundary, upBoundary, count) 60 | points := h.generatePoints(internalKnots) 61 | points = h.distortPoints(points, distortionMean, distortionStdDev, distortionFrequency) 62 | points = h.tweenPoints(points, opts.Tween, targetPoints) 63 | return points 64 | } 65 | 66 | // generateInternalKnots generates the internal knots for the curve. 67 | func (*Curve) generateInternalKnots(leftBoundary, rightBoundary, downBoundary, upBoundary, knotsCount int) []mgl64.Vec2 { 68 | if knotsCount < 0 { 69 | panic("knotsCount can't be negative") 70 | } 71 | if leftBoundary > rightBoundary { 72 | panic("leftBoundary must be less than or equal to rightBoundary") 73 | } 74 | if downBoundary > upBoundary { 75 | panic("downBoundary must be less than or equal to upBoundary") 76 | } 77 | 78 | knotsX := knots(leftBoundary, rightBoundary, knotsCount) 79 | knotsY := knots(downBoundary, upBoundary, knotsCount) 80 | return merge(knotsX, knotsY) 81 | } 82 | 83 | // generatePoints generates Bézier curve points on a curve, according to the internal knots passed as parameter. 84 | func (h *Curve) generatePoints(knots []mgl64.Vec2) []mgl64.Vec2 { 85 | midPointsCount := int(math.Max( 86 | math.Max( 87 | math.Abs(h.fromPoint.X()-h.toPoint.X()), 88 | math.Abs(h.fromPoint.Y()-h.toPoint.Y()), 89 | ), 2, 90 | )) 91 | 92 | knots = append([]mgl64.Vec2{h.fromPoint}, append(knots, h.toPoint)...) 93 | return mgl64.MakeBezierCurve2D(midPointsCount, knots) 94 | } 95 | 96 | // distortPoints distorts the curve described by the points, so that the curve is not ideally smooth. Distortion 97 | // happens by randomly, according to normal distribution, adding an offset to some points. 98 | func (h *Curve) distortPoints(points []mgl64.Vec2, distortionMean, distortionStdDev, distortionFrequency float64) []mgl64.Vec2 { 99 | if distortionFrequency < 0 || distortionFrequency > 1 { 100 | panic("distortionFrequency must be between 0 and 1") 101 | } 102 | 103 | distortedPoints := make([]mgl64.Vec2, len(points)) 104 | for i := 1; i < len(points)-1; i++ { 105 | point := points[i] 106 | if utils.Chance(distortionFrequency) { 107 | delta := rand.NormFloat64()*distortionStdDev + distortionMean 108 | distortedPoints[i] = mgl64.Vec2{point.X(), point.Y() + delta} 109 | } else { 110 | distortedPoints[i] = point 111 | } 112 | } 113 | 114 | distortedPoints = append([]mgl64.Vec2{points[0]}, append(distortedPoints, points[len(points)-1])...) 115 | return distortedPoints 116 | } 117 | 118 | // tweenPoints chooses a number of target points from the points according to tween function. This controls the 119 | // velocity of mouse movement. 120 | func (h *Curve) tweenPoints(points []mgl64.Vec2, tween func(float64) float64, targetPoints int) []mgl64.Vec2 { 121 | if targetPoints < 2 { 122 | panic("targetPoints must be at least 2") 123 | } 124 | 125 | var tweenedPoints []mgl64.Vec2 126 | for i := 0; i < targetPoints; i++ { 127 | index := int(tween(float64(i)/(float64(targetPoints)-1)) * (float64(len(points)) - 1)) 128 | tweenedPoints = append(tweenedPoints, points[index]) 129 | } 130 | return tweenedPoints 131 | } 132 | 133 | // defaultTween is the default tween function. It is a quadratic tween function that begins fast and then decelerates. 134 | func defaultTween(n float64) float64 { 135 | if n < 0 || n > 1 { 136 | panic("parameter must be between 0.0 and 1.0") 137 | } 138 | return -n * (n - 2) 139 | } 140 | -------------------------------------------------------------------------------- /screen/curve_test.go: -------------------------------------------------------------------------------- 1 | package screen 2 | 3 | import ( 4 | "github.com/go-gl/mathgl/mgl64" 5 | "github.com/go-vgo/robotgo" 6 | "github.com/justtaldevelops/go-hcaptcha/utils" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestCurve(t *testing.T) { 12 | mousePosX, mousePosY := robotgo.GetMousePos() 13 | start := mgl64.Vec2{float64(mousePosX), float64(mousePosY)} 14 | end := mgl64.Vec2{1187, 719} 15 | 16 | humanCurve := NewCurve(start, end, &CurveOpts{}) 17 | pause := utils.Between(5, 15) 18 | for _, point := range humanCurve.points { 19 | robotgo.MoveMouse(int(point.X()), int(point.Y())) 20 | time.Sleep(time.Duration(pause) * time.Millisecond) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /screen/options.go: -------------------------------------------------------------------------------- 1 | package screen 2 | 3 | import "math" 4 | 5 | // CurveOpts contains options for a curve. 6 | type CurveOpts struct { 7 | // OffsetBoundaryX is the boundary for X of the offset curve. 8 | OffsetBoundaryX *int 9 | // OffsetBoundaryY is the boundary for Y of the offset curve. 10 | OffsetBoundaryY *int 11 | // LeftBoundary is the boundary for the left of the curve. 12 | LeftBoundary *int 13 | // RightBoundary is the boundary for the right of the curve. 14 | RightBoundary *int 15 | // DownBoundary is the boundary for the bottom of the curve. 16 | DownBoundary *int 17 | // UpBoundary is the boundary for the top of the curve. 18 | UpBoundary *int 19 | // KnotsCount is the number of knots in the curve. 20 | KnotsCount *int 21 | // DistortionMean is the mean of the distortion. 22 | DistortionMean *float64 23 | // DistortionStdDev is the standard deviation of the distortion. 24 | DistortionStdDev *float64 25 | // DistortionFrequency is the frequency of the distortion. 26 | DistortionFrequency *float64 27 | // Tween is the function that tweens values. 28 | Tween func(float64) float64 29 | // TargetPoints is the target points of the curve. 30 | TargetPoints *int 31 | } 32 | 33 | // defaultCurveOpts returns the default curve options. 34 | func (h *Curve) defaultCurveOpts(opts *CurveOpts) { 35 | defaultOffsetBoundaryX := 100 36 | if opts.OffsetBoundaryX == nil { 37 | opts.OffsetBoundaryX = &defaultOffsetBoundaryX 38 | } 39 | 40 | defaultOffsetBoundaryY := 100 41 | if opts.OffsetBoundaryY == nil { 42 | opts.OffsetBoundaryY = &defaultOffsetBoundaryY 43 | } 44 | 45 | defaultLeftBoundary := int(math.Min(h.fromPoint.X(), h.toPoint.X())) 46 | if opts.LeftBoundary == nil { 47 | opts.LeftBoundary = &defaultLeftBoundary 48 | } 49 | 50 | defaultRightBoundary := int(math.Max(h.fromPoint.X(), h.toPoint.X())) 51 | if opts.RightBoundary == nil { 52 | opts.RightBoundary = &defaultRightBoundary 53 | } 54 | 55 | defaultDownBoundary := int(math.Min(h.fromPoint.Y(), h.toPoint.Y())) 56 | if opts.DownBoundary == nil { 57 | opts.DownBoundary = &defaultDownBoundary 58 | } 59 | 60 | defaultUpBoundary := int(math.Max(h.fromPoint.Y(), h.toPoint.Y())) 61 | if opts.UpBoundary == nil { 62 | opts.UpBoundary = &defaultUpBoundary 63 | } 64 | 65 | knotsCount := 2 66 | if opts.KnotsCount == nil { 67 | opts.KnotsCount = &knotsCount 68 | } 69 | 70 | distortionMean := 1.0 71 | if opts.DistortionMean == nil { 72 | opts.DistortionMean = &distortionMean 73 | } 74 | 75 | distortionStdDev := 0.6 76 | if opts.DistortionStdDev == nil { 77 | opts.DistortionStdDev = &distortionStdDev 78 | } 79 | 80 | distortionFrequency := 0.5 81 | if opts.DistortionFrequency == nil { 82 | opts.DistortionFrequency = &distortionFrequency 83 | } 84 | 85 | if opts.Tween == nil { 86 | opts.Tween = defaultTween 87 | } 88 | 89 | targetPoints := 300 90 | if opts.TargetPoints == nil { 91 | opts.TargetPoints = &targetPoints 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /screen/utils.go: -------------------------------------------------------------------------------- 1 | package screen 2 | 3 | import ( 4 | "github.com/go-gl/mathgl/mgl64" 5 | "github.com/justtaldevelops/go-hcaptcha/utils" 6 | ) 7 | 8 | // merge does a merge on two int slices into a slice of knots. 9 | func merge(a, b []int) []mgl64.Vec2 { 10 | if len(a) != len(b) { 11 | panic("arguments must be of same length") 12 | } 13 | 14 | r := make([]mgl64.Vec2, len(a), len(a)) 15 | for i, e := range a { 16 | r[i] = mgl64.Vec2{float64(e), float64(b[i])} 17 | } 18 | return r 19 | } 20 | 21 | // knots generates a random choice of knots based on the size provided. 22 | func knots(firstBoundary, secondBoundary, size int) []int { 23 | result := make([]int, 0, size) 24 | for i := 0; i < size; i++ { 25 | result = append(result, utils.Between(firstBoundary, secondBoundary)) 26 | } 27 | return result 28 | } 29 | -------------------------------------------------------------------------------- /solver.go: -------------------------------------------------------------------------------- 1 | package hcaptcha 2 | 3 | import ( 4 | "github.com/justtaldevelops/go-hcaptcha/utils" 5 | ) 6 | 7 | // Solver is an interface to solve hCaptcha tasks. 8 | type Solver interface { 9 | // Solve solves the hCaptcha tasks using the category, question, and the task. If it was successful, 10 | // it returns true, and in all other cases, it returns false. 11 | Solve(category, question string, tasks []Task) []Task 12 | } 13 | 14 | // GuessSolver solves hCaptcha tasks by guessing the solution. 15 | type GuessSolver struct{} 16 | 17 | // Solve ... 18 | func (s *GuessSolver) Solve(_, _ string, tasks []Task) (answers []Task) { 19 | for _, task := range tasks { 20 | if utils.Chance(0.5) { 21 | answers = append(answers, task) 22 | } 23 | } 24 | return answers 25 | } 26 | -------------------------------------------------------------------------------- /utils/data.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | var ( 4 | // FrameSize is the size of the hCaptcha frame. 5 | FrameSize = [2]int{400, 600} 6 | // TileImageSize is the size of the tile image. 7 | TileImageSize = [2]int{123, 123} 8 | // TileImageStartPosition is the start position of the tile image. 9 | TileImageStartPosition = [2]int{11, 130} 10 | // TileImagePadding is the padding between the tile images. 11 | TileImagePadding = [2]int{5, 6} 12 | // VerifyButtonPosition is the position of the verify button. 13 | VerifyButtonPosition = [2]int{314, 559} 14 | 15 | // TilesPerPage is the number of tiles per page. 16 | TilesPerPage = 9 17 | // TilesPerRow is the number of tiles per row. 18 | TilesPerRow = 3 19 | 20 | // Version is the latest supported version. 21 | Version = "48ebaaf" 22 | // AssetVersion is the latest supported version of the assets. 23 | AssetVersion = "2027f8c" 24 | ) 25 | -------------------------------------------------------------------------------- /utils/rand.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // sRand is a random number generator for hCaptchas. 9 | var sRand = rand.New(rand.NewSource(time.Now().UnixNano())) 10 | 11 | // Chance returns true if the given chance is greater than the random number. 12 | func Chance(chance float64) bool { 13 | return sRand.Float64() < chance 14 | } 15 | 16 | // Between returns a number between two numbers. 17 | func Between(min, max int) int { 18 | return sRand.Intn(max-min) + min 19 | } 20 | -------------------------------------------------------------------------------- /utils/widget.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "math/rand" 4 | 5 | // widgetCharacters are the characters used in randomly generated widget IDs. 6 | var widgetCharacters = []rune("abcdefghijkmnopqrstuvwxyz0123456789") 7 | 8 | // WidgetID generates a new random widget ID. 9 | func WidgetID() string { 10 | b := make([]rune, Between(10, 12)) 11 | for i := range b { 12 | b[i] = widgetCharacters[rand.Intn(len(widgetCharacters))] 13 | } 14 | return string(b) 15 | } 16 | --------------------------------------------------------------------------------