http_mimic 0.5.3 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/http_mimic.gemspec +1 -1
- data/lib/http_mimic/command_builder.rb +5 -3
- data/lib/http_mimic/configuration.rb +1 -1
- data/lib/http_mimic/request.rb +94 -41
- data/lib/http_mimic/response.rb +22 -3
- data/lib/http_mimic/response_parser.rb +6 -0
- data/lib/http_mimic/spa_detector.rb +1 -1
- data/lib/http_mimic/version.rb +2 -1
- data/lib/http_mimic/waf/akamai_solver.rb +174 -45
- data/lib/http_mimic/waf/browser_context.js +1297 -0
- metadata +2 -1
|
@@ -11,29 +11,34 @@ module HttpMimic
|
|
|
11
11
|
class << self
|
|
12
12
|
def solve(target_url, initial_response, options = {})
|
|
13
13
|
cookies = initial_response.cookies.dup
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return nil unless sensor_script_url
|
|
18
|
-
|
|
19
|
-
# 1. Fetch the dynamic sensor script
|
|
20
|
-
script_resp = HttpMimic.get(
|
|
21
|
-
sensor_script_url,
|
|
22
|
-
impersonate: impersonate,
|
|
23
|
-
cookies: cookies,
|
|
24
|
-
headers: { 'Referer' => target_url },
|
|
25
|
-
proxy: options[:proxy],
|
|
26
|
-
auto_fallback: false,
|
|
27
|
-
solve_waf: false
|
|
28
|
-
)
|
|
29
|
-
return nil unless script_resp.success?
|
|
14
|
+
profile = options[:profile]
|
|
15
|
+
impersonate = resolve_impersonate_target(profile, options[:impersonate])
|
|
16
|
+
user_agent = resolve_user_agent(impersonate, options[:user_agent])
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
sensor_script_urls = extract_sensor_script_urls(target_url, initial_response.body)
|
|
19
|
+
return nil if sensor_script_urls.empty?
|
|
33
20
|
|
|
21
|
+
# 1. Fetch all dynamic sensor scripts in order
|
|
22
|
+
scripts_data = []
|
|
23
|
+
sensor_script_urls.each do |s_url|
|
|
24
|
+
s_resp = HttpMimic.get(
|
|
25
|
+
s_url,
|
|
26
|
+
impersonate: impersonate,
|
|
27
|
+
cookies: cookies,
|
|
28
|
+
headers: { 'Referer' => target_url, 'User-Agent' => user_agent },
|
|
29
|
+
proxy: options[:proxy],
|
|
30
|
+
auto_fallback: false,
|
|
31
|
+
solve_waf: false,
|
|
32
|
+
debug: options[:debug]
|
|
33
|
+
)
|
|
34
|
+
scripts_data << { url: s_url, body: s_resp.body } if s_resp.success?
|
|
35
|
+
end
|
|
36
|
+
return nil if scripts_data.empty?
|
|
37
|
+
|
|
38
|
+
context_js = File.read(CONTEXT_JS_PATH)
|
|
34
39
|
doc_title = initial_response.title.to_s
|
|
35
|
-
user_agent = options[:user_agent]
|
|
36
40
|
referer = (options[:headers] || {})['Referer'] || (options[:headers] || {})['referer'] || target_url
|
|
41
|
+
cpt_script_url = sensor_script_urls.find { |u| u.include?('t=') } || sensor_script_urls.first
|
|
37
42
|
|
|
38
43
|
# 2. Phase 1: Virtual browser simulation inside QuickJS (initial telemetry)
|
|
39
44
|
driver_script_1 = build_driver_script(
|
|
@@ -43,7 +48,7 @@ module HttpMimic
|
|
|
43
48
|
referer: referer,
|
|
44
49
|
user_agent: user_agent,
|
|
45
50
|
context_js: context_js,
|
|
46
|
-
|
|
51
|
+
scripts_data: scripts_data,
|
|
47
52
|
phase: 1
|
|
48
53
|
)
|
|
49
54
|
|
|
@@ -53,7 +58,7 @@ module HttpMimic
|
|
|
53
58
|
|
|
54
59
|
# 3. Post Phase 1 sensor data to Akamai endpoint
|
|
55
60
|
updated_cookies = cookies.dup
|
|
56
|
-
post_sensor_data(
|
|
61
|
+
post_sensor_data(cpt_script_url, target_url, impersonate, updated_cookies, sensor_posts_1, user_agent: user_agent, proxy: options[:proxy], debug: options[:debug], cpt_url: cpt_script_url)
|
|
57
62
|
|
|
58
63
|
# 4. Phase 2: If _abck is not verified yet (~0~), perform second round after short pause
|
|
59
64
|
if !verified_abck?(updated_cookies)
|
|
@@ -65,24 +70,27 @@ module HttpMimic
|
|
|
65
70
|
referer: referer,
|
|
66
71
|
user_agent: user_agent,
|
|
67
72
|
context_js: context_js,
|
|
68
|
-
|
|
73
|
+
scripts_data: scripts_data,
|
|
69
74
|
phase: 2
|
|
70
75
|
)
|
|
71
76
|
|
|
72
77
|
result2 = JSRuntime.eval_json(driver_script_2)
|
|
73
78
|
sensor_posts_2 = result2 && result2['sensor_posts']
|
|
74
79
|
if sensor_posts_2 && !sensor_posts_2.empty?
|
|
75
|
-
post_sensor_data(
|
|
80
|
+
post_sensor_data(cpt_script_url, target_url, impersonate, updated_cookies, sensor_posts_2, user_agent: user_agent, proxy: options[:proxy], debug: options[:debug], cpt_url: cpt_script_url)
|
|
76
81
|
end
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
# 5. Retry original target URL with the updated cookies
|
|
80
|
-
retry_headers = (options[:headers] || {}).merge('Referer' => target_url)
|
|
85
|
+
retry_headers = (options[:headers] || {}).merge('Referer' => target_url, 'User-Agent' => user_agent)
|
|
81
86
|
HttpMimic.get(
|
|
82
87
|
target_url,
|
|
83
88
|
options.merge(
|
|
89
|
+
profile: profile,
|
|
90
|
+
impersonate: impersonate,
|
|
84
91
|
cookies: updated_cookies,
|
|
85
92
|
headers: retry_headers,
|
|
93
|
+
user_agent: user_agent,
|
|
86
94
|
auto_fallback: false,
|
|
87
95
|
solve_waf: false
|
|
88
96
|
)
|
|
@@ -94,7 +102,65 @@ module HttpMimic
|
|
|
94
102
|
nil
|
|
95
103
|
end
|
|
96
104
|
|
|
105
|
+
def resolve_impersonate_target(profile, explicit_imp = nil)
|
|
106
|
+
return explicit_imp.to_s if explicit_imp && !explicit_imp.empty?
|
|
107
|
+
|
|
108
|
+
case profile&.to_sym
|
|
109
|
+
when :android, :mobile
|
|
110
|
+
'chrome131_android'
|
|
111
|
+
when :ios
|
|
112
|
+
'safari260_ios'
|
|
113
|
+
when :firefox
|
|
114
|
+
'firefox135'
|
|
115
|
+
when :safari
|
|
116
|
+
'safari180'
|
|
117
|
+
else
|
|
118
|
+
HttpMimic.configuration.default_impersonate || 'chrome131'
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def resolve_user_agent(impersonate, explicit_ua = nil)
|
|
123
|
+
return explicit_ua if explicit_ua && !explicit_ua.empty?
|
|
124
|
+
|
|
125
|
+
imp = impersonate.to_s.downcase
|
|
126
|
+
if imp.include?('android')
|
|
127
|
+
ver = imp[/\d+/] || '131'
|
|
128
|
+
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/#{ver}.0.0.0 Mobile Safari/537.36"
|
|
129
|
+
elsif imp.include?('ios') || imp.include?('iphone') || imp.include?('ipad')
|
|
130
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1"
|
|
131
|
+
elsif imp.include?('chrome')
|
|
132
|
+
ver = imp[/\d+/] || '150'
|
|
133
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/#{ver}.0.0.0 Safari/537.36"
|
|
134
|
+
elsif imp.include?('firefox')
|
|
135
|
+
ver = imp[/\d+/] || '135'
|
|
136
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:#{ver}.0) Gecko/20100101 Firefox/#{ver}.0"
|
|
137
|
+
elsif imp.include?('safari')
|
|
138
|
+
ver = imp[/\d+/] || '18_0'
|
|
139
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/#{ver.tr('_', '.')} Safari/605.1.15"
|
|
140
|
+
else
|
|
141
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
97
145
|
def build_driver_script(params)
|
|
146
|
+
scripts_code = if params[:scripts_data] && !params[:scripts_data].empty?
|
|
147
|
+
params[:scripts_data].map.with_index do |s, idx|
|
|
148
|
+
tag_var = "sTag_#{idx}"
|
|
149
|
+
<<~SCRIPT_EVAL
|
|
150
|
+
const #{tag_var} = document.createElement('script');
|
|
151
|
+
#{tag_var}.src = #{s[:url].to_json};
|
|
152
|
+
document.body.appendChild(#{tag_var});
|
|
153
|
+
document.currentScript = #{tag_var};
|
|
154
|
+
try {
|
|
155
|
+
;\n#{s[:body]}\n;
|
|
156
|
+
} catch(e) {}
|
|
157
|
+
SCRIPT_EVAL
|
|
158
|
+
end.join("\n")
|
|
159
|
+
else
|
|
160
|
+
";\n#{params[:sensor_js]}\n;"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
|
|
98
164
|
<<~JS
|
|
99
165
|
globalThis.__TARGET_URL__ = #{params[:target_url].to_json};
|
|
100
166
|
globalThis.__DOCUMENT_TITLE__ = #{params[:doc_title].to_json};
|
|
@@ -104,7 +170,9 @@ module HttpMimic
|
|
|
104
170
|
globalThis.__TELEMETRY_PHASE__ = #{params[:phase] || 1};
|
|
105
171
|
|
|
106
172
|
#{params[:context_js]}
|
|
107
|
-
|
|
173
|
+
;
|
|
174
|
+
|
|
175
|
+
#{scripts_code}
|
|
108
176
|
|
|
109
177
|
if (typeof globalThis.__simulateHumanInteractions === "function") {
|
|
110
178
|
globalThis.__simulateHumanInteractions();
|
|
@@ -119,37 +187,71 @@ module HttpMimic
|
|
|
119
187
|
JS
|
|
120
188
|
end
|
|
121
189
|
|
|
122
|
-
def post_sensor_data(sensor_script_url, target_url, impersonate, updated_cookies, sensor_posts, proxy: nil)
|
|
190
|
+
def post_sensor_data(sensor_script_url, target_url, impersonate, updated_cookies, sensor_posts, user_agent: nil, proxy: nil, debug: nil, cpt_url: nil)
|
|
123
191
|
origin_url = URI.parse(target_url).tap { |u| u.path = ''; u.query = nil }.to_s rescue target_url
|
|
124
192
|
sensor_posts.each do |post|
|
|
125
193
|
post_body = post['body']
|
|
126
|
-
|
|
194
|
+
method = (post['method'] || 'POST').to_s.upcase
|
|
195
|
+
next if method == 'POST' && (post_body.nil? || post_body.empty?)
|
|
127
196
|
|
|
128
197
|
post_url = if post['url'].to_s.empty?
|
|
129
|
-
target_url
|
|
198
|
+
cpt_url || target_url
|
|
130
199
|
elsif post['url'].start_with?('http')
|
|
131
|
-
post['url']
|
|
200
|
+
u = post['url']
|
|
201
|
+
if cpt_url && !u.include?('t=') && cpt_url.include?('t=')
|
|
202
|
+
q = URI.parse(cpt_url).query rescue nil
|
|
203
|
+
u = "#{u}?#{q}" if q && !q.empty?
|
|
204
|
+
end
|
|
205
|
+
u
|
|
132
206
|
else
|
|
133
207
|
URI.join(target_url, post['url']).to_s
|
|
134
208
|
end
|
|
135
209
|
|
|
210
|
+
|
|
136
211
|
content_type = (post['headers'] && (post['headers']['Content-Type'] || post['headers']['content-type'])) || 'application/json'
|
|
137
212
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
213
|
+
post_headers = {
|
|
214
|
+
'Referer' => target_url,
|
|
215
|
+
'Origin' => origin_url,
|
|
216
|
+
'Accept' => '*/*',
|
|
217
|
+
'Sec-Fetch-Dest' => 'empty',
|
|
218
|
+
'Sec-Fetch-Mode' => 'cors',
|
|
219
|
+
'Sec-Fetch-Site' => 'same-origin'
|
|
220
|
+
}
|
|
221
|
+
if user_agent.to_s.include?('Chrome')
|
|
222
|
+
is_mobile = user_agent.to_s.include?('Mobile') || user_agent.to_s.include?('Android')
|
|
223
|
+
ver = user_agent[/Chrome\/(\d+)/, 1] || '131'
|
|
224
|
+
post_headers['sec-ch-ua'] = %("Google Chrome";v="#{ver}", "Chromium";v="#{ver}", "Not_A Brand";v="24")
|
|
225
|
+
post_headers['sec-ch-ua-mobile'] = is_mobile ? '?1' : '?0'
|
|
226
|
+
post_headers['sec-ch-ua-platform'] = is_mobile ? '"Android"' : '"macOS"'
|
|
227
|
+
end
|
|
228
|
+
post_headers['Content-Type'] = content_type if method == 'POST'
|
|
229
|
+
post_headers['User-Agent'] = user_agent if user_agent
|
|
230
|
+
|
|
231
|
+
post_resp = if method == 'GET'
|
|
232
|
+
HttpMimic.get(
|
|
233
|
+
post_url,
|
|
234
|
+
impersonate: impersonate,
|
|
235
|
+
cookies: updated_cookies,
|
|
236
|
+
headers: post_headers,
|
|
237
|
+
proxy: proxy,
|
|
238
|
+
auto_fallback: false,
|
|
239
|
+
solve_waf: false,
|
|
240
|
+
debug: debug
|
|
241
|
+
)
|
|
242
|
+
else
|
|
243
|
+
HttpMimic.post(
|
|
244
|
+
post_url,
|
|
245
|
+
impersonate: impersonate,
|
|
246
|
+
cookies: updated_cookies,
|
|
247
|
+
headers: post_headers,
|
|
248
|
+
proxy: proxy,
|
|
249
|
+
body: post_body,
|
|
250
|
+
auto_fallback: false,
|
|
251
|
+
solve_waf: false,
|
|
252
|
+
debug: debug
|
|
253
|
+
)
|
|
254
|
+
end
|
|
153
255
|
|
|
154
256
|
post_resp.cookies.each { |k, v| updated_cookies[k] = v } if post_resp&.cookies
|
|
155
257
|
end
|
|
@@ -158,7 +260,12 @@ module HttpMimic
|
|
|
158
260
|
def verified_abck?(cookies)
|
|
159
261
|
return false unless cookies
|
|
160
262
|
abck = cookies['_abck'] || (cookies.respond_to?(:key?) && cookies['_abck'])
|
|
161
|
-
abck.to_s.include?('~0~')
|
|
263
|
+
return true if abck.to_s.include?('~0~')
|
|
264
|
+
|
|
265
|
+
bm_sc = cookies['bm_sc'] || (cookies.respond_to?(:key?) && cookies['bm_sc'])
|
|
266
|
+
return true if bm_sc.to_s.include?('~0~0~0') && !bm_sc.to_s.start_with?('3~')
|
|
267
|
+
|
|
268
|
+
false
|
|
162
269
|
end
|
|
163
270
|
|
|
164
271
|
def extract_sensor_script_url(target_url, html)
|
|
@@ -175,6 +282,28 @@ module HttpMimic
|
|
|
175
282
|
rescue StandardError
|
|
176
283
|
nil
|
|
177
284
|
end
|
|
285
|
+
|
|
286
|
+
def extract_sensor_script_urls(target_url, html)
|
|
287
|
+
return [] if html.nil? || html.empty?
|
|
288
|
+
|
|
289
|
+
matches = html.scan(/<script[^>]*src=["\x27]([^"\x27]+)["\x27]/i).flatten
|
|
290
|
+
akamai_scripts = matches.select do |src|
|
|
291
|
+
src =~ /\/GPv76/i || src =~ /akam/i || src =~ /\/[a-zA-Z0-9_-]{10,}\?[a-zA-Z0-9_=-]+/i || src =~ /_bm\//i
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if akamai_scripts.empty?
|
|
295
|
+
single = extract_sensor_script_url(target_url, html)
|
|
296
|
+
return single ? [single] : []
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
akamai_scripts.map do |raw_src|
|
|
300
|
+
begin
|
|
301
|
+
URI.join(target_url, raw_src.gsub('&', '&')).to_s
|
|
302
|
+
rescue StandardError
|
|
303
|
+
nil
|
|
304
|
+
end
|
|
305
|
+
end.compact
|
|
306
|
+
end
|
|
178
307
|
end
|
|
179
308
|
end
|
|
180
309
|
end
|