-
Notifications
You must be signed in to change notification settings - Fork 0
/
generate.go
304 lines (268 loc) · 8.85 KB
/
generate.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
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
293
294
295
296
297
298
299
300
301
302
303
304
package akamai
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"sync"
)
// GenerateRequest is the API generation request schema.
type GenerateRequest struct {
// UserAgent is the user agent to use when generating sensor data.
//
// Current restrictions apply to this preference. The user agent
// must be a Google Chrome v109 or v110 user agent.
// Callers can use any platform they like, however it is highly
// recommended to use Windows.
UserAgent string `json:"userAgent"`
// Version is the Akamai version to use.
Version Version `json:"version"`
// PageURL is the URL of the page to generate sensor data for.
PageURL string `json:"pageUrl"`
// Abck is the current `_abck` cookie.
Abck string `json:"_abck"`
// BmSz is the current `bm_sz` cookie.
//
// This is optional for every version excluding `2`.
BmSz string `json:"bm_sz,omitempty"`
}
// GenerateResponse is the API generation response schema.
type GenerateResponse struct {
// Payload is the sensor data.
Payload string `json:"payload"`
}
// GenerateSensorData generates sensor data to use to post to an Akamai Bot Manager
// script endpoint to obtain cookies.
//
// It is recommended to use Generate instead as it includes stop signal handling and
// is generally easier to use.
//
// When sending POST requests to generate an `_abck` cookie with the generated sensor data,
// callers SHOULD NOT encode the data as JSON. Akamai Bot Manager sends the payload as JSON,
// but does not properly encode the data as JSON. Because of this, the request body should be
// created as such: `{"sensor_data":"` + <generated sensor data> + `"}`.
// Callers using Generate do not need to worry about this requirement as Generate
// handles this automatically.
func (session Session) GenerateSensorData(ctx context.Context, req *GenerateRequest) (*GenerateResponse, error) {
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://akamai.publicapis.solarsystems.software/v1/sensor/generate", bytes.NewBuffer(encoded))
if err != nil {
return nil, err
}
request.Header.Set("User-Agent", "SolarSystems akamai-sdk-go")
request.Header.Set("x-api-key", session.apiKey)
request.Header.Set("Content-Type", "application/json")
response, err := session.client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
if response.StatusCode != http.StatusCreated {
return nil, ApiOperationError{
StatusCode: response.StatusCode,
Message: GetMessageFromErrorResponse(body),
}
}
var resp GenerateResponse
if err = json.Unmarshal(body, &resp); err != nil {
return nil, err
}
return &resp, nil
}
var (
// ErrInvalidPageURL is an error caused by Session.Generate if the provided page URL is not
// a valid or absolute URL. An absolute URL must contain a scheme and host.
ErrInvalidPageURL = errors.New("akamai-sdk-go: invalid page URL")
)
// Generate generates a set of cookies (_abck, bm_sz, ak_bmsc and possibly others) to use in an HTTP
// request to an API endpoint protected by Akamai Bot Manager. This method handles all possible scenarios
// and outcomes of generation for both the Akamai Bot Manager web SDK ("sensor data") and the pixel challenge
// (if it is present and not solved already). Callers simply have to provide an implementation of DoHttpReqFunc
// and GetCookieFunc; see their documentation for instructions on how to make custom implementations.
//
// Generate makes an HTTP GET request to the given page URL and obtains the required variables from the
// HTML document (pixel challenge script location and web SDK script location). It then makes HTTP requests
// to both scripts, and sends POST requests containing payloads to generate cookies. The pixel challenge
// and sensor data generation happens concurrently, meaning the order of the sent requests may not
// always be the same. Implementations should use a mutex if they need concurrency safety in their implementation
// of DoHttpReqFunc or GetCookieFunc as both functions can be called by multiple goroutines.
//
// Sensor data generation sends a maximum of maxTries requests, after which it gives up. Generation will stop sooner
// if the website uses the stop signal feature; see IsCookieValid for more information.
// Websites typically require one POST request with sensor data from the SolarSystems API to generate a valid _abck
// cookie. Websites with challenges require two. Setting maxTries to two is a reasonable choice.
//
// Generate blocks until solving the pixel challenge and generating an _abck is complete. It is safe for usage
// by multiple goroutines.
//
// Generate panics if doHttpReq or getCookie is nil. pageUrl must also be an absolute URL, and maxTries must be
// a positive, non-zero integer.
func (session Session) Generate(
ctx context.Context,
userAgent,
pageUrl string,
doHttpReq DoHttpReqFunc,
getCookie GetCookieFunc,
maxTries int,
) error {
if doHttpReq == nil {
panic("akamai-sdk-go: nil DoHttpReqFunc passed to Generate")
}
if getCookie == nil {
panic("akamai-sdk-go: nil GetCookieFunc passed to Generate")
}
if maxTries <= 0 {
panic("akamai-sdk-go: maxTries <= 0")
}
// We don't need the parsed URL until later, but we parse it now to ensure it's valid and absolute.
// This will avoid wasting a request if it's invalid.
u, err := url.Parse(pageUrl)
if err != nil {
return err
}
if !u.IsAbs() {
return ErrInvalidPageURL
}
// GET pageUrl
statusCode, pageBody, err := doHttpReq(ctx, OpGetPage, pageUrl, http.MethodGet, nil)
if err == nil && statusCode != http.StatusOK {
err = BadStatusCodeError{StatusCode: statusCode}
}
if err != nil {
return errors.Join(HttpOpError{Op: OpGetPage}, err)
}
// wg is the WaitGroup for all worker goroutines.
var wg sync.WaitGroup
wg.Add(2)
// errs are the errors reported by the workers.
var errs []error
var mu sync.Mutex
// addError appends to errs. It is safe for usage by multiple goroutines.
addError := func(err error) {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
}
// Solve pixel challenge
go func() {
defer wg.Done()
// Get the script's URL and the URL to post the payload to
ok, scriptUrl, postUrl := GetPixelChallengeScriptURL(pageBody)
if !ok {
// Pixel challenge is not present on this page.
return
}
// Get the HTML variable
htmlVar, err := GetPixelChallengeHtmlVar(pageBody)
if err != nil {
addError(err)
return
}
// GET request to pixel script
statusCode, scriptBody, err := doHttpReq(ctx, OpGetPixelChallengeScript, scriptUrl, http.MethodGet, nil)
if err == nil && statusCode != http.StatusOK {
if statusCode == http.StatusNotFound {
// Pixel challenge script returns 404 when the challenge is already solved.
return
}
err = BadStatusCodeError{StatusCode: statusCode}
}
if err != nil {
addError(err)
return
}
// Get dynamic script variable
scriptVar, err := GetPixelChallengeScriptVar(scriptBody)
if err != nil {
addError(err)
return
}
// Generate payload
response, err := session.GeneratePixelPayload(ctx, &PixelSolveRequest{
UserAgent: userAgent,
HtmlVar: htmlVar,
ScriptVar: scriptVar,
})
if err != nil {
addError(err)
return
}
// POST payload
if _, _, err = doHttpReq(
ctx,
OpPostPixelPayload,
postUrl,
http.MethodPost,
bytes.NewBufferString(response.Payload),
); err != nil {
addError(err)
return
}
}()
// Generate _abck
go func() {
defer wg.Done()
// Get script path
ok, scriptPath := GetScriptPath(pageBody)
if !ok {
// If there's no script path on the page then we skip generating.
return
}
// Construct script URL -- scriptPath will always begin with a /
scriptUrl := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, scriptPath)
// GET request to script
statusCode, scriptBody, err := doHttpReq(ctx, OpGetSdkScript, scriptUrl, http.MethodGet, nil)
if err == nil && statusCode != http.StatusOK {
err = BadStatusCodeError{StatusCode: statusCode}
}
if err != nil {
addError(err)
return
}
// Get SDK version
version := GetSdkVersion(scriptBody)
// Generate and post sensor data
for i := 0; i < maxTries; i++ {
request := GenerateRequest{
UserAgent: userAgent,
Version: version,
PageURL: pageUrl,
Abck: getCookie(u, "_abck"),
}
if version == Version2 {
request.BmSz = getCookie(u, "bm_sz")
}
response, err := session.GenerateSensorData(ctx, &request)
if err != nil {
addError(err)
return
}
if _, _, err = doHttpReq(
ctx,
OpPostSensorData,
scriptUrl,
http.MethodPost,
bytes.NewBufferString(fmt.Sprintf(`{"sensor_data":"%s"}`, response.Payload)),
); err != nil {
addError(err)
return
}
if IsCookieValid(getCookie(u, "_abck"), i) {
break
}
}
}()
wg.Wait()
return errors.Join(errs...)
}