-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsolverify.go
More file actions
292 lines (267 loc) · 9.07 KB
/
Copy pathsolverify.go
File metadata and controls
292 lines (267 loc) · 9.07 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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
)
const solverifyURL = "https://solver.solverify.net"
// solverifyProvider solves captchas through Solverify.
type solverifyProvider struct {
apiKey string
}
func (p *solverifyProvider) Name() string { return providerSolverify }
type solverifyCreateRequest struct {
ClientKey string `json:"clientKey"`
Task any `json:"task"`
}
// solverifyTurnstileTask solves the Turnstile widget on Solverify's own
// infrastructure; the token is not IP-bound, so no proxy is passed.
type solverifyTurnstileTask struct {
Type string `json:"type"`
WebsiteURL string `json:"websiteURL"`
WebsiteKey string `json:"websiteKey"`
Action string `json:"action,omitempty"`
CData string `json:"cdata,omitempty"`
}
// solverifyInterstitialTask clears a Cloudflare interstitial in a real browser
// and returns its cookies. Solverify navigates the target itself, so it needs
// no captured HTML — but it does require the proxy the cookie will be bound to,
// and the method the challenged request used. Ports are sent as numeric
// strings, which is what the API expects.
type solverifyInterstitialTask struct {
Type string `json:"type"`
WebsiteURL string `json:"websiteURL"`
UserAgent string `json:"useragent,omitempty"`
// Source asks for the landed page's HTML in solution.html. It is the only
// way to tell a session that was never challenged from one that failed the
// challenge, so it is always requested and reported when no cookie comes
// back.
Source bool `json:"source"`
Method string `json:"method,omitempty"`
ProxyType string `json:"proxyType"`
ProxyAddress string `json:"proxyAddress"`
ProxyPort string `json:"proxyPort"`
ProxyLogin string `json:"proxyLogin,omitempty"`
ProxyPassword string `json:"proxyPassword,omitempty"`
}
type solverifyCreateResponse struct {
ErrorID int `json:"errorId"`
ErrorCode string `json:"errorCode"`
ErrorDescription string `json:"errorDescription"`
TaskID string `json:"taskId"`
}
type solverifyResultRequest struct {
ClientKey string `json:"clientKey"`
TaskID string `json:"taskId"`
}
type solverifyResultResponse struct {
ErrorID int `json:"errorId"`
ErrorCode string `json:"errorCode"`
ErrorDescription string `json:"errorDescription"`
Status string `json:"status"`
Solution struct {
Value string `json:"value"`
Cookies solverifyCookies `json:"cookies"`
UserAgent string `json:"useragent"`
HTML string `json:"html"`
} `json:"solution"`
}
// solverifyCookies is the solved session's cookie jar, documented as a
// name-to-value object. Cookie jars are also commonly serialised as a list of
// cookie objects, so both shapes are accepted rather than failing the whole
// decode over the difference.
type solverifyCookies map[string]string
func (c *solverifyCookies) UnmarshalJSON(data []byte) error {
var byName map[string]string
if err := json.Unmarshal(data, &byName); err == nil {
*c = byName
return nil
}
var list []struct {
Name string `json:"name"`
Value string `json:"value"`
}
if err := json.Unmarshal(data, &list); err != nil {
return fmt.Errorf("cookies is neither an object nor a list: %w", err)
}
jar := make(solverifyCookies, len(list))
for _, cookie := range list {
jar[cookie.Name] = cookie.Value
}
*c = jar
return nil
}
// describeLandedPage summarises the page the solver's browser ended up on.
// That is what separates "Cloudflare never challenged this request" from "the
// request never reached the challenge in the first place".
func describeLandedPage(page string) string {
if page == "" {
return "no page captured"
}
lower := strings.ToLower(page)
for _, marker := range []string{"just a moment", "cf-chl", "challenge-platform"} {
if strings.Contains(lower, marker) {
return "a Cloudflare challenge page"
}
}
return truncate(strings.Join(strings.Fields(page), " "), 160)
}
// names lists the cookies present, for error messages that would otherwise not
// say why a cookie is missing.
func (c solverifyCookies) names() string {
if len(c) == 0 {
return "none"
}
names := make([]string, 0, len(c))
for name := range c {
names = append(names, name)
}
sort.Strings(names)
return strings.Join(names, ", ")
}
// Solverify task states. Anything other than these two means the task is still
// queued or running.
const (
solverifyCompleted = "completed"
solverifyFailed = "failed"
)
// solverifyError renders an API-level failure. Solverify answers with HTTP 200
// even for errors, signalling them through errorId.
func solverifyError(op, code, description string) error {
detail := code
if description != "" {
if detail != "" {
detail += ": "
}
detail += description
}
if detail == "" {
detail = "unknown error"
}
return fmt.Errorf("solverify %s failed (%s)", op, detail)
}
// createTask submits a task and returns its id.
func (p *solverifyProvider) createTask(ctx context.Context, task any) (string, error) {
var resp solverifyCreateResponse
if err := postJSON(ctx, solverifyURL+"/createTask",
solverifyCreateRequest{ClientKey: p.apiKey, Task: task}, &resp); err != nil {
return "", err
}
if resp.ErrorID != 0 {
return "", solverifyError("createTask", resp.ErrorCode, resp.ErrorDescription)
}
if resp.TaskID == "" {
return "", fmt.Errorf("solverify createTask returned no task id")
}
return resp.TaskID, nil
}
// taskResult fetches a task's current state, reporting whether it has finished.
func (p *solverifyProvider) taskResult(ctx context.Context, taskID string) (*solverifyResultResponse, bool, error) {
var resp solverifyResultResponse
if err := postJSON(ctx, solverifyURL+"/getTaskResult",
solverifyResultRequest{ClientKey: p.apiKey, TaskID: taskID}, &resp); err != nil {
return nil, false, err
}
if resp.ErrorID != 0 || resp.Status == solverifyFailed {
return nil, false, solverifyError("getTaskResult", resp.ErrorCode, resp.ErrorDescription)
}
return &resp, resp.Status == solverifyCompleted, nil
}
func (p *solverifyProvider) SolveTurnstile(ctx context.Context, r turnstileRequest) (string, error) {
taskID, err := p.createTask(ctx, solverifyTurnstileTask{
Type: "turnstile",
WebsiteURL: r.PageURL,
WebsiteKey: r.SiteKey,
Action: r.Action,
CData: r.CData,
})
if err != nil {
return "", err
}
var token string
err = pollTask(ctx, func() (bool, error) {
res, done, err := p.taskResult(ctx, taskID)
if err != nil || !done {
return false, err
}
if res.Solution.Value == "" {
return false, fmt.Errorf("solverify returned an empty turnstile token")
}
token = res.Solution.Value
return true, nil
})
if err != nil {
return "", err
}
return token, nil
}
func (p *solverifyProvider) SolveCfClearance(ctx context.Context, r clearanceRequest) (clearanceResult, error) {
if r.ProxyURL == "" {
return clearanceResult{}, fmt.Errorf("cf_clearance requires a proxy (pass -proxy)")
}
proxy, err := parseProxyParts(r.ProxyURL)
if err != nil {
return clearanceResult{}, err
}
if proxy.Scheme != "http" {
return clearanceResult{}, fmt.Errorf("solverify only accepts http proxies, got %q", proxy.Scheme)
}
// Navigate the request that actually carried the challenge — same URL and
// same method. A real browser is served the profile page unchallenged, and
// the analytics endpoint only answers POST, so a GET there misses the rule
// too; either way an unchallenged session is issued no cf_clearance.
target := r.ChallengeURL
if target == "" {
target = r.PageURL
}
method := strings.ToUpper(r.ChallengeMethod)
switch method {
case "":
method = http.MethodGet
case http.MethodGet, http.MethodPost:
default:
return clearanceResult{}, fmt.Errorf("solverify navigates with GET or POST only, got %q", r.ChallengeMethod)
}
taskID, err := p.createTask(ctx, solverifyInterstitialTask{
Type: "interstitial",
WebsiteURL: target,
UserAgent: r.UserAgent,
Source: true,
Method: method,
ProxyType: proxy.Scheme,
ProxyAddress: proxy.Host,
ProxyPort: strconv.Itoa(proxy.Port),
ProxyLogin: proxy.Login,
ProxyPassword: proxy.Password,
})
if err != nil {
return clearanceResult{}, err
}
var result clearanceResult
err = pollTask(ctx, func() (bool, error) {
res, done, err := p.taskResult(ctx, taskID)
if err != nil || !done {
return false, err
}
cookie := res.Solution.Cookies["cf_clearance"]
if cookie == "" {
// Cloudflare only mints cf_clearance when it actually challenges,
// so an unchallenged navigation completes with a cookie jar that
// simply has none. Name what did come back — that distinguishes it
// from a genuinely failed solve.
return false, fmt.Errorf("solverify navigated %s %s without being issued a cf_clearance cookie "+
"(cookies: %s; landed on: %s)",
method, target, res.Solution.Cookies.names(), describeLandedPage(res.Solution.HTML))
}
result = clearanceResult{Cookie: cookie, UserAgent: res.Solution.UserAgent}
return true, nil
})
if err != nil {
return clearanceResult{}, err
}
return result, nil
}