http_mimic 0.5.3 → 0.5.5

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.
@@ -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
- impersonate = options[:impersonate] || HttpMimic.configuration.default_impersonate || 'chrome131'
15
-
16
- sensor_script_url = extract_sensor_script_url(target_url, initial_response.body)
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
- sensor_js = script_resp.body
32
- context_js = File.read(CONTEXT_JS_PATH)
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
- sensor_js: sensor_js,
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(sensor_script_url, target_url, impersonate, updated_cookies, sensor_posts_1, proxy: options[:proxy])
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
- sensor_js: sensor_js,
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(sensor_script_url, target_url, impersonate, updated_cookies, sensor_posts_2, proxy: options[:proxy])
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
- #{params[:sensor_js]}
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
- next if post_body.nil? || post_body.empty?
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
- post_resp = HttpMimic.post(
139
- post_url,
140
- impersonate: impersonate,
141
- cookies: updated_cookies,
142
- headers: {
143
- 'Content-Type' => content_type,
144
- 'Referer' => target_url,
145
- 'Origin' => origin_url,
146
- 'Accept' => '*/*'
147
- },
148
- proxy: proxy,
149
- body: post_body,
150
- auto_fallback: false,
151
- solve_waf: false
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,26 @@ 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
+ URI.join(target_url, raw_src.gsub('&amp;', '&')).to_s
301
+ rescue StandardError
302
+ nil
303
+ end.compact
304
+ end
178
305
  end
179
306
  end
180
307
  end