http_mimic 0.3.0 → 0.3.1
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 +9 -0
- data/README.md +40 -4
- data/http_mimic.gemspec +0 -1
- data/lib/http_mimic/command_builder.rb +16 -0
- data/lib/http_mimic/configuration.rb +10 -0
- data/lib/http_mimic/module_methods.rb +15 -0
- data/lib/http_mimic/request.rb +78 -22
- data/lib/http_mimic/response.rb +25 -13
- data/lib/http_mimic/version.rb +1 -1
- metadata +1 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b09482a3e618dd6390934e85d83de762cffaa062d5610d606d77bdabfb0ff11
|
|
4
|
+
data.tar.gz: 55c4dfeee202c24898a1ad5a12bc5a58df9c5a1e0481b7d08be0fc0c4a909e32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b4a75f41696e4b6a89c73096640ce5f9b3963dff9262aea51ead8271022868397e7c413edfc724cd7c0cbb439a2663bf55f5ea28cd060e491cff0445e88fea7
|
|
7
|
+
data.tar.gz: cf2f82e990474a02e00aebe5d5b0b0ca019a994266279cd84f7fd4dd966e9260b488e1915f2b7eccdf7452e50a838489d4709181ec195bebbc773e6aff62d6d8
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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.3.1] - 2026-08-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Smart Adaptive Multi-Strategy Modes (`:auto`, `:impersonate_first`, `:curl_first`, `:impersonate_only`, `:curl_only`)**:
|
|
12
|
+
- **Zero-Configuration Protection Bypass (`:auto` mode, enabled by default)**: Seamlessly tries `curl-impersonate` first, and if blocked by WAFs with `403`/`429`/`503` (e.g. Akamai bot challenges requiring JS), automatically falls back to standard `curl` with server client headers to reliably retrieve 200 OK.
|
|
13
|
+
- Per-request and class-level configurable execution mode (`mode:`, `auto_fallback:`, `retry_statuses:`).
|
|
14
|
+
- Enhanced `Response` object with `response.mode_used`, `response.fallback_triggered?`, and `response.attempts` metadata.
|
|
15
|
+
- Added live integration test for Akamai-protected site bypass (`test_smart_auto_fallback_on_akamai_protected_site`).
|
|
16
|
+
|
|
8
17
|
## [0.3.0] - 2026-08-24
|
|
9
18
|
|
|
10
19
|
### Added
|
data/README.md
CHANGED
|
@@ -142,6 +142,7 @@ puts "User-Agent: #{res['user_agent']}"
|
|
|
142
142
|
```ruby
|
|
143
143
|
client = HttpMimic::Client.new(
|
|
144
144
|
base_uri: 'https://api.example.com',
|
|
145
|
+
mode: :auto,
|
|
145
146
|
impersonate: 'firefox135',
|
|
146
147
|
timeout: 15,
|
|
147
148
|
headers: {
|
|
@@ -158,12 +159,41 @@ response = client.post('/v1/users', json: { username: 'bob' })
|
|
|
158
159
|
|
|
159
160
|
---
|
|
160
161
|
|
|
162
|
+
## 🧠 Smart Adaptive Modes (Zero-Configuration Scraping)
|
|
163
|
+
|
|
164
|
+
`HttpMimic` provides built-in multi-strategy orchestration so you can fetch protected websites without worrying about which specific WAF (Cloudflare, Akamai, DataDome) protects them:
|
|
165
|
+
|
|
166
|
+
- **`:auto` (Default & Recommended)**: Tries `curl-impersonate` (Chrome 131) first. If blocked by WAFs like Akamai with `403`/`429`/`503` (which require JS telemetry for browsers but allow standard server clients), it **automatically and seamlessly retries with standard curl + server headers**, directly returning `200 OK`.
|
|
167
|
+
- **`:impersonate_first`**: Prefers `curl-impersonate` and automatically falls back to standard `curl` if blocked.
|
|
168
|
+
- **`:curl_first`**: Prefers standard `curl` and automatically upgrades to `curl-impersonate` if blocked.
|
|
169
|
+
- **`:impersonate_only`**: Strictly uses `curl-impersonate` (no retry/fallback).
|
|
170
|
+
- **`:curl_only`**: Strictly uses standard `curl` (no retry/fallback).
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# 1. No-Brain Auto Mode (Works automatically for both Cloudflare & Akamai):
|
|
174
|
+
response = HttpMimic.get('https://www.asics.com/us/en-us/gt-2000-15/p/ANA_1011C235-750.html')
|
|
175
|
+
puts response.code # => 200
|
|
176
|
+
puts response.mode_used # => :curl
|
|
177
|
+
puts response.fallback_triggered? # => true
|
|
178
|
+
|
|
179
|
+
# 2. Per-request mode override:
|
|
180
|
+
response = HttpMimic.get(url, mode: :curl_first)
|
|
181
|
+
response = HttpMimic.get(url, mode: :impersonate_only)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
161
186
|
## ⚙️ Global Configuration
|
|
162
187
|
|
|
163
188
|
Configure global defaults in an initializer (e.g., `config/initializers/http_mimic.rb`):
|
|
164
189
|
|
|
165
190
|
```ruby
|
|
166
191
|
HttpMimic.configure do |config|
|
|
192
|
+
# Multi-strategy & Smart Fallback
|
|
193
|
+
config.mode = :auto # :auto (default), :impersonate_first, :curl_first, :impersonate_only, :curl_only
|
|
194
|
+
config.auto_fallback = true # Automatically retry with alternative profile if blocked
|
|
195
|
+
config.retry_statuses = [403, 429, 503] # Status codes that trigger auto-fallback
|
|
196
|
+
|
|
167
197
|
# Browser simulation & request defaults
|
|
168
198
|
config.default_impersonate = 'chrome131' # Default browser target
|
|
169
199
|
config.default_timeout = 30 # Request timeout (seconds)
|
|
@@ -188,6 +218,9 @@ end
|
|
|
188
218
|
|
|
189
219
|
| Option | Type | Description |
|
|
190
220
|
| :--- | :--- | :--- |
|
|
221
|
+
| `:mode` | Symbol | Execution strategy: `:auto` (default), `:impersonate_first`, `:curl_first`, `:impersonate_only`, `:curl_only` |
|
|
222
|
+
| `:auto_fallback` | Boolean | Whether to automatically retry with alternative profile on blocked status (default `true`) |
|
|
223
|
+
| `:retry_statuses`| Array | Status codes that trigger auto-fallback (default `[403, 429, 503]`) |
|
|
191
224
|
| `:impersonate` | String | Target browser to mimic (e.g., `'chrome131'`, `'chrome120'`, `'firefox135'`, `'safari180'`, `'tor145'`) |
|
|
192
225
|
| `:binary` | String | Path to a custom `curl-impersonate` executable |
|
|
193
226
|
| `:query` / `:params` | Hash | URL query parameters (supports nested parameters and encoding) |
|
|
@@ -234,10 +267,13 @@ response.headers['content-type'] # => Case-insensitive header access
|
|
|
234
267
|
response.cookies['session_id'] # => Parsed Set-Cookie store
|
|
235
268
|
response.history # => Array of redirect history metadata
|
|
236
269
|
|
|
237
|
-
# Underlying execution details
|
|
238
|
-
response.
|
|
239
|
-
response.
|
|
240
|
-
response.
|
|
270
|
+
# Underlying execution & multi-strategy details
|
|
271
|
+
response.mode_used # => :impersonate or :curl
|
|
272
|
+
response.fallback_triggered? # => true if smart fallback was executed
|
|
273
|
+
response.attempts # => Array of execution metadata for each attempt
|
|
274
|
+
response.exit_code # => Process exit status (0 for success)
|
|
275
|
+
response.stderr # => Stderr output from curl
|
|
276
|
+
response.command # => Array of the exact CLI arguments executed
|
|
241
277
|
```
|
|
242
278
|
|
|
243
279
|
---
|
data/http_mimic.gemspec
CHANGED
|
@@ -14,7 +14,6 @@ Gem::Specification.new do |spec|
|
|
|
14
14
|
spec.license = "MIT"
|
|
15
15
|
|
|
16
16
|
spec.metadata = {
|
|
17
|
-
"homepage_uri" => "https://github.com/anxgang/http_mimic",
|
|
18
17
|
"source_code_uri" => "https://github.com/anxgang/http_mimic",
|
|
19
18
|
"bug_tracker_uri" => "https://github.com/anxgang/http_mimic/issues",
|
|
20
19
|
"changelog_uri" => "https://github.com/anxgang/http_mimic/blob/main/CHANGELOG.md"
|
|
@@ -171,6 +171,15 @@ module HttpMimic
|
|
|
171
171
|
args << '-A' << options[:user_agent].to_s
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
+
# Profile-specific headers & HTTP version defaults (for server/curl mode)
|
|
175
|
+
if options[:profile] == :curl
|
|
176
|
+
args << '--http1.1' unless (options[:curl_options] || []).to_s.include?('--http')
|
|
177
|
+
headers_to_send['User-Agent'] ||= (options[:user_agent] || 'Ruby')
|
|
178
|
+
headers_to_send['Accept'] ||= '*/*'
|
|
179
|
+
headers_to_send['Accept-Encoding'] ||= 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3'
|
|
180
|
+
headers_to_send['Connection'] ||= 'close'
|
|
181
|
+
end
|
|
182
|
+
|
|
174
183
|
# Append headers
|
|
175
184
|
headers_to_send.each do |k, v|
|
|
176
185
|
if v.is_a?(Array)
|
|
@@ -194,6 +203,13 @@ module HttpMimic
|
|
|
194
203
|
end
|
|
195
204
|
|
|
196
205
|
def resolve_binary
|
|
206
|
+
# 0. If profile is explicitly set to :curl, use system curl
|
|
207
|
+
if options[:profile] == :curl
|
|
208
|
+
curl_path = find_executable('curl') || 'curl'
|
|
209
|
+
return curl_path if executable?(curl_path)
|
|
210
|
+
raise BinaryNotFoundError, "System curl executable was not found."
|
|
211
|
+
end
|
|
212
|
+
|
|
197
213
|
# 1. Use explicit binary_path from options or config if provided
|
|
198
214
|
explicit = options[:binary] || config.binary_path
|
|
199
215
|
if explicit
|
|
@@ -13,6 +13,11 @@ module HttpMimic
|
|
|
13
13
|
attr_accessor :logger
|
|
14
14
|
attr_accessor :debug
|
|
15
15
|
|
|
16
|
+
# Multi-strategy and smart auto-fallback settings
|
|
17
|
+
attr_accessor :mode
|
|
18
|
+
attr_accessor :auto_fallback
|
|
19
|
+
attr_accessor :retry_statuses
|
|
20
|
+
|
|
16
21
|
# Webdrivers-like auto download and driver management settings
|
|
17
22
|
attr_accessor :auto_download
|
|
18
23
|
attr_accessor :driver_version
|
|
@@ -31,6 +36,11 @@ module HttpMimic
|
|
|
31
36
|
@logger = nil
|
|
32
37
|
@debug = false
|
|
33
38
|
|
|
39
|
+
# Multi-strategy defaults: :auto seamlessly falls back between impersonate and curl
|
|
40
|
+
@mode = :auto
|
|
41
|
+
@auto_fallback = true
|
|
42
|
+
@retry_statuses = [403, 429, 503]
|
|
43
|
+
|
|
34
44
|
# Enable automatic downloading of curl-impersonate driver (lexiforest) by default
|
|
35
45
|
@auto_download = true
|
|
36
46
|
@driver_version = 'v2.1.1'
|
|
@@ -41,6 +41,21 @@ module HttpMimic
|
|
|
41
41
|
default_options[:cookies].merge!(c)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
def mode(m = nil)
|
|
45
|
+
return default_options[:mode] if m.nil?
|
|
46
|
+
default_options[:mode] = m
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def auto_fallback(enabled = nil)
|
|
50
|
+
return default_options[:auto_fallback] if enabled.nil?
|
|
51
|
+
default_options[:auto_fallback] = enabled
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def retry_statuses(statuses = nil)
|
|
55
|
+
return default_options[:retry_statuses] if statuses.nil?
|
|
56
|
+
default_options[:retry_statuses] = statuses
|
|
57
|
+
end
|
|
58
|
+
|
|
44
59
|
def default_options
|
|
45
60
|
@default_options ||= {}
|
|
46
61
|
end
|
data/lib/http_mimic/request.rb
CHANGED
|
@@ -14,34 +14,89 @@ module HttpMimic
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def perform
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
17
|
+
mode = (options[:mode] || config.mode || :auto).to_sym
|
|
18
|
+
auto_fallback = options.fetch(:auto_fallback, config.auto_fallback)
|
|
19
|
+
retry_statuses = options[:retry_statuses] || config.retry_statuses || [403, 429, 503]
|
|
20
|
+
|
|
21
|
+
attempts = []
|
|
22
|
+
profiles_to_try = determine_profiles(mode, auto_fallback)
|
|
23
|
+
|
|
24
|
+
response = nil
|
|
25
|
+
final_status = nil
|
|
26
|
+
final_stderr = nil
|
|
27
|
+
final_command = nil
|
|
28
|
+
|
|
29
|
+
profiles_to_try.each_with_index do |profile, index|
|
|
30
|
+
current_opts = options.merge(profile: profile)
|
|
31
|
+
|
|
32
|
+
builder = CommandBuilder.new(method, url, current_opts, config)
|
|
33
|
+
binary, args, stdin_data, final_url = builder.build
|
|
34
|
+
full_command = [binary] + args
|
|
35
|
+
|
|
36
|
+
log_debug("Executing HttpMimic command (attempt #{index + 1}, profile: #{profile}): #{full_command.join(' ')}")
|
|
37
|
+
log_debug("Stdin data: #{stdin_data}") if stdin_data
|
|
38
|
+
|
|
39
|
+
stdout, stderr, status = execute_open3(full_command, stdin_data)
|
|
40
|
+
|
|
41
|
+
log_debug("Curl exit status: #{status.exitstatus}")
|
|
42
|
+
log_debug("Curl stderr: #{stderr}") unless stderr.empty?
|
|
43
|
+
|
|
44
|
+
response = ResponseParser.new(
|
|
45
|
+
stdout,
|
|
46
|
+
exit_status: status,
|
|
47
|
+
stderr: stderr,
|
|
48
|
+
command: full_command,
|
|
49
|
+
request_url: final_url
|
|
50
|
+
).parse
|
|
51
|
+
|
|
52
|
+
response.mode_used = profile
|
|
53
|
+
response.fallback_triggered = (index > 0)
|
|
54
|
+
|
|
55
|
+
attempts << {
|
|
56
|
+
attempt: index + 1,
|
|
57
|
+
profile: profile,
|
|
58
|
+
command: full_command,
|
|
59
|
+
code: response.code,
|
|
60
|
+
exit_code: status.exitstatus,
|
|
61
|
+
success: response.success?
|
|
62
|
+
}
|
|
63
|
+
response.attempts = attempts
|
|
64
|
+
|
|
65
|
+
final_status = status
|
|
66
|
+
final_stderr = stderr
|
|
67
|
+
final_command = full_command
|
|
68
|
+
|
|
69
|
+
is_blocked = (status.exitstatus != 0) || retry_statuses.include?(response.code)
|
|
70
|
+
if !is_blocked || (index == profiles_to_try.size - 1)
|
|
71
|
+
break
|
|
72
|
+
end
|
|
37
73
|
|
|
38
|
-
|
|
74
|
+
log_debug("[HttpMimic] Attempt #{index + 1} with #{profile} resulted in status #{response.code}. Triggering smart fallback to next profile...")
|
|
75
|
+
end
|
|
39
76
|
|
|
77
|
+
handle_errors(final_status, final_stderr, final_command, response)
|
|
40
78
|
response
|
|
41
79
|
end
|
|
42
80
|
|
|
43
81
|
private
|
|
44
82
|
|
|
83
|
+
def determine_profiles(mode, auto_fallback)
|
|
84
|
+
case mode
|
|
85
|
+
when :curl, :curl_first
|
|
86
|
+
auto_fallback ? [:curl, :impersonate] : [:curl]
|
|
87
|
+
when :curl_only
|
|
88
|
+
[:curl]
|
|
89
|
+
when :impersonate_only
|
|
90
|
+
[:impersonate]
|
|
91
|
+
when :impersonate_first
|
|
92
|
+
auto_fallback ? [:impersonate, :curl] : [:impersonate]
|
|
93
|
+
when :auto, :smart
|
|
94
|
+
auto_fallback ? [:impersonate, :curl] : [:impersonate]
|
|
95
|
+
else
|
|
96
|
+
auto_fallback ? [:impersonate, :curl] : [:impersonate]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
45
100
|
def execute_open3(command_array, stdin_data)
|
|
46
101
|
if stdin_data
|
|
47
102
|
Open3.capture3(*command_array, stdin_data: stdin_data)
|
|
@@ -54,6 +109,7 @@ module HttpMimic
|
|
|
54
109
|
|
|
55
110
|
def handle_errors(status, stderr, command, response)
|
|
56
111
|
raise_error = options.fetch(:raise_on_error, config.raise_on_error)
|
|
112
|
+
return unless status
|
|
57
113
|
exit_code = status.exitstatus
|
|
58
114
|
|
|
59
115
|
if exit_code != 0
|
|
@@ -72,7 +128,7 @@ module HttpMimic
|
|
|
72
128
|
if raise_error
|
|
73
129
|
raise error_class.new(message, exit_code: exit_code, stderr: stderr, command: command, response: response)
|
|
74
130
|
end
|
|
75
|
-
elsif raise_error && response
|
|
131
|
+
elsif raise_error && response&.error?
|
|
76
132
|
raise CommandError.new("HTTP request failed (status #{response.code})", exit_code: exit_code, stderr: stderr, command: command, response: response)
|
|
77
133
|
end
|
|
78
134
|
end
|
data/lib/http_mimic/response.rb
CHANGED
|
@@ -16,22 +16,34 @@ module HttpMimic
|
|
|
16
16
|
:exit_code,
|
|
17
17
|
:command
|
|
18
18
|
|
|
19
|
+
attr_accessor :mode_used,
|
|
20
|
+
:fallback_triggered,
|
|
21
|
+
:attempts
|
|
22
|
+
|
|
19
23
|
alias status code
|
|
24
|
+
alias strategy_used mode_used
|
|
20
25
|
|
|
21
26
|
def initialize(attributes = {})
|
|
22
|
-
@code
|
|
23
|
-
@http_version
|
|
24
|
-
@status_message
|
|
25
|
-
@headers
|
|
26
|
-
@cookies
|
|
27
|
-
@body
|
|
28
|
-
@parsed_response
|
|
29
|
-
@raw_headers
|
|
30
|
-
@history
|
|
31
|
-
@request_url
|
|
32
|
-
@stderr
|
|
33
|
-
@exit_code
|
|
34
|
-
@command
|
|
27
|
+
@code = attributes[:code] || 0
|
|
28
|
+
@http_version = attributes[:http_version]
|
|
29
|
+
@status_message = attributes[:status_message]
|
|
30
|
+
@headers = attributes[:headers] || Headers.new
|
|
31
|
+
@cookies = attributes[:cookies] || Cookies.new
|
|
32
|
+
@body = attributes[:body] || ''
|
|
33
|
+
@parsed_response = attributes[:parsed_response]
|
|
34
|
+
@raw_headers = attributes[:raw_headers] || ''
|
|
35
|
+
@history = attributes[:history] || []
|
|
36
|
+
@request_url = attributes[:request_url]
|
|
37
|
+
@stderr = attributes[:stderr] || ''
|
|
38
|
+
@exit_code = attributes[:exit_code] || 0
|
|
39
|
+
@command = attributes[:command]
|
|
40
|
+
@mode_used = attributes[:mode_used] || :impersonate
|
|
41
|
+
@fallback_triggered = attributes[:fallback_triggered] || false
|
|
42
|
+
@attempts = attributes[:attempts] || []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def fallback_triggered?
|
|
46
|
+
!!@fallback_triggered
|
|
35
47
|
end
|
|
36
48
|
|
|
37
49
|
def success?
|
data/lib/http_mimic/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: http_mimic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- anxgang
|
|
@@ -78,7 +78,6 @@ homepage: https://github.com/anxgang/http_mimic
|
|
|
78
78
|
licenses:
|
|
79
79
|
- MIT
|
|
80
80
|
metadata:
|
|
81
|
-
homepage_uri: https://github.com/anxgang/http_mimic
|
|
82
81
|
source_code_uri: https://github.com/anxgang/http_mimic
|
|
83
82
|
bug_tracker_uri: https://github.com/anxgang/http_mimic/issues
|
|
84
83
|
changelog_uri: https://github.com/anxgang/http_mimic/blob/main/CHANGELOG.md
|