http_mimic 0.5.1 → 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,88 +11,86 @@ 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
- auto_fallback: false,
26
- solve_waf: false
27
- )
28
- 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])
29
17
 
30
- sensor_js = script_resp.body
31
- context_js = File.read(CONTEXT_JS_PATH)
32
- cookie_str = cookies.to_cookie_string
18
+ sensor_script_urls = extract_sensor_script_urls(target_url, initial_response.body)
19
+ return nil if sensor_script_urls.empty?
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?
33
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
- # 2. Run virtual browser simulation inside QuickJS
39
- driver_script = <<~JS
40
- globalThis.__TARGET_URL__ = #{target_url.to_json};
41
- globalThis.__DOCUMENT_TITLE__ = #{doc_title.to_json};
42
- globalThis.__INITIAL_COOKIES__ = #{cookie_str.to_json};
43
- globalThis.__REFERRER__ = #{referer.to_json};
44
- #{user_agent ? "globalThis.__USER_AGENT__ = #{user_agent.to_json};" : ""}
45
-
46
- #{context_js}
47
- #{sensor_js}
48
-
49
- if (typeof globalThis.__simulateHumanInteractions === "function") {
50
- globalThis.__simulateHumanInteractions();
51
- }
52
- if (typeof globalThis.__drainEventLoop === "function") {
53
- globalThis.__drainEventLoop(100);
54
- }
55
-
56
- JSON.stringify({
57
- sensor_posts: globalThis.__sensor_posts
58
- });
59
- JS
43
+ # 2. Phase 1: Virtual browser simulation inside QuickJS (initial telemetry)
44
+ driver_script_1 = build_driver_script(
45
+ target_url: target_url,
46
+ doc_title: doc_title,
47
+ cookie_str: cookies.to_cookie_string,
48
+ referer: referer,
49
+ user_agent: user_agent,
50
+ context_js: context_js,
51
+ scripts_data: scripts_data,
52
+ phase: 1
53
+ )
60
54
 
61
- result = JSRuntime.eval_json(driver_script)
62
- sensor_posts = result && result['sensor_posts']
63
- return nil if sensor_posts.nil? || sensor_posts.empty?
55
+ result1 = JSRuntime.eval_json(driver_script_1)
56
+ sensor_posts_1 = result1 && result1['sensor_posts']
57
+ return nil if sensor_posts_1.nil? || sensor_posts_1.empty?
64
58
 
65
- # 3. Post sensor data to Akamai endpoint
59
+ # 3. Post Phase 1 sensor data to Akamai endpoint
66
60
  updated_cookies = cookies.dup
67
- sensor_posts.each do |post|
68
- post_body = post['body']
69
- next if post_body.nil? || post_body.empty?
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)
70
62
 
71
- post_resp = HttpMimic.post(
72
- sensor_script_url,
73
- impersonate: impersonate,
74
- cookies: updated_cookies,
75
- headers: {
76
- 'Content-Type' => 'text/plain;charset=UTF-8',
77
- 'Referer' => target_url,
78
- 'Origin' => URI.parse(target_url).tap { |u| u.path = ''; u.query = nil }.to_s,
79
- 'Accept' => '*/*'
80
- },
81
- body: post_body,
82
- auto_fallback: false,
83
- solve_waf: false
63
+ # 4. Phase 2: If _abck is not verified yet (~0~), perform second round after short pause
64
+ if !verified_abck?(updated_cookies)
65
+ sleep 0.8
66
+ driver_script_2 = build_driver_script(
67
+ target_url: target_url,
68
+ doc_title: doc_title,
69
+ cookie_str: updated_cookies.to_cookie_string,
70
+ referer: referer,
71
+ user_agent: user_agent,
72
+ context_js: context_js,
73
+ scripts_data: scripts_data,
74
+ phase: 2
84
75
  )
85
76
 
86
- post_resp.cookies.each { |k, v| updated_cookies[k] = v }
77
+ result2 = JSRuntime.eval_json(driver_script_2)
78
+ sensor_posts_2 = result2 && result2['sensor_posts']
79
+ if sensor_posts_2 && !sensor_posts_2.empty?
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)
81
+ end
87
82
  end
88
83
 
89
- # 4. Retry original target URL with the updated cookies
90
- retry_headers = (options[:headers] || {}).merge('Referer' => target_url)
84
+ # 5. Retry original target URL with the updated cookies
85
+ retry_headers = (options[:headers] || {}).merge('Referer' => target_url, 'User-Agent' => user_agent)
91
86
  HttpMimic.get(
92
87
  target_url,
93
88
  options.merge(
89
+ profile: profile,
90
+ impersonate: impersonate,
94
91
  cookies: updated_cookies,
95
92
  headers: retry_headers,
93
+ user_agent: user_agent,
96
94
  auto_fallback: false,
97
95
  solve_waf: false
98
96
  )
@@ -104,19 +102,206 @@ module HttpMimic
104
102
  nil
105
103
  end
106
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
+
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
+
164
+ <<~JS
165
+ globalThis.__TARGET_URL__ = #{params[:target_url].to_json};
166
+ globalThis.__DOCUMENT_TITLE__ = #{params[:doc_title].to_json};
167
+ globalThis.__INITIAL_COOKIES__ = #{params[:cookie_str].to_json};
168
+ globalThis.__REFERRER__ = #{params[:referer].to_json};
169
+ #{params[:user_agent] ? "globalThis.__USER_AGENT__ = #{params[:user_agent].to_json};" : ""}
170
+ globalThis.__TELEMETRY_PHASE__ = #{params[:phase] || 1};
171
+
172
+ #{params[:context_js]}
173
+ ;
174
+
175
+ #{scripts_code}
176
+
177
+ if (typeof globalThis.__simulateHumanInteractions === "function") {
178
+ globalThis.__simulateHumanInteractions();
179
+ }
180
+ if (typeof globalThis.__drainEventLoop === "function") {
181
+ globalThis.__drainEventLoop(100);
182
+ }
183
+
184
+ JSON.stringify({
185
+ sensor_posts: globalThis.__sensor_posts
186
+ });
187
+ JS
188
+ end
189
+
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)
191
+ origin_url = URI.parse(target_url).tap { |u| u.path = ''; u.query = nil }.to_s rescue target_url
192
+ sensor_posts.each do |post|
193
+ post_body = post['body']
194
+ method = (post['method'] || 'POST').to_s.upcase
195
+ next if method == 'POST' && (post_body.nil? || post_body.empty?)
196
+
197
+ post_url = if post['url'].to_s.empty?
198
+ cpt_url || target_url
199
+ elsif post['url'].start_with?('http')
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
206
+ else
207
+ URI.join(target_url, post['url']).to_s
208
+ end
209
+
210
+
211
+ content_type = (post['headers'] && (post['headers']['Content-Type'] || post['headers']['content-type'])) || 'application/json'
212
+
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
255
+
256
+ post_resp.cookies.each { |k, v| updated_cookies[k] = v } if post_resp&.cookies
257
+ end
258
+ end
259
+
260
+ def verified_abck?(cookies)
261
+ return false unless cookies
262
+ abck = cookies['_abck'] || (cookies.respond_to?(:key?) && cookies['_abck'])
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
269
+ end
270
+
107
271
  def extract_sensor_script_url(target_url, html)
108
272
  return nil if html.nil? || html.empty?
109
273
 
110
- match = html.match(/<script[^>]*src=["\x27]([^"\x27]*\/[a-zA-Z0-9_-]{10,}\?[a-zA-Z0-9_=-]+)["\x27]/i)
274
+ match = html.match(/<script[^>]*src=["\x27]([^"\x27]*\/[a-zA-Z0-9_\-\/]+\?[^"\x27\s>]+)["\x27]/i)
111
275
  match ||= html.match(/<script[^>]*src=["\x27]([^"\x27]*akam[^"\x27]*)["\x27]/i)
112
- match ||= html.match(/<script[^>]*src=["\x27]([^"\x27]*\/[a-zA-Z0-9_\-\/]+\?v=[a-zA-Z0-9_-]+)["\x27]/i)
276
+ match ||= html.match(/<script[^>]*src=["\x27]([^"\x27]*\/[a-zA-Z0-9_-]{10,}\?[a-zA-Z0-9_=-]+)["\x27]/i)
113
277
 
114
278
  return nil unless match
115
279
 
116
- URI.join(target_url, match[1]).to_s
280
+ raw_url = match[1].gsub('&amp;', '&')
281
+ URI.join(target_url, raw_url).to_s
117
282
  rescue StandardError
118
283
  nil
119
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
120
305
  end
121
306
  end
122
307
  end