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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -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 +172 -45
- data/lib/http_mimic/waf/browser_context.js +1297 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f042a9d602736a448c5341e306e8a46c68fcc38e5159cc881a157a52845d994
|
|
4
|
+
data.tar.gz: 03f41604be51bc6050e1778cd4411a0c3749ad04ef823c7d99839036ec2a1784
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53fa3674cd113da005dfa6a91b46ec2481dbe0c0f8e5fefb3594bb0e84d4b7258569c519d818bcb14a10034de61308155878cfb06cd8965f3e9c7e82613a338c
|
|
7
|
+
data.tar.gz: 47696a2a7f80d0ed2a8ca81236cd9a00f1de6fa8e5ae02bfae3aa2ef2b2965169497e49ee3aec585757c7fae12ac6a63da4398b3c57b720e3ccf6917416e433b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.5] - 2026-09-05
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **QuickJS CPT & Bot Manager Telemetry Resolution (Tier 1 WAF Bypass)**:
|
|
12
|
+
- **Standard `document.cookie` CookieJar**: Replaced the plain property in `browser_context.js` with a compliant `Map`-backed CookieJar. When Akamai CPT writes `document.cookie = "bm_lso=..."`, existing session cookies (`bm_sz`, `_abck`) are preserved rather than overwritten. This fixes the critical bug where main sensor telemetry fell back to default hash `8888888` instead of the session cookie hash.
|
|
13
|
+
- **`HTMLScriptElement.src` Property Reflection**: Implemented bidirectional synchronization between `el.src` and `el.getAttribute("src")`. CPT scripts can now successfully inspect `document.currentScript.getAttribute("src")` to locate their challenge parameters and relative post endpoints.
|
|
14
|
+
- **Accurate `document.currentScript` Execution Pipeline**: `AkamaiSolver.build_driver_script` now assigns `document.currentScript` before evaluating each script block, matching real browser multi-script execution order.
|
|
15
|
+
- **Hybrid `XMLHttpRequest` with `responseURL` Support**: Retained instance-level methods (`open`, `send`) required by obfuscated Akamai scripts while maintaining prototype-level methods for DOM script hooks, and populating `responseURL` to allow `challenge.html` reload checks to succeed.
|
|
16
|
+
- **Challenge Query String Preservation in Telemetry POST**: When sending CPT PoW solutions, `AkamaiSolver` now preserves query parameters (e.g. `v=...&t=...`) from the original challenge script URL so that edge servers correctly issue validated `bm_sc` session tokens.
|
|
17
|
+
- **End-to-End Akamai Verification**:
|
|
18
|
+
- `HttpMimic.get('https://www.adidas.com/om/en/samba-og-shoes/KK2371.html')` now successfully solves the CPT challenge within QuickJS and returns the complete 400KB+ product HTML without invoking Tier 2 Obscura.
|
|
19
|
+
|
|
20
|
+
## [0.5.4] - 2026-09-05
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **Challenge Page Regression Prevention**:
|
|
25
|
+
- `HttpMimic::Request` no longer preserves un-bypassed WAF challenge pages (such as Akamai 200 CPT skeletons) as `best_response`. If all resolution and fallback attempts fail, true status codes (e.g. 403) are preserved instead of overwriting with skeleton 200 responses.
|
|
26
|
+
- **Explicit Profile Forwarding in Fallbacks & Retries**:
|
|
27
|
+
- `determine_profiles` now respects `explicit_profile = options[:profile]`, ensuring targeted profiles (e.g., `:android`) remain active when `auto_fallback: false` and are prioritized during fallback loops.
|
|
28
|
+
- `AkamaiSolver` now explicitly forwards `profile` and concrete `impersonate` targets during final URL retries to prevent TLS/fingerprint mismatches.
|
|
29
|
+
- **Mobile/Android Emulation Fidelity in QuickJS Browser Polyfill**:
|
|
30
|
+
- Fixed typo in Android platform identifier (`Linux armv8l` instead of `Linux armv81`).
|
|
31
|
+
- Mobile environments now correctly emulate zero-length plugin and mimeType lists (removing desktop-only PDF plugins).
|
|
32
|
+
- Aligned mobile screen dimensions (412x915 portrait-primary) and WebGL renderer parameters (Qualcomm Adreno 640).
|
|
33
|
+
- **Default Impersonate Profile Upgraded to `chrome150`**:
|
|
34
|
+
- Upgraded default client TLS fingerprint to `chrome150` to satisfy modern Akamai edge HTTP/2 and TLS cipher requirements.
|
|
35
|
+
- **Modern CORS & Fetch Headers in Sensor POSTs**:
|
|
36
|
+
- Automatically injected `Sec-Fetch-Dest: empty`, `Sec-Fetch-Mode: cors`, `Sec-Fetch-Site: same-origin`, and client hints (`sec-ch-ua`, `sec-ch-ua-mobile`, `sec-ch-ua-platform`).
|
|
37
|
+
|
|
8
38
|
## [0.5.3] - 2026-09-05
|
|
9
39
|
|
|
10
40
|
### Added
|
data/http_mimic.gemspec
CHANGED
|
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
|
|
22
22
|
spec.required_ruby_version = ">= 2.3.0"
|
|
23
23
|
|
|
24
|
-
spec.files = Dir["lib/**/*.rb", "README.md", "CHANGELOG.md", "LICENSE.txt", "http_mimic.gemspec"]
|
|
24
|
+
spec.files = Dir["lib/**/*.{rb,js}", "README.md", "CHANGELOG.md", "LICENSE.txt", "http_mimic.gemspec"]
|
|
25
25
|
spec.require_paths = ["lib"]
|
|
26
26
|
|
|
27
27
|
spec.add_development_dependency "bundler", ">= 1.17"
|
|
@@ -15,7 +15,7 @@ module HttpMimic
|
|
|
15
15
|
|
|
16
16
|
TARGET_BINARY_MAP = {
|
|
17
17
|
# Chrome Desktop
|
|
18
|
-
'chrome' => %w[curl_chrome131 curl_chrome124 curl_chrome120 curl_chrome116 curl_chrome110 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
18
|
+
'chrome' => %w[curl_chrome150 curl_chrome146 curl_chrome145 curl_chrome136 curl_chrome133a curl_chrome131 curl_chrome124 curl_chrome120 curl_chrome116 curl_chrome110 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
19
19
|
'chrome116' => %w[curl_chrome116 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
20
20
|
'chrome120' => %w[curl_chrome120 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
21
21
|
'chrome123' => %w[curl_chrome123 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
@@ -118,10 +118,12 @@ module HttpMimic
|
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# Timeouts
|
|
121
|
-
|
|
121
|
+
default_max_time = options[:auto_proxy] ? 12 : config.default_timeout
|
|
122
|
+
timeout = options[:timeout] || options[:read_timeout] || default_max_time
|
|
122
123
|
args << '--max-time' << timeout.to_s if timeout
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
default_conn_timeout = options[:auto_proxy] ? 4 : config.default_connect_timeout
|
|
126
|
+
connect_timeout = options[:connect_timeout] || default_conn_timeout
|
|
125
127
|
args << '--connect-timeout' << connect_timeout.to_s if connect_timeout
|
|
126
128
|
|
|
127
129
|
# SSL / Insecure
|
data/lib/http_mimic/request.rb
CHANGED
|
@@ -48,8 +48,8 @@ module HttpMimic
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
attempts = []
|
|
51
|
-
profiles_to_try = determine_profiles(mode, auto_fallback)
|
|
52
|
-
|
|
51
|
+
profiles_to_try = determine_profiles(mode, auto_fallback, options[:profile])
|
|
52
|
+
waf_solve_attempts = 0
|
|
53
53
|
best_response = nil
|
|
54
54
|
proxy_retries_left = should_auto_proxy ? (options[:proxy_retries] || config.proxy_retries || 3) : 0
|
|
55
55
|
|
|
@@ -60,7 +60,7 @@ module HttpMimic
|
|
|
60
60
|
|
|
61
61
|
profiles_to_try.each_with_index do |profile, index|
|
|
62
62
|
# Never fall back to plain curl if a WAF challenge has already been detected/attempted
|
|
63
|
-
if profile == :curl &&
|
|
63
|
+
if profile == :curl && waf_solve_attempts > 0
|
|
64
64
|
log_debug("[HttpMimic] Skipping plain :curl fallback for protected WAF target.")
|
|
65
65
|
next
|
|
66
66
|
end
|
|
@@ -79,25 +79,6 @@ module HttpMimic
|
|
|
79
79
|
log_debug("Curl exit status: #{status.exitstatus}")
|
|
80
80
|
log_debug("Curl stderr: #{stderr}") unless stderr.empty?
|
|
81
81
|
|
|
82
|
-
# Handle proxy failure retry when auto_proxy is enabled
|
|
83
|
-
if should_auto_proxy && current_opts[:proxy] && (status.exitstatus != 0)
|
|
84
|
-
is_proxy_err = [5, 7, 28, 35, 56].include?(status.exitstatus) ||
|
|
85
|
-
stderr.include?('Failed to connect to') ||
|
|
86
|
-
stderr.include?('tunneling failed') ||
|
|
87
|
-
stderr.downcase.include?('proxy')
|
|
88
|
-
|
|
89
|
-
if is_proxy_err
|
|
90
|
-
ProxyPool.mark_dead(current_opts[:proxy])
|
|
91
|
-
if proxy_retries_left > 0
|
|
92
|
-
proxy_retries_left -= 1
|
|
93
|
-
new_proxy = ProxyPool.get
|
|
94
|
-
log_debug("[HttpMimic::ProxyPool] Proxy #{current_opts[:proxy]} failed (exit #{status.exitstatus}). Retrying with new proxy #{new_proxy} (#{proxy_retries_left} retries left)...")
|
|
95
|
-
options[:proxy] = new_proxy
|
|
96
|
-
redo
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
82
|
response = ResponseParser.new(
|
|
102
83
|
stdout,
|
|
103
84
|
exit_status: status,
|
|
@@ -106,7 +87,23 @@ module HttpMimic
|
|
|
106
87
|
request_url: final_url
|
|
107
88
|
).parse
|
|
108
89
|
|
|
109
|
-
|
|
90
|
+
# Handle proxy failure retry when auto_proxy is enabled
|
|
91
|
+
is_proxy_failure = should_auto_proxy && current_opts[:proxy] && proxy_failure?(status, stderr, response)
|
|
92
|
+
if is_proxy_failure
|
|
93
|
+
ProxyPool.mark_dead(current_opts[:proxy])
|
|
94
|
+
if proxy_retries_left > 0
|
|
95
|
+
proxy_retries_left -= 1
|
|
96
|
+
new_proxy = ProxyPool.get
|
|
97
|
+
log_debug("[HttpMimic::ProxyPool] Proxy #{current_opts[:proxy]} failed (exit #{status&.exitstatus}, code #{response&.code}, empty body: #{response&.body.to_s.empty?}). Retrying with new proxy #{new_proxy} (#{proxy_retries_left} retries left)...")
|
|
98
|
+
options[:proxy] = new_proxy
|
|
99
|
+
redo
|
|
100
|
+
else
|
|
101
|
+
log_debug("[HttpMimic::ProxyPool] Proxy retries exhausted (#{current_opts[:proxy]} failed). Aborting profile fallback.")
|
|
102
|
+
break
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if should_auto_proxy && current_opts[:proxy] && response.success? && !is_proxy_failure
|
|
110
107
|
ProxyPool.mark_alive(current_opts[:proxy])
|
|
111
108
|
end
|
|
112
109
|
|
|
@@ -127,14 +124,18 @@ module HttpMimic
|
|
|
127
124
|
final_stderr = stderr
|
|
128
125
|
final_command = full_command
|
|
129
126
|
|
|
130
|
-
# Track best response so a 200
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
best_response
|
|
136
|
-
|
|
137
|
-
|
|
127
|
+
# Track best response so a 200 isn't wiped out by a failed fallback.
|
|
128
|
+
# Only accept responses from successful curl executions that are not proxy failures or WAF challenges.
|
|
129
|
+
if response && (status.nil? || status.exitstatus == 0) && !is_proxy_failure
|
|
130
|
+
is_challenge = Waf::Detector.challenge_page?(response)
|
|
131
|
+
if !is_challenge
|
|
132
|
+
if best_response.nil?
|
|
133
|
+
best_response = response
|
|
134
|
+
elsif response.success? && !best_response.success?
|
|
135
|
+
best_response = response
|
|
136
|
+
elsif response.code == 200 && best_response.code != 200
|
|
137
|
+
best_response = response
|
|
138
|
+
end
|
|
138
139
|
end
|
|
139
140
|
end
|
|
140
141
|
|
|
@@ -145,13 +146,13 @@ module HttpMimic
|
|
|
145
146
|
end
|
|
146
147
|
|
|
147
148
|
auto_solve_waf = options.fetch(:solve_waf, config.auto_solve_waf)
|
|
148
|
-
is_blocked = (status.exitstatus != 0) || retry_statuses.include?(response.code) || Waf::Detector.challenge_page?(response)
|
|
149
|
+
is_blocked = (status.exitstatus != 0) || is_proxy_failure || retry_statuses.include?(response.code) || Waf::Detector.challenge_page?(response)
|
|
149
150
|
|
|
150
|
-
# Only attempt WAF resolution
|
|
151
|
-
if is_blocked && auto_solve_waf &&
|
|
151
|
+
# Only attempt WAF resolution if origin server returned challenge (not a proxy error)
|
|
152
|
+
if is_blocked && !is_proxy_failure && auto_solve_waf && waf_solve_attempts < 2 && method.to_s.upcase == 'GET'
|
|
152
153
|
waf_type = Waf::Detector.detect(response)
|
|
153
154
|
if waf_type
|
|
154
|
-
|
|
155
|
+
waf_solve_attempts += 1
|
|
155
156
|
log_debug("[HttpMimic] Detected #{waf_type.to_s.capitalize} WAF challenge. Attempting to solve with QuickJS...")
|
|
156
157
|
solved_resp = Waf.solve(url, response, current_opts)
|
|
157
158
|
if solved_resp
|
|
@@ -160,7 +161,8 @@ module HttpMimic
|
|
|
160
161
|
if response.cookies && !response.cookies.empty?
|
|
161
162
|
options[:cookies] = (options[:cookies] || {}).merge(response.cookies.to_h)
|
|
162
163
|
end
|
|
163
|
-
|
|
164
|
+
is_challenge_res = Waf::Detector.challenge_page?(response)
|
|
165
|
+
if !is_challenge_res && (response.success? || (response.code == 200 && best_response&.code != 200))
|
|
164
166
|
best_response = response
|
|
165
167
|
end
|
|
166
168
|
end
|
|
@@ -175,17 +177,16 @@ module HttpMimic
|
|
|
175
177
|
end
|
|
176
178
|
|
|
177
179
|
# Preserve best response if the last attempt resulted in a regression (e.g. 403 / error after getting 200)
|
|
178
|
-
if best_response && (response.nil? || response.error? || (final_status && final_status.exitstatus != 0))
|
|
180
|
+
if best_response && !Waf::Detector.challenge_page?(best_response) && (response.nil? || response.error? || (final_status && final_status.exitstatus != 0) || Waf::Detector.challenge_page?(response))
|
|
179
181
|
if best_response.success? || (best_response.code == 200 && response&.code != 200)
|
|
180
182
|
response = best_response
|
|
181
183
|
end
|
|
182
184
|
end
|
|
183
185
|
|
|
184
|
-
# Automatic SPA Detection & Obscura rendering fallback
|
|
186
|
+
# Automatic SPA Detection & Obscura rendering fallback (strictly requires auto_render_spa: true)
|
|
185
187
|
auto_render_spa = options.fetch(:auto_render_spa, config.auto_render_spa)
|
|
186
|
-
auto_solve_waf = options.fetch(:solve_waf, config.auto_solve_waf)
|
|
187
188
|
should_render_spa = auto_render_spa && response && response.success? && SpaDetector.spa?(response)
|
|
188
|
-
should_render_cpt =
|
|
189
|
+
should_render_cpt = auto_render_spa && response && Waf::Detector.challenge_page?(response)
|
|
189
190
|
|
|
190
191
|
if (should_render_spa || should_render_cpt) && method.to_s.upcase == 'GET'
|
|
191
192
|
target_reason = should_render_cpt ? 'WAF challenge page' : 'unhydrated SPA shell'
|
|
@@ -231,13 +232,34 @@ module HttpMimic
|
|
|
231
232
|
end
|
|
232
233
|
end
|
|
233
234
|
|
|
235
|
+
# If the final response was an intercepted dummy 200 from a broken/intercepting proxy, override code
|
|
236
|
+
if response && should_auto_proxy && proxy_failure?(final_status, final_stderr, response)
|
|
237
|
+
if response.code == 200 && response.body.to_s.empty?
|
|
238
|
+
response.instance_variable_set(:@code, 0)
|
|
239
|
+
response.instance_variable_set(:@status_message, 'Proxy Interception Failure (Empty Body)')
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# If the final response is an un-bypassed WAF challenge page, annotate status message so it's clear
|
|
244
|
+
if response && response.challenge_page? && (response.status_message.to_s.empty? || response.status_message == 'OK')
|
|
245
|
+
type_str = response.challenge_type ? response.challenge_type.to_s.capitalize : 'WAF'
|
|
246
|
+
response.instance_variable_set(:@status_message, "Challenge Required (#{type_str})")
|
|
247
|
+
end
|
|
248
|
+
|
|
234
249
|
handle_errors(final_status, final_stderr, final_command, response)
|
|
235
250
|
response
|
|
236
251
|
end
|
|
237
252
|
|
|
238
253
|
private
|
|
239
254
|
|
|
240
|
-
def determine_profiles(mode, auto_fallback)
|
|
255
|
+
def determine_profiles(mode, auto_fallback, explicit_profile = nil)
|
|
256
|
+
if explicit_profile
|
|
257
|
+
p_sym = explicit_profile.to_sym
|
|
258
|
+
return [p_sym] unless auto_fallback
|
|
259
|
+
defaults = [:impersonate, :android, :ios, :firefox, :safari, :curl]
|
|
260
|
+
return ([p_sym] + defaults).uniq
|
|
261
|
+
end
|
|
262
|
+
|
|
241
263
|
case mode
|
|
242
264
|
when :curl, :curl_first
|
|
243
265
|
auto_fallback ? [:curl, :impersonate, :android, :ios, :firefox] : [:curl]
|
|
@@ -270,6 +292,37 @@ module HttpMimic
|
|
|
270
292
|
end
|
|
271
293
|
end
|
|
272
294
|
|
|
295
|
+
def proxy_failure?(status, stderr, response)
|
|
296
|
+
# 1. Non-zero exit status from curl when using proxy (connection failed, SSL error, empty reply, etc.)
|
|
297
|
+
return true if status.nil? || status.exitstatus != 0
|
|
298
|
+
|
|
299
|
+
# 2. Origin server did not return a valid response (e.g. only CONNECT tunnel block or code 0)
|
|
300
|
+
return true if response.nil? || response.code == 0
|
|
301
|
+
|
|
302
|
+
# 3. Known proxy-level error codes
|
|
303
|
+
return true if [407, 502, 503, 504].include?(response.code)
|
|
304
|
+
|
|
305
|
+
# 4. Proxy fake 200 OK interception detection:
|
|
306
|
+
# A GET request that returns 200 with empty body is almost always a proxy interception / dummy response
|
|
307
|
+
if method.to_s.upcase == 'GET' && response.code == 200 && response.body.to_s.empty?
|
|
308
|
+
server_hdr = response.headers['server'].to_s.downcase
|
|
309
|
+
via_hdr = response.headers['via'].to_s.downcase
|
|
310
|
+
content_len = response.headers['content-length']
|
|
311
|
+
|
|
312
|
+
is_proxy_marker = server_hdr.include?('proxy') ||
|
|
313
|
+
server_hdr.include?('squid') ||
|
|
314
|
+
server_hdr.include?('console') ||
|
|
315
|
+
server_hdr.include?('privoxy') ||
|
|
316
|
+
server_hdr.include?('tinyproxy') ||
|
|
317
|
+
via_hdr.include?('proxy') ||
|
|
318
|
+
content_len == '0'
|
|
319
|
+
|
|
320
|
+
return true if is_proxy_marker
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
false
|
|
324
|
+
end
|
|
325
|
+
|
|
273
326
|
def execute_open3(command_array, stdin_data)
|
|
274
327
|
if stdin_data
|
|
275
328
|
Open3.capture3(*command_array, stdin_data: stdin_data)
|
data/lib/http_mimic/response.rb
CHANGED
|
@@ -46,8 +46,26 @@ module HttpMimic
|
|
|
46
46
|
!!@fallback_triggered
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def challenge_page?
|
|
50
|
+
if defined?(HttpMimic::Waf::Detector)
|
|
51
|
+
@challenge_page ||= HttpMimic::Waf::Detector.challenge_page?(self)
|
|
52
|
+
else
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
alias challenge? challenge_page?
|
|
57
|
+
alias blocked? challenge_page?
|
|
58
|
+
|
|
59
|
+
def challenge_type
|
|
60
|
+
if defined?(HttpMimic::Waf::Detector)
|
|
61
|
+
@challenge_type ||= HttpMimic::Waf::Detector.detect(self)
|
|
62
|
+
else
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
49
67
|
def success?
|
|
50
|
-
code >= 200 && code < 300
|
|
68
|
+
exit_code == 0 && (code >= 200 && code < 300) && !challenge_page?
|
|
51
69
|
end
|
|
52
70
|
alias ok? success?
|
|
53
71
|
|
|
@@ -64,7 +82,7 @@ module HttpMimic
|
|
|
64
82
|
end
|
|
65
83
|
|
|
66
84
|
def error?
|
|
67
|
-
client_error? || server_error?
|
|
85
|
+
exit_code != 0 || client_error? || server_error? || challenge_page?
|
|
68
86
|
end
|
|
69
87
|
|
|
70
88
|
def binary?
|
|
@@ -159,7 +177,8 @@ module HttpMimic
|
|
|
159
177
|
end
|
|
160
178
|
|
|
161
179
|
def inspect
|
|
162
|
-
"
|
|
180
|
+
challenge_info = challenge_page? ? " @challenge=true (#{challenge_type || 'WAF'})" : ""
|
|
181
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} @code=#{code}#{challenge_info} @status_message=#{status_message.inspect} @headers=#{headers.to_h.inspect} @parsed_response=#{parsed_response.inspect}>"
|
|
163
182
|
end
|
|
164
183
|
|
|
165
184
|
private
|
|
@@ -6,6 +6,7 @@ module HttpMimic
|
|
|
6
6
|
class ResponseParser
|
|
7
7
|
HTTP_STATUS_LINE_REGEX = /\AHTTP\/(?<version>[\d\.]+)\s+(?<code>\d{3})(?:\s+(?<message>.*))?/i
|
|
8
8
|
HTTP_STATUS_LINE_B = /\AHTTP\/(?:[\d\.]+)\s+\d{3}/i
|
|
9
|
+
PROXY_CONNECT_REGEX = /\AHTTP\/[\d\.]+\s+200\s+Connection\s+established/i
|
|
9
10
|
BINARY_MIME_KEYWORDS = %w[image/ audio/ video/ pdf octet-stream zip gzip tar compressed stream font wasm].freeze
|
|
10
11
|
|
|
11
12
|
attr_reader :raw_output, :exit_status, :stderr, :command, :request_url
|
|
@@ -22,6 +23,11 @@ module HttpMimic
|
|
|
22
23
|
# Split header blocks and body in binary-safe manner
|
|
23
24
|
header_blocks, raw_body = split_headers_and_body(@raw_output)
|
|
24
25
|
|
|
26
|
+
# Strip proxy CONNECT tunnel handshake blocks (e.g. "HTTP/1.1 200 Connection established")
|
|
27
|
+
while header_blocks.first && header_blocks.first =~ PROXY_CONNECT_REGEX
|
|
28
|
+
header_blocks.shift
|
|
29
|
+
end
|
|
30
|
+
|
|
25
31
|
final_header_block = header_blocks.last || ''
|
|
26
32
|
history_header_blocks = header_blocks.size > 1 ? header_blocks[0...-1] : []
|
|
27
33
|
|
|
@@ -25,7 +25,7 @@ module HttpMimic
|
|
|
25
25
|
# @param response [HttpMimic::Response]
|
|
26
26
|
# @return [Boolean]
|
|
27
27
|
def spa?(response)
|
|
28
|
-
return false unless response && response.
|
|
28
|
+
return false unless response && (response.code >= 200 && response.code < 300)
|
|
29
29
|
return false if response.respond_to?(:binary?) && response.binary?
|
|
30
30
|
|
|
31
31
|
content_type = response.headers['content-type'].to_s.downcase
|