├── .gitignore
├── LICENSE
├── README.md
├── akamai.go
├── akamai
├── internal
│ ├── radix.go
│ └── radix_test.go
├── pixel.go
├── pixel_test.go
├── script_path.go
├── script_path_test.go
├── sec_cpt.go
├── sec_cpt_easyjson.go
├── sec_cpt_test.go
├── stop_signal.go
└── stop_signal_test.go
├── api.go
├── datadome.go
├── datadome
└── parse.go
├── go.mod
├── go.sum
├── incapsula.go
├── incapsula
├── dynamic.go
├── dynamic_test.go
├── utmvc.go
└── utmvc_test.go
├── internal
├── decompress.go
└── zstd.go
├── kasada.go
├── kasada
└── script_path.go
├── models.go
├── models_easyjson.go
└── session.go
/.gitignore:
--------------------------------------------------------------------------------
1 | /scripts/*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Hyper Solutions
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hyper Solutions SDK
2 |
3 | ## Installation
4 |
5 | To use the Hyper Solutions SDK in your Go project, you need to install it using the following command:
6 |
7 | ```
8 | go get github.com/Hyper-Solutions/hyper-sdk-go/v2
9 | ```
10 |
11 | ## Usage
12 |
13 | ### Creating a Session
14 |
15 | To start using the SDK, you need to create a new `Session` instance by providing your API key:
16 |
17 | ```go
18 | session := hyper.NewSession("your-api-key")
19 | ```
20 |
21 | You can also optionally set a JWT key and a custom HTTP client:
22 |
23 | ```go
24 | session := hyper.NewSession("your-api-key").
25 | WithJwtKey("your-jwt-key").
26 | WithClient(customHTTPClient)
27 | ```
28 |
29 | ## Akamai
30 |
31 | The Akamai package provides functions for interacting with Akamai Bot Manager, including generating sensor data, parsing script path, parsing pixel challenges, and handling sec-cpt challenges.
32 |
33 | ### Generating Sensor Data
34 |
35 | To generate sensor data required for generating valid Akamai cookies, use the `GenerateSensorData` function:
36 |
37 | ```go
38 | sensorData, err := session.GenerateSensorData(ctx, &hyper.SensorInput{
39 | // Set the required input fields
40 | })
41 | if err != nil {
42 | // Handle the error
43 | }
44 | ```
45 |
46 | ### Parsing Script Path
47 |
48 | To parse the Akamai Bot Manager script path from the given HTML code, use the `ParseScriptPath` function:
49 |
50 | ```go
51 | scriptPath, err := akamai.ParseScriptPath(reader)
52 | if err != nil {
53 | // Handle the error
54 | }
55 | ```
56 |
57 | ### Handling Sec-Cpt Challenges
58 |
59 | The Akamai package provides functions for handling sec-cpt challenges:
60 |
61 | - `ParseSecCptChallenge`: Parses a sec-cpt challenge from an `io.Reader`.
62 | - `ParseSecCptChallengeFromJson`: Parses a sec-cpt challenge from an `io.Reader`.
63 | - `GenerateSecCptPayload`: Generates a sec-cpt payload using the provided sec-cpt cookie.
64 | - `Sleep`: Sleeps for the duration specified in the sec-cpt challenge.
65 | - `SleepWithContext`: Sleeps for the duration specified in the sec-cpt challenge, this is context aware.
66 |
67 | ### Validating Cookies
68 |
69 | The Akamai package provides functions for validating cookies:
70 |
71 | - `IsCookieValid`: Determines if the provided `_abck` cookie value is valid based on the given request count.
72 | - `IsCookieInvalidated`: Determines if the current session requires more sensors to be sent.
73 |
74 |
75 | ### Generating Pixel Data
76 |
77 | To generate pixel data, use the `GeneratePixelData` function:
78 |
79 | ```go
80 | pixelData, err := session.GeneratePixelData(ctx, &hyper.PixelInput{
81 | // Set the required input fields
82 | })
83 | if err != nil {
84 | // Handle the error
85 | }
86 | ```
87 |
88 | ### Parsing Pixel Challenges
89 |
90 | The Akamai package provides functions for parsing pixel challenges:
91 |
92 | - `ParsePixelHtmlVar`: Parses the required pixel challenge variable from the given HTML code.
93 | - `ParsePixelScriptURL`: Parses the script URL of the pixel challenge script and the URL to post a generated payload to from the given HTML code.
94 | - `ParsePixelScriptVar`: Parses the dynamic value from the pixel script.
95 | ## Incapsula
96 |
97 | The Incapsula package provides functions for interacting with Incapsula, including generating Reese84 sensor data, UTMVC cookies, and parsing UTMVC script paths.
98 |
99 | ### Generating Reese84 Sensor
100 |
101 | To generate sensor data required for generating valid Reese84 cookies, use the `GenerateReese84Sensor` function:
102 |
103 | ```go
104 | sensorData, err := session.GenerateReese84Sensor(ctx, site, userAgent)
105 | if err != nil {
106 | // Handle the error
107 | }
108 | ```
109 |
110 | ### Generating UTMVC Cookie
111 |
112 | To generate the UTMVC cookie using the Hyper Solutions API, use the `GenerateUtmvcCookie` function:
113 |
114 | ```go
115 | utmvcCookie, err := session.GenerateUtmvcCookie(ctx, &hyper.UtmvcInput{
116 | Script: "your-script",
117 | SessionIds: []string{"session-id-1", "session-id-2"},
118 | UserAgent: "user-agent-here"
119 | })
120 | if err != nil {
121 | // Handle the error
122 | }
123 | ```
124 |
125 | ### Parsing UTMVC Script Path
126 |
127 | To parse the UTMVC script path from a given script content, use the `ParseUtmvcScriptPath` function:
128 |
129 | ```go
130 | scriptPath, err := incapsula.ParseUtmvcScriptPath(scriptReader)
131 | if err != nil {
132 | // Handle the error
133 | }
134 | ```
135 |
136 | ### Generating UTMVC Submit Path
137 |
138 | To generate a unique UTMVC submit path with a random query parameter, use the `GetUtmvcSubmitPath` function:
139 |
140 | ```go
141 | submitPath := incapsula.GetUtmvcSubmitPath()
142 | ```
143 |
144 | ## Kasada
145 |
146 | The Kasada package provides functions for interacting with Kasada Bot Manager, including parsing script path.
147 |
148 | ### Generating Payload Data (CT)
149 |
150 | To generate payload data required for generating valid `x-kpsdk-ct` tokens, use the `GenerateKasadaPayload` function:
151 |
152 | ```go
153 | payload, headers, err := session.GenerateKasadaPayload(ctx, &hyper.KasadaPayloadInput{
154 | // Set the required input fields
155 | })
156 | if err != nil {
157 | // Handle the error
158 | }
159 | ```
160 |
161 | ### Generating Pow Data (CD)
162 |
163 | To generate POW data (`x-kpsdk-cd`) tokens, use the `GenerateKasadaPow` function:
164 |
165 | ```go
166 | payload, err := session.GenerateKasadaPow(ctx, &hyper.KasadaPowInput{
167 | // Set the required input fields
168 | })
169 | if err != nil {
170 | // Handle the error
171 | }
172 | ```
173 |
174 | ### Parsing Script Path
175 |
176 | To parse the Kasada script path from the given blocked page (status code 429) HTML code, use the `ParseScriptPath` function:
177 |
178 | ```go
179 | scriptPath, err := kasada.ParseScriptPath(reader)
180 | if err != nil {
181 | // Handle the error
182 | }
183 | // will look like: /ips.js?...
184 | ```
185 |
186 | ## DataDome
187 |
188 | The DataDome package provides functions for interacting with DataDome Bot Manager, including parsing device link URLs
189 | for interstitial and slider.
190 |
191 | ### Generating Interstitial Payload
192 |
193 | To generate payload data required for solving interstitial, use the `GenerateDataDomeInterstitial` function:
194 |
195 | ```go
196 | payload, headers, err := session.GenerateDataDomeInterstitial(ctx, &hyper.DataDomeInterstitialInput{
197 | // Set the required input fields
198 | })
199 | if err != nil {
200 | // Handle the error
201 | }
202 | // Use the payload to POST to https://geo.captcha-delivery.com/interstitial/
203 | ```
204 |
205 | ### Generating Slider Payload
206 |
207 | To solve DataDome Slider, use the `GenerateDataDomeSlider` function:
208 |
209 | ```go
210 | checkUrl, headers, err := session.GenerateDataDomeSlider(ctx, &hyper.DataDomeSliderInput{
211 | // Set the required input fields
212 | })
213 | if err != nil {
214 | // Handle the error
215 | }
216 | // Create a GET request to the checkUrl
217 | ```
218 |
219 | ### Parsing Interstitial DeviceLink URL
220 |
221 | To parse the Interstitial DeviceLink URL from the HTML code, use the `ParseInterstitialDeviceCheckLink` function:
222 |
223 | ```go
224 | deviceLink, err := datadome.ParseInterstitialDeviceCheckLink(reader, datadomeCookie, referer)
225 | if err != nil {
226 | // Handle the error
227 | }
228 | // deviceLink will look like: https://geo.captcha-delivery.com/interstitial/?...
229 | ```
230 |
231 | ### Parsing Slider DeviceLink URL
232 |
233 | To parse the Slider DeviceLink URL from the HTML code, use the `ParseSliderDeviceCheckLink` function:
234 |
235 | ```go
236 | deviceLink, err := datadome.ParseSliderDeviceCheckLink(reader, datadomeCookie, referer)
237 | if err != nil {
238 | // Handle the error
239 | }
240 | // deviceLink will look like: https://geo.captcha-delivery.com/captcha/?...
241 | ```
242 |
243 | ## Contributing
244 |
245 | If you find any issues or have suggestions for improvement, please open an issue or submit a pull request.
246 |
247 | ## License
248 |
249 | This SDK is licensed under the [MIT License](LICENSE).
250 |
--------------------------------------------------------------------------------
/akamai.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | )
7 |
8 | // GenerateSensorData returns the sensor data required to generate valid akamai cookies using the Hyper Solutions API.
9 | func (s *Session) GenerateSensorData(ctx context.Context, input *SensorInput) (string, string, error) {
10 | response, err := sendRequest[*SensorInput, *apiResponse](ctx, s, "https://akm.hypersolutions.co/v2/sensor", input)
11 | if err != nil {
12 | return "", "", err
13 | }
14 | if response.Error != "" {
15 | return "", "", fmt.Errorf("api returned with: %s", response.Error)
16 | }
17 |
18 | return response.Payload, response.Context, nil
19 | }
20 |
21 | // ParseV3Dynamic returns the dynamic values for a v3 dynamic script
22 | func (s *Session) ParseV3Dynamic(ctx context.Context, input *DynamicInput) (string, error) {
23 | response, err := sendRequest[*DynamicInput, *apiResponse](ctx, s, "https://akm.hypersolutions.co/v3dynamic", input)
24 | if err != nil {
25 | return "", err
26 | }
27 | if response.Error != "" {
28 | return "", fmt.Errorf("api returned with: %s", response.Error)
29 | }
30 |
31 | return response.Payload, nil
32 | }
33 |
34 | // GeneratePixelData returns the pixel data using the Hyper Solutions API.
35 | func (s *Session) GeneratePixelData(ctx context.Context, input *PixelInput) (string, error) {
36 | response, err := sendRequest[*PixelInput, *apiResponse](ctx, s, "https://akm.hypersolutions.co/pixel", input)
37 | if err != nil {
38 | return "", err
39 | }
40 | if response.Error != "" {
41 | return "", fmt.Errorf("api returned with: %s", response.Error)
42 | }
43 |
44 | return response.Payload, nil
45 | }
46 |
47 | // GenerateSbsdData returns the sbsd payload using the Hyper Solutions API.
48 | func (s *Session) GenerateSbsdData(ctx context.Context, input *SbsdInput) (string, error) {
49 | response, err := sendRequest[*SbsdInput, *apiResponse](ctx, s, "https://akm.hypersolutions.co/sbsd", input)
50 | if err != nil {
51 | return "", err
52 | }
53 | if response.Error != "" {
54 | return "", fmt.Errorf("api returned with: %s", response.Error)
55 | }
56 |
57 | return response.Payload, nil
58 | }
59 |
--------------------------------------------------------------------------------
/akamai/internal/radix.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "math"
5 | )
6 |
7 | const (
8 | kBufferSize = 2200
9 | // 0x7FF0'0000'0000'0000
10 | kExponentMask = 9218868437227405312
11 | kPhysicalSignificandSize = 52
12 | kExponentBias = 0x3FF + kPhysicalSignificandSize
13 | chars = "0123456789abcdefghijklmnopqrstuvwxyz"
14 | )
15 |
16 | // FloatToStringRadix Translated from https://github.com/v8/v8/blob/master/src/numbers/conversions.cc#L1269
17 | // and https://github.com/v8/v8/blob/master/src/numbers/double.h
18 | func FloatToStringRadix(value float64, radix int, buf []byte) int {
19 | buffer := make([]byte, kBufferSize)
20 | integerCursor := kBufferSize / 2
21 | fractionCursor := integerCursor
22 |
23 | negative := value < 0
24 | if negative {
25 | value = -value
26 | }
27 |
28 | integer := math.Floor(value)
29 | fraction := value - integer
30 | delta := 0.5 * (double(value).NextDouble() - value)
31 | delta = math.Max(double(0.0).NextDouble(), delta)
32 |
33 | if fraction >= delta {
34 | buffer[fractionCursor] = 46
35 | fractionCursor++
36 |
37 | for {
38 | fraction *= float64(radix)
39 | delta *= float64(radix)
40 |
41 | digit := int(fraction)
42 | buffer[fractionCursor] = chars[digit]
43 | fractionCursor++
44 |
45 | fraction -= float64(digit)
46 |
47 | if fraction > 0.5 || (fraction == 0.5 && ((digit & 1) != 0)) {
48 | if fraction+delta > 1 {
49 | for {
50 | fractionCursor--
51 | if fractionCursor == kBufferSize/2 {
52 | integer += 1
53 | break
54 | }
55 |
56 | c := buffer[fractionCursor]
57 | var digit byte
58 | if c > 57 {
59 | digit = c - 97 + 10
60 | } else {
61 | digit = c - 48
62 | }
63 |
64 | if int(digit+1) < radix {
65 | buffer[fractionCursor] = chars[digit+1]
66 | fractionCursor++
67 | break
68 | }
69 | }
70 | break
71 | }
72 | }
73 |
74 | if !(fraction >= delta) {
75 | break
76 | }
77 | }
78 | }
79 |
80 | for double(integer/float64(radix)).Exponent() > 0 {
81 | integer /= float64(radix)
82 | integerCursor--
83 | buffer[integerCursor] = 48
84 | }
85 |
86 | for {
87 | remainder := math.Remainder(integer, float64(radix))
88 | integerCursor--
89 | buffer[integerCursor] = chars[int(remainder)]
90 | integer = (integer - remainder) / float64(radix)
91 |
92 | if !(integer > 0) {
93 | break
94 | }
95 | }
96 |
97 | if negative {
98 | integerCursor--
99 | buffer[integerCursor] = 45
100 | }
101 |
102 | //copy(buf[offset:], buffer[integerCursor:fractionCursor])
103 | copy(buf, buffer[integerCursor:fractionCursor])
104 | return fractionCursor - integerCursor
105 | }
106 |
107 | type double float64
108 |
109 | func (d double) AsUint64() uint64 {
110 | return math.Float64bits(float64(d))
111 | }
112 |
113 | func (d double) NextDouble() float64 {
114 | d64 := d.AsUint64()
115 |
116 | // When the original float is negative, you must decrement
117 | // the uint to get the next greater double when converting back
118 | if d < 0 {
119 | return math.Float64frombits(d64 - 1)
120 | }
121 |
122 | return math.Float64frombits(d64 + 1)
123 | }
124 |
125 | func (d double) Exponent() int {
126 | d64 := d.AsUint64()
127 |
128 | biasedE := math.Float64bits(float64((d64 & kExponentMask) >> kPhysicalSignificandSize))
129 | return int(biasedE) - kExponentBias
130 | }
131 |
--------------------------------------------------------------------------------
/akamai/internal/radix_test.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "math/rand"
5 | "testing"
6 | )
7 |
8 | const correctOutput = "0.da49605db64e5"
9 |
10 | func TestFloatToStringRadix16(t *testing.T) {
11 | buf := make([]byte, 18)
12 | length := FloatToStringRadix(0.852682135466517, 16, buf)
13 | v := string(buf[:length])
14 | if v != correctOutput {
15 | t.Errorf("expected %v, got: %v", correctOutput, v)
16 | }
17 | }
18 |
19 | func BenchmarkFloatToStringRadix16(b *testing.B) {
20 | buf := make([]byte, 18)
21 |
22 | b.ReportAllocs()
23 | for n := 0; n < b.N; n++ {
24 | FloatToStringRadix(rand.Float64(), 16, buf)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/akamai/pixel.go:
--------------------------------------------------------------------------------
1 | package akamai
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "regexp"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | var (
12 | pixelHtmlExpr = regexp.MustCompile(`bazadebezolkohpepadr="(\d+)"`)
13 | pixelScriptUrlExpr = regexp.MustCompile(`src="(https?://.+/akam/\d+/\w+)"`)
14 | pixelScriptVarExpr = regexp.MustCompile(`g=_\[(\d+)]`)
15 | pixelScriptStringArrayExpr = regexp.MustCompile(`var _=\[(.+)];`)
16 | pixelScriptStringsExpr = regexp.MustCompile(`("[^",]*")`)
17 |
18 | ErrPixelHtmlVarNotFound = errors.New("hyper-sdk: pixel HTML var not found")
19 | ErrPixelScriptUrlNotFound = errors.New("hyper-sdk: script URL not found")
20 | ErrPixelScriptVarNotFound = errors.New("hyper-sdk: script var not found")
21 | )
22 |
23 | // ParsePixelHtmlVar gets the required pixel challenge variable from the given HTML code src.
24 | func ParsePixelHtmlVar(reader io.Reader) (int, error) {
25 | src, err := io.ReadAll(reader)
26 | if err != nil {
27 | return 0, errors.Join(ErrPixelHtmlVarNotFound, err)
28 | }
29 |
30 | matches := pixelHtmlExpr.FindSubmatch(src)
31 | if len(matches) < 2 {
32 | return 0, ErrPixelHtmlVarNotFound
33 | }
34 |
35 | if v, err := strconv.Atoi(string(matches[1])); err == nil {
36 | return v, nil
37 | } else {
38 | return 0, errors.Join(ErrPixelHtmlVarNotFound, err)
39 | }
40 | }
41 |
42 | // ParsePixelScriptURL gets the script URL of the pixel challenge script and the URL
43 | // to post a generated payload to from the given HTML code src.
44 | func ParsePixelScriptURL(reader io.Reader) (string, string, error) {
45 | src, err := io.ReadAll(reader)
46 | if err != nil {
47 | return "", "", errors.Join(ErrPixelScriptUrlNotFound, err)
48 | }
49 |
50 | matches := pixelScriptUrlExpr.FindSubmatch(src)
51 | if len(matches) < 2 {
52 | return "", "", errors.Join(ErrPixelScriptUrlNotFound, err)
53 | }
54 |
55 | scriptUrl := string(matches[1])
56 |
57 | // Create postUrl
58 | parts := strings.Split(scriptUrl, "/")
59 | parts[len(parts)-1] = "pixel_" + parts[len(parts)-1]
60 | postUrl := strings.Join(parts, "/")
61 |
62 | return scriptUrl, postUrl, nil
63 | }
64 |
65 | // ParsePixelScriptVar gets the dynamic value from the pixel script
66 | func ParsePixelScriptVar(reader io.Reader) (string, error) {
67 | src, err := io.ReadAll(reader)
68 | if err != nil {
69 | return "", errors.Join(ErrPixelScriptVarNotFound, err)
70 | }
71 |
72 | index := pixelScriptVarExpr.FindSubmatch(src)
73 | if len(index) < 2 {
74 | return "", ErrPixelScriptVarNotFound
75 | }
76 | stringIndex, err := strconv.Atoi(string(index[1]))
77 | if err != nil {
78 | return "", ErrPixelScriptVarNotFound
79 | }
80 |
81 | arrayDeclaration := pixelScriptStringArrayExpr.FindSubmatch(src)
82 | if len(arrayDeclaration) < 2 {
83 | return "", ErrPixelScriptVarNotFound
84 | }
85 |
86 | rawStrings := pixelScriptStringsExpr.FindAllSubmatch(arrayDeclaration[1], -1)
87 | if stringIndex >= len(rawStrings) {
88 | return "", ErrPixelScriptVarNotFound
89 | }
90 |
91 | if len(rawStrings[stringIndex]) < 2 {
92 | return "", ErrPixelScriptVarNotFound
93 | }
94 |
95 | if v, err := strconv.Unquote(string(rawStrings[stringIndex][1])); err == nil {
96 | return v, nil
97 | } else {
98 | return "", errors.Join(ErrPixelScriptVarNotFound, err)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/akamai/pixel_test.go:
--------------------------------------------------------------------------------
1 | package akamai
2 |
3 | import (
4 | "io"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | func TestParsePixelHtmlVar(t *testing.T) {
10 | if v, err := ParsePixelHtmlVar(strings.NewReader(``)); err != nil {
11 | t.Fatal(err)
12 | } else {
13 | t.Log(v)
14 | }
15 | }
16 |
17 | func TestParsePixelScriptURL(t *testing.T) {
18 | if scriptUrl, postUrl, err := ParsePixelScriptURL(strings.NewReader(`src="https://www.example.com/akam/13/c88db65"`)); err != nil {
19 | t.Fatal(err)
20 | } else {
21 | t.Log(scriptUrl, postUrl)
22 | }
23 | }
24 |
25 | func BenchmarkParsePixelScriptURL(b *testing.B) {
26 | script := strings.NewReader(`src="https://www.example.com/akam/13/c88db65"`)
27 |
28 | b.ResetTimer()
29 | b.ReportAllocs()
30 | for i := 0; i < b.N; i++ {
31 | _, _, err := ParsePixelScriptURL(script)
32 | if err != nil {
33 | b.Fatal(err)
34 | }
35 |
36 | script.Seek(0, io.SeekStart)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/akamai/script_path.go:
--------------------------------------------------------------------------------
1 | package akamai
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "regexp"
7 | )
8 |
9 | var (
10 | scriptPathExpr = regexp.MustCompile(``)
11 |
12 | ErrScriptPathNotFound = errors.New("hyper-sdk: script path not found")
13 | )
14 |
15 | // ParseScriptPath gets the Akamai Bot Manager web SDK path from the given HTML code src.
16 | func ParseScriptPath(reader io.Reader) (string, error) {
17 | src, err := io.ReadAll(reader)
18 | if err != nil {
19 | return "", errors.Join(ErrScriptPathNotFound, err)
20 | }
21 |
22 | matches := scriptPathExpr.FindSubmatch(src)
23 | if len(matches) < 2 {
24 | return "", ErrScriptPathNotFound
25 | }
26 |
27 | return string(matches[1]), nil
28 | }
29 |
--------------------------------------------------------------------------------
/akamai/script_path_test.go:
--------------------------------------------------------------------------------
1 | package akamai
2 |
3 | import (
4 | "io"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | func TestParseScriptPath(t *testing.T) {
10 | path, err := ParseScriptPath(strings.NewReader(``))
11 | if err != nil {
12 | t.Fatal(err)
13 | }
14 |
15 | t.Log(path)
16 | }
17 |
18 | func BenchmarkParseScriptPath(b *testing.B) {
19 | script := strings.NewReader(``)
20 |
21 | b.ResetTimer()
22 | b.ReportAllocs()
23 | for i := 0; i < b.N; i++ {
24 | _, err := ParseScriptPath(script)
25 | if err != nil {
26 | b.Fatal(err)
27 | }
28 |
29 | script.Seek(0, io.SeekStart)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/akamai/sec_cpt.go:
--------------------------------------------------------------------------------
1 | package akamai
2 |
3 | import (
4 | "context"
5 | "crypto/sha256"
6 | "encoding/base64"
7 | "errors"
8 | "github.com/mailru/easyjson"
9 | "io"
10 | "math/rand"
11 | "regexp"
12 | "strconv"
13 | "strings"
14 | "time"
15 |
16 | "github.com/Hyper-Solutions/hyper-sdk-go/v2/akamai/internal"
17 | "github.com/Hyper-Solutions/orderedobject"
18 | jsoniter "github.com/json-iterator/go"
19 | )
20 |
21 | var (
22 | secDurationExpr = regexp.MustCompile(`data-duration=(\d+)`)
23 | secChallengeExpr = regexp.MustCompile(`challenge="(.*?)"`)
24 | secPageExpr = regexp.MustCompile(`data-duration=\d+\s+src="([^"]+)"`)
25 |
26 | ErrSecCptParsing = errors.New("hyper-sdk: error parsing sec-cpt")
27 | ErrSecCptInvalidCookie = errors.New("hyper-sdk: malformed sec_cpt cookie")
28 | )
29 |
30 | type SecCptChallenge struct {
31 | ChallengePath string
32 |
33 | duration int
34 | challengeData *secCptChallengeData
35 | }
36 |
37 | //easyjson:json
38 | type secCptChallengeData struct {
39 | Token string `json:"token"`
40 | Timestamp int `json:"timestamp"`
41 | Nonce string `json:"nonce"`
42 | Difficulty int `json:"difficulty"`
43 | Count int `json:"count"`
44 | Timeout int `json:"timeout"`
45 | CPU bool `json:"cpu"`
46 | VerifyURL string `json:"verify_url"`
47 | }
48 |
49 | //easyjson:json
50 | type secCptApiResponse struct {
51 | SecCpChallenge string `json:"sec-cp-challenge"`
52 | Provider string `json:"provider"`
53 | BrandingURLContent string `json:"branding_url_content"`
54 | ChlgDuration int `json:"chlg_duration"`
55 | Token string `json:"token"`
56 | Timestamp int `json:"timestamp"`
57 | Nonce string `json:"nonce"`
58 | Difficulty int `json:"difficulty"`
59 | Timeout int `json:"timeout"`
60 | CPU bool `json:"cpu"`
61 | }
62 |
63 | // ParseSecCptChallenge parses a sec-cpt challenge from an io.Reader.
64 | //
65 | // The function extracts the challenge data, duration, and challenge path from the provided HTML content.
66 | // It returns a *SecCptChallenge struct containing the parsed information and any error encountered during parsing.
67 | //
68 | // Example usage:
69 | //
70 | // html := ``
71 | // challenge, err := ParseSecCptChallenge(strings.NewReader(html))
72 | // if err != nil {
73 | // // Handle the error
74 | // }
75 | //
76 | // Parameters:
77 | // - reader: An io.Reader containing the HTML content with the sec-cpt challenge.
78 | //
79 | // Returns:
80 | // - *SecCptChallenge: A pointer to a SecCptChallenge struct containing the parsed challenge data, duration, and challenge path.
81 | // - error: An error encountered during parsing, or nil if parsing was successful.
82 | //
83 | // Errors:
84 | // - ErrSecCptParsing: Returned when there is an error parsing the sec-cpt challenge data.
85 | // - Other errors may be returned by the underlying io.Reader or JSON unmarshaling.
86 | func ParseSecCptChallenge(html io.Reader) (*SecCptChallenge, error) {
87 | src, err := io.ReadAll(html)
88 | if err != nil {
89 | return nil, errors.Join(ErrSecCptParsing, err)
90 | }
91 |
92 | challengeData, err := parseSecCptChallengeData(src)
93 | if err != nil {
94 | return nil, err
95 | }
96 |
97 | duration, err := parseSecCptDuration(src)
98 | if err != nil {
99 | return nil, err
100 | }
101 |
102 | challengePath, err := parseSecCptChallengePath(src)
103 | if err != nil {
104 | return nil, err
105 | }
106 |
107 | return &SecCptChallenge{
108 | challengeData: challengeData,
109 | duration: duration,
110 | ChallengePath: challengePath,
111 | }, nil
112 | }
113 |
114 | // ParseSecCptChallengeFromJson parses a sec-cpt challenge from a JSON payload.
115 | //
116 | // The function takes an io.Reader containing the JSON payload of the sec-cpt challenge
117 | // and unmarshals it into a secCptApiResponse struct. It then extracts the necessary
118 | // information from the struct to create a SecCptChallenge struct.
119 | //
120 | // Example usage:
121 | //
122 | // jsonPayload := `{"sec-cp-challenge":"true","provider":"crypto","branding_url_content":"/_sec/cp_challenge/crypto_message-4-3.htm","chlg_duration":30,"token":"AAQAAAAJ____9z_ZPsdHbk36hg2f6np2sGJDXmkwGmBiMBr_DDEmSWfi8Zt7BdtjWrNd9KD4DS_vim0VnK2wsa8tIC7XWsCshkvDF9J9Rf5EFwBU00c6SMXTaSNSTcDR-HVFGp3uAa67Mb3I6HeifXbjALcEomjcnwa9ZNQdDWuTAUTgNGbYw09A8AXIuP9DNv3QktUx488FV38Rm6xBXr66-MmD05hsBhucIYpLS_VCJVs9OFPnWsksPJ19ibw2K3fabfJbzIdB3Xv3J0kzLQ0gY7bpLRXK1oAcUTxNNsy-LQGe_lyV6INQ4ojPLGJpOTk","timestamp":1713283747,"nonce":"ebccdb479fcb92636fbc","difficulty":15000,"timeout":1000,"cpu":false}`
123 | // challenge, err := ParseSecCptChallengeFromJson(strings.NewReader(jsonPayload))
124 | // if err != nil {
125 | // // Handle the error
126 | // }
127 | //
128 | // Parameters:
129 | // - payload: An io.Reader containing the JSON payload of the sec-cpt challenge.
130 | //
131 | // Returns:
132 | // - *SecCptChallenge: A pointer to a SecCptChallenge struct containing the parsed challenge data, duration, and challenge path.
133 | // - error: An error encountered during parsing, or nil if parsing was successful.
134 | //
135 | // Errors:
136 | // - Any error returned by the JSON unmarshaling process.
137 | func ParseSecCptChallengeFromJson(payload io.Reader) (*SecCptChallenge, error) {
138 | var apiResponse secCptApiResponse
139 | if err := easyjson.UnmarshalFromReader(payload, &apiResponse); err != nil {
140 | return nil, err
141 | }
142 |
143 | return &SecCptChallenge{
144 | challengeData: &secCptChallengeData{
145 | Token: apiResponse.Token,
146 | Timestamp: apiResponse.Timestamp,
147 | Nonce: apiResponse.Nonce,
148 | Difficulty: apiResponse.Difficulty,
149 | Timeout: apiResponse.Timeout,
150 | },
151 | duration: apiResponse.ChlgDuration,
152 | ChallengePath: apiResponse.BrandingURLContent,
153 | }, nil
154 | }
155 |
156 | func parseSecCptChallengeData(src []byte) (*secCptChallengeData, error) {
157 | challengeMatches := secChallengeExpr.FindSubmatch(src)
158 | if len(challengeMatches) < 2 {
159 | return nil, ErrSecCptParsing
160 | }
161 |
162 | decodedChallenge := make([]byte, base64.StdEncoding.DecodedLen(len(challengeMatches[1])))
163 | n, err := base64.StdEncoding.Decode(decodedChallenge, challengeMatches[1])
164 | if err != nil {
165 | return nil, err
166 | }
167 |
168 | var cd secCptChallengeData
169 | if err := easyjson.Unmarshal(decodedChallenge[:n], &cd); err != nil {
170 | return nil, err
171 | }
172 |
173 | return &cd, nil
174 | }
175 |
176 | func parseSecCptDuration(src []byte) (int, error) {
177 | durationMatches := secDurationExpr.FindSubmatch(src)
178 | if len(durationMatches) < 2 {
179 | return 0, ErrSecCptParsing
180 | }
181 |
182 | duration, err := strconv.Atoi(string(durationMatches[1]))
183 | if err != nil {
184 | return 0, errors.Join(ErrSecCptParsing, err)
185 | }
186 |
187 | return duration, nil
188 | }
189 |
190 | func parseSecCptChallengePath(src []byte) (string, error) {
191 | pageMatches := secPageExpr.FindSubmatch(src)
192 | if len(pageMatches) < 2 {
193 | return "", ErrSecCptParsing
194 | }
195 |
196 | return string(pageMatches[1]), nil
197 | }
198 |
199 | // GenerateSecCptPayload generates the payload for the sec-cpt challenge.
200 | //
201 | // The function takes the sec_cpt cookie value as input and extracts the necessary information
202 | // to generate the payload. It generates the answers for the challenge using the `generateSecCptAnswers`
203 | // function and creates an ordered object containing the token and answers.
204 | //
205 | // Example usage:
206 | //
207 | // secCptCookie := "..."
208 | // payload, err := challenge.GenerateSecCptPayload(secCptCookie)
209 | // if err != nil {
210 | // // Handle the error
211 | // }
212 | // // Use the generated payload
213 | // fmt.Println(string(payload))
214 | //
215 | // Parameters:
216 | // - secCptCookie: A string representing the value of the sec_cpt cookie.
217 | //
218 | // Returns:
219 | // - []byte: The generated payload as a byte slice.
220 | // - error: An error encountered during payload generation, or nil if generation was successful.
221 | //
222 | // Errors:
223 | // - errors.New("error parsing sec_cpt cookie"): Returned when the sec_cpt cookie is not in the expected format.
224 | // - Other errors may be returned by the underlying JSON marshaling.
225 | func (s *SecCptChallenge) GenerateSecCptPayload(secCptCookie string) ([]byte, error) {
226 | sec, _, found := strings.Cut(secCptCookie, "~")
227 | if !found {
228 | return nil, ErrSecCptInvalidCookie
229 | }
230 |
231 | answers := generateSecCptAnswers(sec, s.challengeData)
232 |
233 | payload := orderedobject.NewObject[any](2)
234 | payload.Set("token", s.challengeData.Token)
235 | payload.Set("answers", answers)
236 |
237 | return jsoniter.Marshal(payload)
238 | }
239 |
240 | // Sleep sleeps for the duration specified in the sec-cpt challenge.
241 | //
242 | // The function uses the `duration` field of the `SecCptChallenge` struct to determine
243 | // the number of seconds to sleep. It blocks the current goroutine for the specified duration.
244 | //
245 | // Example usage:
246 | //
247 | // challenge, err := ParseSecCptChallenge(html)
248 | // if err != nil {
249 | // // Handle the error
250 | // }
251 | // challenge.Sleep()
252 | //
253 | // Parameters:
254 | // - None
255 | //
256 | // Returns:
257 | // - None
258 | func (s *SecCptChallenge) Sleep() {
259 | time.Sleep(time.Second * time.Duration(s.duration))
260 | }
261 |
262 | // SleepWithContext sleeps for the duration specified in the sec-cpt challenge or until the provided context is done.
263 | //
264 | // The function uses the `duration` field of the `SecCptChallenge` struct to determine
265 | // the number of seconds to sleep. It creates a timer with the specified duration and waits for either
266 | // the timer to expire or the provided context to be done. If the context is done before the timer expires,
267 | // the timer is stopped to prevent it from firing.
268 | //
269 | // Example usage:
270 | //
271 | // challenge, err := ParseSecCptChallenge(html)
272 | // if err != nil {
273 | // // Handle the error
274 | // }
275 | // ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
276 | // defer cancel()
277 | // challenge.SleepWithContext(ctx)
278 | //
279 | // Parameters:
280 | // - ctx: A context.Context that can be used to cancel the sleep operation.
281 | //
282 | // Returns:
283 | // - None
284 | func (s *SecCptChallenge) SleepWithContext(ctx context.Context) {
285 | timer := time.NewTimer(time.Second * time.Duration(s.duration))
286 | select {
287 | case <-ctx.Done():
288 | if !timer.Stop() {
289 | <-timer.C
290 | }
291 | case <-timer.C:
292 | }
293 | }
294 |
295 | func generateSecCptAnswers(sec string, challengeData *secCptChallengeData) []string {
296 | answers := make([]string, challengeData.Count)
297 | challenge := sec + strconv.Itoa(challengeData.Timestamp) + challengeData.Nonce
298 | hash := sha256.New()
299 | var hashBytes [sha256.Size]byte
300 |
301 | for i := range answers {
302 | initialPart := []byte(challenge + strconv.Itoa(challengeData.Difficulty+i))
303 |
304 | buf := make([]byte, len(initialPart)+64)
305 | copy(buf, initialPart)
306 |
307 | for {
308 | answerLen := internal.FloatToStringRadix(rand.Float64(), 16, buf[len(initialPart):])
309 | hash.Reset()
310 | hash.Write(buf[:len(initialPart)+answerLen])
311 | hash.Sum(hashBytes[:0])
312 |
313 | var output int
314 | for _, v := range hashBytes {
315 | output = int(int32(uint32((output<<8)|int(v)) >> 0))
316 | output %= challengeData.Difficulty + i
317 | }
318 |
319 | if output != 0 {
320 | continue
321 | }
322 |
323 | answers[i] = string(buf[len(initialPart) : len(initialPart)+answerLen])
324 | break
325 | }
326 | }
327 |
328 | return answers
329 | }
330 |
--------------------------------------------------------------------------------
/akamai/sec_cpt_easyjson.go:
--------------------------------------------------------------------------------
1 | // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
2 |
3 | package akamai
4 |
5 | import (
6 | json "encoding/json"
7 | easyjson "github.com/mailru/easyjson"
8 | jlexer "github.com/mailru/easyjson/jlexer"
9 | jwriter "github.com/mailru/easyjson/jwriter"
10 | )
11 |
12 | // suppress unused package warning
13 | var (
14 | _ *json.RawMessage
15 | _ *jlexer.Lexer
16 | _ *jwriter.Writer
17 | _ easyjson.Marshaler
18 | )
19 |
20 | func easyjsonAb81e4ffDecodeGithubComHyperSolutionsHyperSdkGoAkamai(in *jlexer.Lexer, out *secCptChallengeData) {
21 | isTopLevel := in.IsStart()
22 | if in.IsNull() {
23 | if isTopLevel {
24 | in.Consumed()
25 | }
26 | in.Skip()
27 | return
28 | }
29 | in.Delim('{')
30 | for !in.IsDelim('}') {
31 | key := in.UnsafeFieldName(false)
32 | in.WantColon()
33 | if in.IsNull() {
34 | in.Skip()
35 | in.WantComma()
36 | continue
37 | }
38 | switch key {
39 | case "token":
40 | out.Token = string(in.String())
41 | case "timestamp":
42 | out.Timestamp = int(in.Int())
43 | case "nonce":
44 | out.Nonce = string(in.String())
45 | case "difficulty":
46 | out.Difficulty = int(in.Int())
47 | case "count":
48 | out.Count = int(in.Int())
49 | case "timeout":
50 | out.Timeout = int(in.Int())
51 | case "cpu":
52 | out.CPU = bool(in.Bool())
53 | case "verify_url":
54 | out.VerifyURL = string(in.String())
55 | default:
56 | in.SkipRecursive()
57 | }
58 | in.WantComma()
59 | }
60 | in.Delim('}')
61 | if isTopLevel {
62 | in.Consumed()
63 | }
64 | }
65 | func easyjsonAb81e4ffEncodeGithubComHyperSolutionsHyperSdkGoAkamai(out *jwriter.Writer, in secCptChallengeData) {
66 | out.RawByte('{')
67 | first := true
68 | _ = first
69 | {
70 | const prefix string = ",\"token\":"
71 | out.RawString(prefix[1:])
72 | out.String(string(in.Token))
73 | }
74 | {
75 | const prefix string = ",\"timestamp\":"
76 | out.RawString(prefix)
77 | out.Int(int(in.Timestamp))
78 | }
79 | {
80 | const prefix string = ",\"nonce\":"
81 | out.RawString(prefix)
82 | out.String(string(in.Nonce))
83 | }
84 | {
85 | const prefix string = ",\"difficulty\":"
86 | out.RawString(prefix)
87 | out.Int(int(in.Difficulty))
88 | }
89 | {
90 | const prefix string = ",\"count\":"
91 | out.RawString(prefix)
92 | out.Int(int(in.Count))
93 | }
94 | {
95 | const prefix string = ",\"timeout\":"
96 | out.RawString(prefix)
97 | out.Int(int(in.Timeout))
98 | }
99 | {
100 | const prefix string = ",\"cpu\":"
101 | out.RawString(prefix)
102 | out.Bool(bool(in.CPU))
103 | }
104 | {
105 | const prefix string = ",\"verify_url\":"
106 | out.RawString(prefix)
107 | out.String(string(in.VerifyURL))
108 | }
109 | out.RawByte('}')
110 | }
111 |
112 | // MarshalJSON supports json.Marshaler interface
113 | func (v secCptChallengeData) MarshalJSON() ([]byte, error) {
114 | w := jwriter.Writer{}
115 | easyjsonAb81e4ffEncodeGithubComHyperSolutionsHyperSdkGoAkamai(&w, v)
116 | return w.Buffer.BuildBytes(), w.Error
117 | }
118 |
119 | // MarshalEasyJSON supports easyjson.Marshaler interface
120 | func (v secCptChallengeData) MarshalEasyJSON(w *jwriter.Writer) {
121 | easyjsonAb81e4ffEncodeGithubComHyperSolutionsHyperSdkGoAkamai(w, v)
122 | }
123 |
124 | // UnmarshalJSON supports json.Unmarshaler interface
125 | func (v *secCptChallengeData) UnmarshalJSON(data []byte) error {
126 | r := jlexer.Lexer{Data: data}
127 | easyjsonAb81e4ffDecodeGithubComHyperSolutionsHyperSdkGoAkamai(&r, v)
128 | return r.Error()
129 | }
130 |
131 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface
132 | func (v *secCptChallengeData) UnmarshalEasyJSON(l *jlexer.Lexer) {
133 | easyjsonAb81e4ffDecodeGithubComHyperSolutionsHyperSdkGoAkamai(l, v)
134 | }
135 | func easyjsonAb81e4ffDecodeGithubComHyperSolutionsHyperSdkGoAkamai1(in *jlexer.Lexer, out *secCptApiResponse) {
136 | isTopLevel := in.IsStart()
137 | if in.IsNull() {
138 | if isTopLevel {
139 | in.Consumed()
140 | }
141 | in.Skip()
142 | return
143 | }
144 | in.Delim('{')
145 | for !in.IsDelim('}') {
146 | key := in.UnsafeFieldName(false)
147 | in.WantColon()
148 | if in.IsNull() {
149 | in.Skip()
150 | in.WantComma()
151 | continue
152 | }
153 | switch key {
154 | case "sec-cp-challenge":
155 | out.SecCpChallenge = string(in.String())
156 | case "provider":
157 | out.Provider = string(in.String())
158 | case "branding_url_content":
159 | out.BrandingURLContent = string(in.String())
160 | case "chlg_duration":
161 | out.ChlgDuration = int(in.Int())
162 | case "token":
163 | out.Token = string(in.String())
164 | case "timestamp":
165 | out.Timestamp = int(in.Int())
166 | case "nonce":
167 | out.Nonce = string(in.String())
168 | case "difficulty":
169 | out.Difficulty = int(in.Int())
170 | case "timeout":
171 | out.Timeout = int(in.Int())
172 | case "cpu":
173 | out.CPU = bool(in.Bool())
174 | default:
175 | in.SkipRecursive()
176 | }
177 | in.WantComma()
178 | }
179 | in.Delim('}')
180 | if isTopLevel {
181 | in.Consumed()
182 | }
183 | }
184 | func easyjsonAb81e4ffEncodeGithubComHyperSolutionsHyperSdkGoAkamai1(out *jwriter.Writer, in secCptApiResponse) {
185 | out.RawByte('{')
186 | first := true
187 | _ = first
188 | {
189 | const prefix string = ",\"sec-cp-challenge\":"
190 | out.RawString(prefix[1:])
191 | out.String(string(in.SecCpChallenge))
192 | }
193 | {
194 | const prefix string = ",\"provider\":"
195 | out.RawString(prefix)
196 | out.String(string(in.Provider))
197 | }
198 | {
199 | const prefix string = ",\"branding_url_content\":"
200 | out.RawString(prefix)
201 | out.String(string(in.BrandingURLContent))
202 | }
203 | {
204 | const prefix string = ",\"chlg_duration\":"
205 | out.RawString(prefix)
206 | out.Int(int(in.ChlgDuration))
207 | }
208 | {
209 | const prefix string = ",\"token\":"
210 | out.RawString(prefix)
211 | out.String(string(in.Token))
212 | }
213 | {
214 | const prefix string = ",\"timestamp\":"
215 | out.RawString(prefix)
216 | out.Int(int(in.Timestamp))
217 | }
218 | {
219 | const prefix string = ",\"nonce\":"
220 | out.RawString(prefix)
221 | out.String(string(in.Nonce))
222 | }
223 | {
224 | const prefix string = ",\"difficulty\":"
225 | out.RawString(prefix)
226 | out.Int(int(in.Difficulty))
227 | }
228 | {
229 | const prefix string = ",\"timeout\":"
230 | out.RawString(prefix)
231 | out.Int(int(in.Timeout))
232 | }
233 | {
234 | const prefix string = ",\"cpu\":"
235 | out.RawString(prefix)
236 | out.Bool(bool(in.CPU))
237 | }
238 | out.RawByte('}')
239 | }
240 |
241 | // MarshalJSON supports json.Marshaler interface
242 | func (v secCptApiResponse) MarshalJSON() ([]byte, error) {
243 | w := jwriter.Writer{}
244 | easyjsonAb81e4ffEncodeGithubComHyperSolutionsHyperSdkGoAkamai1(&w, v)
245 | return w.Buffer.BuildBytes(), w.Error
246 | }
247 |
248 | // MarshalEasyJSON supports easyjson.Marshaler interface
249 | func (v secCptApiResponse) MarshalEasyJSON(w *jwriter.Writer) {
250 | easyjsonAb81e4ffEncodeGithubComHyperSolutionsHyperSdkGoAkamai1(w, v)
251 | }
252 |
253 | // UnmarshalJSON supports json.Unmarshaler interface
254 | func (v *secCptApiResponse) UnmarshalJSON(data []byte) error {
255 | r := jlexer.Lexer{Data: data}
256 | easyjsonAb81e4ffDecodeGithubComHyperSolutionsHyperSdkGoAkamai1(&r, v)
257 | return r.Error()
258 | }
259 |
260 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface
261 | func (v *secCptApiResponse) UnmarshalEasyJSON(l *jlexer.Lexer) {
262 | easyjsonAb81e4ffDecodeGithubComHyperSolutionsHyperSdkGoAkamai1(l, v)
263 | }
264 | func easyjsonAb81e4ffDecodeGithubComHyperSolutionsHyperSdkGoAkamai2(in *jlexer.Lexer, out *SecCptChallenge) {
265 | isTopLevel := in.IsStart()
266 | if in.IsNull() {
267 | if isTopLevel {
268 | in.Consumed()
269 | }
270 | in.Skip()
271 | return
272 | }
273 | in.Delim('{')
274 | for !in.IsDelim('}') {
275 | key := in.UnsafeFieldName(false)
276 | in.WantColon()
277 | if in.IsNull() {
278 | in.Skip()
279 | in.WantComma()
280 | continue
281 | }
282 | switch key {
283 | case "ChallengePath":
284 | out.ChallengePath = string(in.String())
285 | default:
286 | in.SkipRecursive()
287 | }
288 | in.WantComma()
289 | }
290 | in.Delim('}')
291 | if isTopLevel {
292 | in.Consumed()
293 | }
294 | }
295 | func easyjsonAb81e4ffEncodeGithubComHyperSolutionsHyperSdkGoAkamai2(out *jwriter.Writer, in SecCptChallenge) {
296 | out.RawByte('{')
297 | first := true
298 | _ = first
299 | {
300 | const prefix string = ",\"ChallengePath\":"
301 | out.RawString(prefix[1:])
302 | out.String(string(in.ChallengePath))
303 | }
304 | out.RawByte('}')
305 | }
306 |
307 | // MarshalJSON supports json.Marshaler interface
308 | func (v SecCptChallenge) MarshalJSON() ([]byte, error) {
309 | w := jwriter.Writer{}
310 | easyjsonAb81e4ffEncodeGithubComHyperSolutionsHyperSdkGoAkamai2(&w, v)
311 | return w.Buffer.BuildBytes(), w.Error
312 | }
313 |
314 | // MarshalEasyJSON supports easyjson.Marshaler interface
315 | func (v SecCptChallenge) MarshalEasyJSON(w *jwriter.Writer) {
316 | easyjsonAb81e4ffEncodeGithubComHyperSolutionsHyperSdkGoAkamai2(w, v)
317 | }
318 |
319 | // UnmarshalJSON supports json.Unmarshaler interface
320 | func (v *SecCptChallenge) UnmarshalJSON(data []byte) error {
321 | r := jlexer.Lexer{Data: data}
322 | easyjsonAb81e4ffDecodeGithubComHyperSolutionsHyperSdkGoAkamai2(&r, v)
323 | return r.Error()
324 | }
325 |
326 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface
327 | func (v *SecCptChallenge) UnmarshalEasyJSON(l *jlexer.Lexer) {
328 | easyjsonAb81e4ffDecodeGithubComHyperSolutionsHyperSdkGoAkamai2(l, v)
329 | }
330 |
--------------------------------------------------------------------------------
/akamai/sec_cpt_test.go:
--------------------------------------------------------------------------------
1 | package akamai
2 |
3 | import (
4 | "io"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | const secCptHtml = ``
10 |
11 | const secCptJson = `{"sec-cp-challenge":"true","provider":"crypto","branding_url_content":"/_sec/cp_challenge/crypto_message-4-3.htm","chlg_duration":30,"token":"AAQAAAAJ_____9z_ZPsdHbk36hg2f6np2sGJDXmkwGmBiMBr_DDEmSWfi8Zt7BdtjWrNd9KD4DS_vim0VnK2wsa8tIC7XWsCshkvDF9J9Rf5EFwBU00c6SMXTaSNSTcDR-HVFGp3uAa67Mb3I6HeifXbjALcEomjcnwa9ZNQdDWuTAUTgNGbYw09A8AXIuP9DNv3QktUx488FV38Rm6xBXr66-MmD05hsBhucIYpLS_VCJVs9OFPnWsksPJ19ibw2K3fabfJbzIdB3Xv3J0kzLQ0gY7bpLRXK1oAcUTxNNsy-LQGe_lyV6INQ4ojPLGJpOTk","timestamp":1713283747,"nonce":"ebccdb479fcb92636fbc","difficulty":15000,"timeout":1000,"cpu":false}`
12 |
13 | func TestParseSecCptChallenge(t *testing.T) {
14 | input := strings.NewReader(secCptHtml)
15 |
16 | challenge, err := ParseSecCptChallenge(input)
17 | if err != nil {
18 | t.Fatal(err)
19 | }
20 |
21 | if challenge.duration != 5 {
22 | t.Errorf("Expected duration to be 5, got %d", challenge.duration)
23 | }
24 |
25 | if challenge.challengeData.Difficulty != 2500 {
26 | t.Errorf("Expected difficulty to be 2500, got %d", challenge.challengeData.Difficulty)
27 | }
28 |
29 | if challenge.ChallengePath != "/_sec/cp_challenge/ak-challenge-4-3.htm" {
30 | t.Errorf("Expected challenge path to be '/_sec/cp_challenge/ak-challenge-4-3.htm', got %s", challenge.ChallengePath)
31 | }
32 | }
33 |
34 | func TestParseSecCptChallengeData(t *testing.T) {
35 | input := strings.NewReader(secCptHtml)
36 | src, _ := io.ReadAll(input)
37 |
38 | challengeData, err := parseSecCptChallengeData(src)
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 |
43 | if challengeData.Token != "AAQAAAAJ_____8qtEZNLUTePXrEsHuMA6qtfNwh0aF4y0BXhd6NRuvTEtjSsZE5nckh_H4_HP9nnlwEz84XYA-PiErO4BcCzW9Jey7YLrFbcN5H7O_DbpQJXJ9s46LrrbTBd0XuTUpCXXRmrZwblW5fF-mxQAWHieNi6IFucTSNLzlYgmJRpBLXdGRpG9KYAu9cWU4dSuCj2-ICFQch2-CP_pZG1eUbgw5D-1J4OtKo9GBK5cLamjHCGAe5Suii-mLLbaioYgn6K4VgsZmOyZp2G6xqWiN7yfGDIy5xDmzlquyBaG5YAPLLpaiosmXzvPkM" {
44 | t.Errorf("Expected token to be 'AAQAAAAJ_____8qtEZNLUTePXrEsHuMA6qtfNwh0aF4y0BXhd6NRuvTEtjSsZE5nckh_H4_HP9nnlwEz84XYA-PiErO4BcCzW9Jey7YLrFbcN5H7O_DbpQJXJ9s46LrrbTBd0XuTUpCXXRmrZwblW5fF-mxQAWHieNi6IFucTSNLzlYgmJRpBLXdGRpG9KYAu9cWU4dSuCj2-ICFQch2-CP_pZG1eUbgw5D-1J4OtKo9GBK5cLamjHCGAe5Suii-mLLbaioYgn6K4VgsZmOyZp2G6xqWiN7yfGDIy5xDmzlquyBaG5YAPLLpaiosmXzvPkM', got %s", challengeData.Token)
45 | }
46 |
47 | if challengeData.Timestamp != 1712237549 {
48 | t.Errorf("Expected timestamp to be 1712237549, got %d", challengeData.Timestamp)
49 | }
50 |
51 | if challengeData.Nonce != "ab3028d518738bd8faad" {
52 | t.Errorf("Expected nonce to be 'ab3028d518738bd8faad', got %s", challengeData.Nonce)
53 | }
54 |
55 | if challengeData.Difficulty != 2500 {
56 | t.Errorf("Expected difficulty to be 2500, got %d", challengeData.Difficulty)
57 | }
58 |
59 | if challengeData.Timeout != 1000 {
60 | t.Errorf("Expected timeout to be 1000, got %d", challengeData.Timeout)
61 | }
62 | }
63 |
64 | func TestParseSecCptChallengeFromJson(t *testing.T) {
65 | challenge, err := ParseSecCptChallengeFromJson(strings.NewReader(secCptJson))
66 | if err != nil {
67 | t.Fatal(err)
68 | }
69 |
70 | if challenge.challengeData.Token != "AAQAAAAJ_____9z_ZPsdHbk36hg2f6np2sGJDXmkwGmBiMBr_DDEmSWfi8Zt7BdtjWrNd9KD4DS_vim0VnK2wsa8tIC7XWsCshkvDF9J9Rf5EFwBU00c6SMXTaSNSTcDR-HVFGp3uAa67Mb3I6HeifXbjALcEomjcnwa9ZNQdDWuTAUTgNGbYw09A8AXIuP9DNv3QktUx488FV38Rm6xBXr66-MmD05hsBhucIYpLS_VCJVs9OFPnWsksPJ19ibw2K3fabfJbzIdB3Xv3J0kzLQ0gY7bpLRXK1oAcUTxNNsy-LQGe_lyV6INQ4ojPLGJpOTk" {
71 | t.Fail()
72 | }
73 |
74 | if challenge.challengeData.Timestamp != 1713283747 {
75 | t.Fail()
76 | }
77 |
78 | if challenge.challengeData.Nonce != "ebccdb479fcb92636fbc" {
79 | t.Fail()
80 | }
81 |
82 | if challenge.challengeData.Difficulty != 15000 {
83 | t.Fail()
84 | }
85 |
86 | if challenge.challengeData.Timeout != 1000 {
87 | t.Fail()
88 | }
89 | }
90 |
91 | func BenchmarkParseSecCptChallenge(b *testing.B) {
92 | b.ReportAllocs()
93 | for i := 0; i < b.N; i++ {
94 | if _, err := ParseSecCptChallenge(strings.NewReader(secCptHtml)); err != nil {
95 | b.Error(err)
96 | }
97 | }
98 | }
99 |
100 | func BenchmarkSecCptChallenge_GenerateSecCptPayload(b *testing.B) {
101 | input := strings.NewReader(secCptHtml)
102 |
103 | challenge, err := ParseSecCptChallenge(input)
104 | if err != nil {
105 | b.Fatal(err)
106 | }
107 | const cookie = `3F3B2D3E2ABE67693EE8134E57C501C8~...`
108 |
109 | b.ReportAllocs()
110 | for i := 0; i < b.N; i++ {
111 | if _, err := challenge.GenerateSecCptPayload(cookie); err != nil {
112 | b.Error(err)
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/akamai/stop_signal.go:
--------------------------------------------------------------------------------
1 | package akamai
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 | )
7 |
8 | // IsCookieValid determines if the provided _abck cookie value is valid, based on Akamai Bot Manager's
9 | // client-side stop signal mechanism using the given request count. If the result is true, the client is ADVISED
10 | // to halt further sensor data submissions. Submitting further would still produce a valid cookie but is unnecessary.
11 | //
12 | // The stop signal mechanism in the Akamai Bot Manager's client-side script informs a client that the cookie received is
13 | // valid and that any additional submissions are superfluous.
14 | //
15 | // However, some applications do not activate the stop signal feature. In such scenarios, the client will continue
16 | // submitting data whenever a trigger event occurs. Under these circumstances, verifying the authenticity of a cookie
17 | // without sending it to a secured endpoint becomes challenging.
18 | func IsCookieValid(cookie string, requestCount int) bool {
19 | parts := strings.Split(cookie, "~")
20 | if len(parts) < 2 {
21 | return false
22 | }
23 |
24 | requestThreshold, err := strconv.Atoi(parts[1])
25 | if err != nil {
26 | requestThreshold = -1
27 | }
28 |
29 | return requestThreshold != -1 && requestCount >= requestThreshold
30 | }
31 |
32 | // IsCookieInvalidated determines if the current session requires more sensors to be sent.
33 | //
34 | // Protected endpoints can invalidate a session by setting a new _abck cookie that ends in '~0~-1~-1' or similar.
35 | // This function returns if such an invalidated cookie is present, if it is present you should be able to make the
36 | // cookie valid again with only 1 sensor post.
37 | func IsCookieInvalidated(cookie string) bool {
38 | parts := strings.Split(cookie, "~")
39 | if len(parts) < 4 {
40 | return false
41 | }
42 |
43 | signal, err := strconv.Atoi(parts[3])
44 | if err != nil {
45 | signal = -1
46 | }
47 |
48 | return signal > -1
49 | }
50 |
--------------------------------------------------------------------------------
/akamai/stop_signal_test.go:
--------------------------------------------------------------------------------
1 | package akamai
2 |
3 | import "testing"
4 |
5 | func TestIsCookieValid(t *testing.T) {
6 | const (
7 | validCookie = `0C8A2251CC04F60F59160D6AD92DA8A0~0~YAAQlivJF6o1GjGGAQAAaNihYgldsErwKa3aAlB+oRlgZYviinJa+Q29XMXmkwJUNCgQPooQUyhfjhAgavSMACfCk1doYnUa4dYmsVUWbWB+QGFEPuwcvLVQscLV8taHWIFuFxb94vEJ8MnSY9sQlhRN9i2iNgZ5QJz8h2s2mMm4ZO+i890DPaHfJPkSrYtc9ivgbjDA/jFpK6k2Pq8Pu25dCI55zOqOeSyaChgtJyF6KvlnlyVrqOa12tThX+prb52et7FRGmqhw8LU5X1E07WShiKDmJw2Rb8+odHcA28bD3EITXTy43QFb2PKR9Q3jy57KFEQYFaeW4xfAe8BxjdpkYt5vSH50nmbi1SXOWIxL4QV0b8psJgMCIq+ZMR+ZBU2opuHgxAucktvbIffGuJPWFYJu8thjxr5HGBtZBUqc6LwccFQI+DZd+hpZfsRJscvNx0yWmiPN8/gJiVLGkjHWYL5xmVaVCYceTtGL+7N~-1~-1~-1`
8 |
9 | invalidCookie = `3B508597CDC152514C3D85CF2749455C~-1~YAAQNcMTAgMgNEyKAQAAaxaxTwrFBKU37u+fFlXF8NMV3M3n4A2lNyvNTV14HhbYAyJWQdsaUYXmjc/7GGsPQ8EYyPjUZr6X8guTu9q9mZY2ZeF5IWdB2jRHLVzEttloRl8RMGS+dP34QSaMYx98elcgQchq+DAiRDB1XoeKDzwdZnxhfLRu8vAilIaR+i/NPf1Y1fR+n85SIp5OlpMJYK71eoL/D9wZTbpZQVHbSP4rhHG4wNaHNkuo5KkwWpgbQfLWIwhatuhn/xZ1mJEQVnZwhHg9aDF+ooxiPm+2+HK/QI5zSGZ8zz9CMZ9PI+jegXtqznth6eBXJ7ZXCr5VfgjnepKpJW3WJfKv53dHHWUtkuBEOTozjuR1RYcsFo07oNmUqWBhQ0EslRCJpMAn7ehwuyC2w7BRlrZuSvDOAcxUgbgN2LSBEA==~-1~-1~-1`
10 | )
11 |
12 | if !IsCookieValid(validCookie, 1) {
13 | t.Fail()
14 | }
15 |
16 | if IsCookieValid(invalidCookie, 1) {
17 | t.Fail()
18 | }
19 | }
20 |
21 | func BenchmarkIsCookieValid(b *testing.B) {
22 | const cookie = `0C8A2251CC04F60F59160D6AD92DA8A0~0~YAAQlivJF6o1GjGGAQAAaNihYgldsErwKa3aAlB+oRlgZYviinJa+Q29XMXmkwJUNCgQPooQUyhfjhAgavSMACfCk1doYnUa4dYmsVUWbWB+QGFEPuwcvLVQscLV8taHWIFuFxb94vEJ8MnSY9sQlhRN9i2iNgZ5QJz8h2s2mMm4ZO+i890DPaHfJPkSrYtc9ivgbjDA/jFpK6k2Pq8Pu25dCI55zOqOeSyaChgtJyF6KvlnlyVrqOa12tThX+prb52et7FRGmqhw8LU5X1E07WShiKDmJw2Rb8+odHcA28bD3EITXTy43QFb2PKR9Q3jy57KFEQYFaeW4xfAe8BxjdpkYt5vSH50nmbi1SXOWIxL4QV0b8psJgMCIq+ZMR+ZBU2opuHgxAucktvbIffGuJPWFYJu8thjxr5HGBtZBUqc6LwccFQI+DZd+hpZfsRJscvNx0yWmiPN8/gJiVLGkjHWYL5xmVaVCYceTtGL+7N~-1~-1~-1`
23 |
24 | b.ReportAllocs()
25 | for i := 0; i < b.N; i++ {
26 | IsCookieValid(cookie, 1)
27 | }
28 | }
29 |
30 | func TestIsCookieInvalidated(t *testing.T) {
31 | const (
32 | invalidatedCookie = `7D715603CF98EF4593FB1BABFA0BA525~-1~YAAQ5oFlX6qoLeeGAQAAi7B99gl3E4KSKtdW8AEn7RB8D1CpzSqsnYR5E24Q66mWSu8yXMOVdmjYPVFVad6QNKZ/w6xs2sU9sX/t4GNgLFqNLn3Qcags4msWUL6Mdlmh/MKPZWiBnU6pGnAec9cdYW9gAWiZ3kSvCxJHYD536EBIJKKkZ/EcCCKauQbnn+TUuSp4D2jSQfUEOaXMTiSREKRnqLpc9lmgG8hkFBeeyWlu7vv+iussTelN6o5zCHwH16ztaLQTDVRclRGaUo2jqN7dpDd8V0WvZ7NnbNsiU2Ac52TBM7Kjl5/l2ltAAlYr+vgnc3QRhbCo8trn2RrEP7nkCRF1RzQ3HvG097nul3hcRPXitfIslgVG9ur67LTwpRt58DqjgjNz4qHR5R77VzyTUQPt8ZQzMeh4s9TOr/E=~0~-1~-1`
33 | )
34 |
35 | if !IsCookieInvalidated(invalidatedCookie) {
36 | t.Fail()
37 | }
38 | }
39 |
40 | func BenchmarkIsCookieInvalidated(b *testing.B) {
41 | const cookie = `0C8A2251CC04F60F59160D6AD92DA8A0~0~YAAQlivJF6o1GjGGAQAAaNihYgldsErwKa3aAlB+oRlgZYviinJa+Q29XMXmkwJUNCgQPooQUyhfjhAgavSMACfCk1doYnUa4dYmsVUWbWB+QGFEPuwcvLVQscLV8taHWIFuFxb94vEJ8MnSY9sQlhRN9i2iNgZ5QJz8h2s2mMm4ZO+i890DPaHfJPkSrYtc9ivgbjDA/jFpK6k2Pq8Pu25dCI55zOqOeSyaChgtJyF6KvlnlyVrqOa12tThX+prb52et7FRGmqhw8LU5X1E07WShiKDmJw2Rb8+odHcA28bD3EITXTy43QFb2PKR9Q3jy57KFEQYFaeW4xfAe8BxjdpkYt5vSH50nmbi1SXOWIxL4QV0b8psJgMCIq+ZMR+ZBU2opuHgxAucktvbIffGuJPWFYJu8thjxr5HGBtZBUqc6LwccFQI+DZd+hpZfsRJscvNx0yWmiPN8/gJiVLGkjHWYL5xmVaVCYceTtGL+7N~-1~-1~-1`
42 |
43 | b.ReportAllocs()
44 | for i := 0; i < b.N; i++ {
45 | IsCookieInvalidated(cookie)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/api.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "errors"
7 | "fmt"
8 | "github.com/Hyper-Solutions/hyper-sdk-go/v2/internal"
9 | jsoniter "github.com/json-iterator/go"
10 | "github.com/mailru/easyjson"
11 | "github.com/mailru/easyjson/buffer"
12 | "github.com/mailru/easyjson/jwriter"
13 | "net/http"
14 | )
15 |
16 | func sendRequest[V easyjson.Marshaler, T easyjson.Unmarshaler](ctx context.Context, s *Session, url string, input V) (response T, err error) {
17 | if s.ApiKey == "" {
18 | return response, errors.New("missing api key")
19 | }
20 |
21 | w := jwriter.Writer{
22 | Flags: 0,
23 | Error: nil,
24 | Buffer: buffer.Buffer{},
25 | NoEscapeHTML: true,
26 | }
27 |
28 | input.MarshalEasyJSON(&w)
29 |
30 | if w.Error != nil {
31 | return response, w.Error
32 | }
33 | payload := w.Buffer.BuildBytes()
34 |
35 | useCompression := false
36 |
37 | if len(payload) > 1000 {
38 | compressedBody, err := internal.CompressZstd(payload)
39 | if err != nil {
40 | return response, fmt.Errorf("failed to compress request body with zstd: %w", err)
41 | }
42 | payload = compressedBody
43 | useCompression = true
44 | }
45 |
46 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
47 | if err != nil {
48 | return response, err
49 | }
50 | req.Header.Set("content-type", "application/json")
51 | req.Header.Set("accept-encoding", "zstd")
52 | req.Header.Set("x-api-key", s.ApiKey)
53 |
54 | if useCompression {
55 | req.Header.Set("content-encoding", "zstd")
56 | }
57 |
58 | if s.JwtKey != nil {
59 | signature, err := s.generateSignature()
60 | if err != nil {
61 | return response, err
62 | }
63 | req.Header.Set("x-signature", signature)
64 | }
65 |
66 | resp, err := s.Client.Do(req)
67 | if err != nil {
68 | return response, err
69 | }
70 | defer resp.Body.Close()
71 |
72 | respBody, err := internal.DecompressResponse(resp)
73 | if err != nil {
74 | return response, err
75 | }
76 |
77 | if err := jsoniter.Unmarshal(respBody, &response); err != nil {
78 | return response, err
79 | }
80 |
81 | return response, nil
82 | }
83 |
--------------------------------------------------------------------------------
/datadome.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | )
7 |
8 | // GenerateDataDomeSlider returns the URL that will return a solved datadome cookie when blocked by captcha, and
9 | // the extra sec-ch-* headers used on consequent requests.
10 | func (s *Session) GenerateDataDomeSlider(ctx context.Context, input *DataDomeSliderInput) (string, *Headers, error) {
11 | response, err := sendRequest[*DataDomeSliderInput, *apiResponse](ctx, s, "https://datadome.hypersolutions.co/slider", input)
12 | if err != nil {
13 | return "", nil, err
14 | }
15 |
16 | if response.Error != "" {
17 | return "", nil, fmt.Errorf("api returned with: %s", response.Error)
18 | }
19 |
20 | return response.Payload, response.Headers, nil
21 | }
22 |
23 | // GenerateDataDomeInterstitial returns the form data string that is used in the POST request to receive a solved datadome cookie, and
24 | // the extra sec-ch-* headers used on consequent requests.
25 | func (s *Session) GenerateDataDomeInterstitial(ctx context.Context, input *DataDomeInterstitialInput) (string, *Headers, error) {
26 | response, err := sendRequest[*DataDomeInterstitialInput, *apiResponse](ctx, s, "https://datadome.hypersolutions.co/interstitial", input)
27 | if err != nil {
28 | return "", nil, err
29 | }
30 |
31 | if response.Error != "" {
32 | return "", nil, fmt.Errorf("api returned with: %s", response.Error)
33 | }
34 |
35 | return response.Payload, response.Headers, nil
36 | }
37 |
38 | // GenerateDataDomeTags returns the tags data string that is used in the POST request to receive a solved datadome cookie.
39 | func (s *Session) GenerateDataDomeTags(ctx context.Context, input *DataDomeTagsInput) (string, error) {
40 | response, err := sendRequest[*DataDomeTagsInput, *apiResponse](ctx, s, "https://datadome.hypersolutions.co/tags", input)
41 | if err != nil {
42 | return "", err
43 | }
44 |
45 | if response.Error != "" {
46 | return "", fmt.Errorf("api returned with: %s", response.Error)
47 | }
48 |
49 | return response.Payload, nil
50 | }
51 |
--------------------------------------------------------------------------------
/datadome/parse.go:
--------------------------------------------------------------------------------
1 | package datadome
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | jsoniter "github.com/json-iterator/go"
7 | "github.com/justhyped/OrderedForm"
8 | "io"
9 | "regexp"
10 | "strconv"
11 | )
12 |
13 | var (
14 | ddRegex = regexp.MustCompile(`var\s+dd\s*=\s*\{\s*([\s\S]*?)\s*}`)
15 | singleQuoteRegex = regexp.MustCompile(`'([^']*)'`)
16 | keyRegex = regexp.MustCompile(`([^"]|^)(\b\w+)\s*: `)
17 | )
18 |
19 | // ParseInterstitialDeviceCheckLink parses the device check url (/interstitial/?initialCid...) from a blocked response body
20 | //
21 | // the datadome cookie is the current value of the 'datadome' cookie
22 | func ParseInterstitialDeviceCheckLink(body io.Reader, datadomeCookie, referer string) (string, error) {
23 | bodyBytes, err := io.ReadAll(body)
24 | if err != nil {
25 | return "", err
26 | }
27 |
28 | matches := ddRegex.FindSubmatch(bodyBytes)
29 | if matches == nil || len(matches) < 2 {
30 | return "", errors.New("DD object not found")
31 | }
32 |
33 | jsonObject := append([]byte("{"), bytes.TrimSpace(matches[1])...)
34 | jsonObject = append(jsonObject, []byte("}")...)
35 | jsonObject = singleQuoteRegex.ReplaceAll(jsonObject, []byte(`"$1"`))
36 | jsonObject = keyRegex.ReplaceAll(jsonObject, []byte(`$1"$2":`))
37 |
38 | var d dd
39 | err = jsoniter.Unmarshal(jsonObject, &d)
40 | if err != nil {
41 | return "", err
42 | }
43 |
44 | form := new(OrderedForm.OrderedForm)
45 | form.Set("initialCid", d.Cid)
46 | form.Set("hash", d.Hsh)
47 | form.Set("cid", datadomeCookie)
48 | form.Set("referer", referer)
49 | form.Set("s", strconv.FormatInt(d.S, 10))
50 | form.Set("b", strconv.FormatInt(d.B, 10))
51 | form.Set("dm", "cd")
52 |
53 | return "https://geo.captcha-delivery.com/interstitial/?" + form.URLEncode(), nil
54 | }
55 |
56 | // ParseSliderDeviceCheckLink parses the device check url (/captcha/?initialCid...) from a blocked response body
57 | //
58 | // the datadome cookie is the current value of the 'datadome' cookie
59 | func ParseSliderDeviceCheckLink(body io.Reader, datadomeCookie, referer string) (string, error) {
60 | bodyBytes, err := io.ReadAll(body)
61 | if err != nil {
62 | return "", err
63 | }
64 |
65 | matches := ddRegex.FindSubmatch(bodyBytes)
66 | if matches == nil || len(matches) < 2 {
67 | return "", errors.New("DD object not found")
68 | }
69 |
70 | jsonObject := append([]byte("{"), bytes.TrimSpace(matches[1])...)
71 | jsonObject = append(jsonObject, []byte("}")...)
72 |
73 | jsonObject = singleQuoteRegex.ReplaceAll(jsonObject, []byte(`"$1"`))
74 | jsonObject = keyRegex.ReplaceAll(jsonObject, []byte(`$1"$2":`))
75 |
76 | var d dd
77 | err = jsoniter.Unmarshal(jsonObject, &d)
78 | if err != nil {
79 | return "", err
80 | }
81 |
82 | if d.T == "bv" {
83 | return "", errors.New("proxy blocked")
84 | }
85 |
86 | form := new(OrderedForm.OrderedForm)
87 | form.Set("initialCid", d.Cid)
88 | form.Set("hash", d.Hsh)
89 | form.Set("cid", datadomeCookie)
90 | form.Set("t", d.T)
91 | form.Set("referer", referer)
92 | form.Set("s", strconv.FormatInt(d.S, 10))
93 | form.Set("e", d.E)
94 | form.Set("dm", "cd")
95 |
96 | return "https://geo.captcha-delivery.com/captcha/?" + form.URLEncode(), nil
97 | }
98 |
99 | type dd struct {
100 | Rt string `json:"rt"`
101 | Cid string `json:"cid"`
102 | Hsh string `json:"hsh"`
103 | B int64 `json:"b"`
104 | S int64 `json:"s"`
105 | E string `json:"e"`
106 | T string `json:"t"`
107 | }
108 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Hyper-Solutions/hyper-sdk-go/v2
2 |
3 | go 1.22.0
4 |
5 | require (
6 | github.com/Hyper-Solutions/orderedobject v1.0.0
7 | github.com/andybalholm/brotli v1.1.1
8 | github.com/golang-jwt/jwt/v5 v5.2.2
9 | github.com/json-iterator/go v1.1.12
10 | github.com/justhyped/OrderedForm v0.0.0-20230202094228-bc7aa3c135e8
11 | github.com/klauspost/compress v1.18.0
12 | github.com/mailru/easyjson v0.9.0
13 | )
14 |
15 | require (
16 | github.com/josharian/intern v1.0.0 // indirect
17 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
18 | github.com/modern-go/reflect2 v1.0.2 // indirect
19 | )
20 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/Hyper-Solutions/orderedobject v1.0.0 h1:y91n3VD0fIzv3Zj9OA8ACqTJS+v8EkoTj3VEe8cZWwA=
2 | github.com/Hyper-Solutions/orderedobject v1.0.0/go.mod h1:zv+R9tKJzKHsmriExjZbU2e0TT6fcXk+GE5QrrFln6Y=
3 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
4 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
9 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
10 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
11 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
12 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
13 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
14 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
15 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
16 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
17 | github.com/justhyped/OrderedForm v0.0.0-20230202094228-bc7aa3c135e8 h1:dlQIJuSXTHJNoWxyq1+/dWr+WZv/Wj+x0v11oCszeoU=
18 | github.com/justhyped/OrderedForm v0.0.0-20230202094228-bc7aa3c135e8/go.mod h1:dhyuQuEIjVZRWCyZmq2BVKzXFLdMiidE7yRX8BTlqmw=
19 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
20 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
21 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
22 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
23 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
24 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
25 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
28 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
29 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
33 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
34 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
35 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
36 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
37 |
--------------------------------------------------------------------------------
/incapsula.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/url"
7 | )
8 |
9 | // GenerateReese84Sensor returns the sensor data required to generate valid reese84 cookies using the Hyper Solutions API.
10 | func (s *Session) GenerateReese84Sensor(ctx context.Context, site string, input *ReeseInput) (string, error) {
11 | response, err := sendRequest[*ReeseInput, *apiResponse](ctx, s, "https://incapsula.hypersolutions.co/reese84/"+url.PathEscape(site), input)
12 | if err != nil {
13 | return "", err
14 | }
15 | if response.Error != "" {
16 | return "", fmt.Errorf("api returned with: %s", response.Error)
17 | }
18 |
19 | return response.Payload, nil
20 | }
21 |
22 | // GenerateUtmvcCookie returns the utmvc cookie using the Hyper Solutions API.
23 | func (s *Session) GenerateUtmvcCookie(ctx context.Context, input *UtmvcInput) (string, string, error) {
24 | response, err := sendRequest[*UtmvcInput, *apiResponse](ctx, s, "https://incapsula.hypersolutions.co/utmvc", input)
25 | if err != nil {
26 | return "", "", err
27 | }
28 | if response.Error != "" {
29 | return "", "", fmt.Errorf("api returned with: %s", response.Error)
30 | }
31 |
32 | return response.Payload, response.Swhanedl, nil
33 | }
34 |
--------------------------------------------------------------------------------
/incapsula/dynamic.go:
--------------------------------------------------------------------------------
1 | package incapsula
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "net/url"
7 | "regexp"
8 | "strings"
9 | )
10 |
11 | var (
12 | reeseScriptRegex = regexp.MustCompile(`src\s*=\s*"((/[^/]+/\d+)(?:\?.*)?)"`)
13 |
14 | ErrReeseScriptNotFound = errors.New("hyper: reese script not found")
15 | ErrNotInterruptionPage = errors.New("hyper: not an interruption page")
16 | ErrInvalidURL = errors.New("hyper: invalid URL")
17 | )
18 |
19 | // ParseDynamicReeseScript parses the sensor path and script path from the given HTML content.
20 | //
21 | // This function searches the provided HTML for a script element containing a specific pattern
22 | // and extracts both the sensor path (shortened path) and script path (the full path).
23 | // It requires that the HTML contains "Pardon Our Interruption" to confirm it's the correct page type.
24 | // It also takes a URL string, extracts the hostname, and appends it to the sensor path.
25 | // Returns the sensor path (with hostname) and script path if found, or appropriate errors otherwise.
26 | func ParseDynamicReeseScript(html io.Reader, urlStr string) (sensorPath string, scriptPath string, err error) {
27 | // Parse the URL to extract hostname
28 | parsedURL, err := url.Parse(urlStr)
29 | if err != nil {
30 | return "", "", ErrInvalidURL
31 | }
32 | hostname := parsedURL.Hostname()
33 |
34 | bytes, err := io.ReadAll(html)
35 | if err != nil {
36 | return "", "", err
37 | }
38 |
39 | content := string(bytes)
40 |
41 | // Verify this is an interruption page
42 | if !strings.Contains(content, "Pardon Our Interruption") {
43 | return "", "", ErrNotInterruptionPage
44 | }
45 |
46 | matches := reeseScriptRegex.FindStringSubmatch(content)
47 | if len(matches) < 3 {
48 | return "", "", ErrReeseScriptNotFound
49 | }
50 |
51 | scriptPath = matches[1]
52 | sensorPath = matches[2]
53 |
54 | // Append the hostname to the sensor path
55 | return sensorPath + "?d=" + hostname, scriptPath, nil
56 | }
57 |
--------------------------------------------------------------------------------
/incapsula/dynamic_test.go:
--------------------------------------------------------------------------------
1 | package incapsula
2 |
3 | import (
4 | "os"
5 | "testing"
6 | )
7 |
8 | func TestParseDynamicReeseScript(t *testing.T) {
9 | file, err := os.Open("../scripts/reese.html")
10 | if err != nil {
11 | t.Fatal(err)
12 | }
13 | defer file.Close()
14 |
15 | sensorPath, scriptPath, err := ParseDynamicReeseScript(file, "https://www.smythstoys.com/")
16 | if err != nil {
17 | t.Fatal(err)
18 | }
19 |
20 | t.Log(sensorPath, scriptPath)
21 | }
22 |
--------------------------------------------------------------------------------
/incapsula/utmvc.go:
--------------------------------------------------------------------------------
1 | package incapsula
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "math/rand"
7 | "regexp"
8 | "strconv"
9 | )
10 |
11 | var (
12 | scriptRegex = regexp.MustCompile(`src="(/_Incapsula_Resource\?[^"]*)"`)
13 |
14 | ErrScriptNotFound = errors.New("hyper: utmvc script not found")
15 | )
16 |
17 | // ParseUtmvcScriptPath parses the UTMVC script path from the given script content.
18 | //
19 | // This function searches the provided script content for a specific pattern matching the UTMVC script path
20 | // using a precompiled regular expression. It extracts and returns the first match if found.
21 | func ParseUtmvcScriptPath(script io.Reader) (string, error) {
22 | bytes, err := io.ReadAll(script)
23 | if err != nil {
24 | return "", err
25 | }
26 |
27 | match := scriptRegex.FindSubmatch(bytes)
28 | if len(match) < 2 {
29 | return "", ErrScriptNotFound
30 | }
31 |
32 | return string(match[1]), nil
33 | }
34 |
35 | // GetUtmvcSubmitPath generates a UTMVC submit path with a unique random query parameter.
36 | //
37 | // This function constructs a submit path for the UTMVC script by appending a random floating-point number as a query
38 | // parameter. The random number is used to ensure the uniqueness of the request.
39 | func GetUtmvcSubmitPath() string {
40 | return "/_Incapsula_Resource?SWKMTFSR=1&e=" + strconv.FormatFloat(rand.Float64(), 'g', -1, 64)
41 | }
42 |
--------------------------------------------------------------------------------
/incapsula/utmvc_test.go:
--------------------------------------------------------------------------------
1 | package incapsula
2 |
3 | import (
4 | "os"
5 | "testing"
6 | )
7 |
8 | func TestParseUtmvcScriptPath(t *testing.T) {
9 | file, err := os.Open("../scripts/utmvc.html")
10 | if err != nil {
11 | t.Fatal(err)
12 | }
13 |
14 | scriptPath, err := ParseUtmvcScriptPath(file)
15 | if err != nil {
16 | t.Fatal(err)
17 | }
18 |
19 | t.Log(scriptPath)
20 | }
21 |
--------------------------------------------------------------------------------
/internal/decompress.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "bytes"
5 | "github.com/andybalholm/brotli"
6 | "github.com/klauspost/compress/flate"
7 | "github.com/klauspost/compress/gzip"
8 | "github.com/klauspost/compress/zlib"
9 | "github.com/klauspost/compress/zstd"
10 | "io"
11 | "net/http"
12 | "strings"
13 | "sync"
14 | )
15 |
16 | // Decoder pools
17 | var (
18 | zstdDecoderPool = sync.Pool{
19 | New: func() interface{} {
20 | decoder, err := zstd.NewReader(nil)
21 | if err != nil {
22 | panic(err)
23 | }
24 | return decoder
25 | },
26 | }
27 |
28 | gzipDecoderPool = sync.Pool{
29 | New: func() interface{} {
30 | return new(gzip.Reader)
31 | },
32 | }
33 | )
34 |
35 | // DecompressResponse decompresses the response body based on Content-Encoding header
36 | func DecompressResponse(resp *http.Response) ([]byte, error) {
37 | if resp == nil || resp.Body == nil {
38 | return nil, nil
39 | }
40 |
41 | encoding := strings.ToLower(resp.Header.Get("Content-Encoding"))
42 | if encoding == "" {
43 | return io.ReadAll(resp.Body)
44 | }
45 |
46 | switch {
47 | case strings.Contains(encoding, "gzip"):
48 | return handleGzip(resp.Body)
49 | case strings.Contains(encoding, "zstd"):
50 | return handleZstd(resp.Body)
51 | case strings.Contains(encoding, "br"):
52 | return handleBrotli(resp.Body)
53 | case strings.Contains(encoding, "deflate"):
54 | return handleDeflate(resp.Body)
55 | default:
56 | // Unknown encoding, try to read as-is
57 | return io.ReadAll(resp.Body)
58 | }
59 | }
60 |
61 | func handleGzip(body io.ReadCloser) ([]byte, error) {
62 | reader := gzipDecoderPool.Get().(*gzip.Reader)
63 | defer gzipDecoderPool.Put(reader)
64 |
65 | if err := reader.Reset(body); err != nil {
66 | return nil, err
67 | }
68 | defer reader.Close()
69 |
70 | return io.ReadAll(reader)
71 | }
72 |
73 | func handleZstd(body io.ReadCloser) ([]byte, error) {
74 | decoder := zstdDecoderPool.Get().(*zstd.Decoder)
75 | defer zstdDecoderPool.Put(decoder)
76 |
77 | decoder.Reset(body)
78 | return io.ReadAll(decoder)
79 | }
80 |
81 | func handleBrotli(body io.ReadCloser) ([]byte, error) {
82 | brReader := brotli.NewReader(body)
83 | return io.ReadAll(brReader)
84 | }
85 |
86 | func handleDeflate(body io.ReadCloser) ([]byte, error) {
87 | // Read the entire compressed body first
88 | compressedData, err := io.ReadAll(body)
89 | if err != nil {
90 | return nil, err
91 | }
92 |
93 | // First try with zlib (RFC 1950) which is the correct format for HTTP "deflate"
94 | zlibReader, err := zlib.NewReader(bytes.NewReader(compressedData))
95 | if err == nil {
96 | defer zlibReader.Close()
97 | return io.ReadAll(zlibReader)
98 | }
99 |
100 | // If zlib fails, try raw deflate as a fallback (RFC 1951)
101 | // Some servers incorrectly send raw deflate data without the zlib wrapper
102 | rawReader := flate.NewReader(bytes.NewReader(compressedData))
103 | defer rawReader.Close()
104 | return io.ReadAll(rawReader)
105 | }
106 |
--------------------------------------------------------------------------------
/internal/zstd.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "github.com/klauspost/compress/zstd"
7 | "sync"
8 | )
9 |
10 | var zstdEncoderPool = sync.Pool{
11 | New: func() interface{} {
12 | encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedDefault))
13 | if err != nil {
14 | panic(fmt.Sprintf("failed to create zstd encoder: %v", err))
15 | }
16 | return encoder
17 | },
18 | }
19 |
20 | func CompressZstd(data []byte) ([]byte, error) {
21 | encoderInterface := zstdEncoderPool.Get()
22 | encoder := encoderInterface.(*zstd.Encoder)
23 | defer zstdEncoderPool.Put(encoder)
24 |
25 | var buf bytes.Buffer
26 |
27 | encoder.Reset(&buf)
28 |
29 | if _, err := encoder.Write(data); err != nil {
30 | return nil, err
31 | }
32 |
33 | if err := encoder.Close(); err != nil {
34 | return nil, err
35 | }
36 |
37 | return buf.Bytes(), nil
38 | }
39 |
--------------------------------------------------------------------------------
/kasada.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "context"
5 | "encoding/base64"
6 | "fmt"
7 | )
8 |
9 | // GenerateKasadaPayload returns the payload to POST to /tl in bytes, and the generated headers
10 | func (s *Session) GenerateKasadaPayload(ctx context.Context, input *KasadaPayloadInput) ([]byte, *KasadaHeaders, error) {
11 | response, err := sendRequest[*KasadaPayloadInput, *kasadaPayloadOutput](ctx, s, "https://kasada.hypersolutions.co/payload", input)
12 | if err != nil {
13 | return nil, nil, err
14 | }
15 |
16 | if response.Error != "" {
17 | return nil, nil, fmt.Errorf("api returned with: %s", response.Error)
18 | }
19 |
20 | decodedPayload, err := base64.StdEncoding.DecodeString(response.Payload)
21 | if err != nil {
22 | return nil, nil, err
23 | }
24 |
25 | return decodedPayload, &response.Headers, nil
26 | }
27 |
28 | // GenerateKasadaPow returns the x-kpsdk-cd value
29 | func (s *Session) GenerateKasadaPow(ctx context.Context, input *KasadaPowInput) (string, error) {
30 | response, err := sendRequest[*KasadaPowInput, *apiResponse](ctx, s, "https://kasada.hypersolutions.co/cd", input)
31 | if err != nil {
32 | return "", err
33 | }
34 |
35 | if response.Error != "" {
36 | return "", fmt.Errorf("api returned with: %s", response.Error)
37 | }
38 |
39 | return response.Payload, nil
40 | }
41 |
--------------------------------------------------------------------------------
/kasada/script_path.go:
--------------------------------------------------------------------------------
1 | package kasada
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "regexp"
7 | "strings"
8 | )
9 |
10 | var (
11 | scriptPathExpr = regexp.MustCompile(`