http_mimic 0.3.1 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b09482a3e618dd6390934e85d83de762cffaa062d5610d606d77bdabfb0ff11
4
- data.tar.gz: 55c4dfeee202c24898a1ad5a12bc5a58df9c5a1e0481b7d08be0fc0c4a909e32
3
+ metadata.gz: 9b0a9fff1b74c98ea24c294a491b00ba57e270d24e6c0bb7a669e6d2db264d8a
4
+ data.tar.gz: e11d3ae615b92abcfedda8547120290d82020acd82230353a127cc97dfc844f6
5
5
  SHA512:
6
- metadata.gz: 1b4a75f41696e4b6a89c73096640ce5f9b3963dff9262aea51ead8271022868397e7c413edfc724cd7c0cbb439a2663bf55f5ea28cd060e491cff0445e88fea7
7
- data.tar.gz: cf2f82e990474a02e00aebe5d5b0b0ca019a994266279cd84f7fd4dd966e9260b488e1915f2b7eccdf7452e50a838489d4709181ec195bebbc773e6aff62d6d8
6
+ metadata.gz: 5e0a93bb67bb84582e24df07ea4d51d291a53f0b48ab77ad0bd4a63b666bede4b493bbb405ad34d76602e086eafd76a455668c2a491488d83dd2c0be1060fe66
7
+ data.tar.gz: c616cafefeb23d78af7320beab5ddc3b8f79aafda440fd1fac889f5a5990417b31716105e8c74cab566e6c0ed6eca04a82a397ddc4242eb59caed519e52739de
data/CHANGELOG.md CHANGED
@@ -5,6 +5,35 @@ 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.4.0] - 2026-08-30
9
+
10
+ ### Added
11
+ - **Target-Specific Navigation Headers & Client Hints Simulation**:
12
+ - Automatically generates browser-accurate Navigation Headers (`sec-fetch-dest`, `sec-fetch-mode`, `sec-fetch-site`, `upgrade-insecure-requests`) and Client Hints (`sec-ch-ua`, `sec-ch-ua-mobile`, `sec-ch-ua-platform`) tailored to the selected impersonation target.
13
+ - Firefox (`firefox*`, `ff*`), Safari / iOS (`safari*`, `ios*`), and Tor targets strictly omit Chromium-only `sec-ch-ua*` headers to avoid anti-bot fingerprint anomalies and detection.
14
+ - Android Chrome targets properly send `sec-ch-ua-mobile: ?1` and `sec-ch-ua-platform: "Android"`.
15
+ - Edge targets properly send `"Microsoft Edge"` brand headers with `"Windows"` platform.
16
+ - **Unified Persistent CookieStore (`persist_cookies`)**:
17
+ - Added `:persist_cookies` request option and `persist_cookies` class-level DSL method to automatically load and persist session cookies across requests per host into `~/.http_mimic/cookies/`.
18
+ - Added helper methods: `HttpMimic.load_cookies(host, max_age: nil)`, `HttpMimic.save_cookies(host, cookies, ttl: nil)`, and `HttpMimic.clear_cookies!(host = nil)`.
19
+ - **WAF Challenge Solving & Fallback Performance Optimizations**:
20
+ - Added single-pass WAF solver guard (`waf_solve_attempted`) to prevent redundant JavaScript solver executions across fallback attempts.
21
+ - Added cross-attempt cookie accumulation to preserve session state between fallback profiles.
22
+ - Added `HttpMimic::Waf::Detector.challenge_page?` to detect interstitial challenges disguised under HTTP 200 responses.
23
+
24
+ ## [0.3.2] - 2026-08-29
25
+
26
+ ### Added
27
+ - **Mobile Impersonation & Multi-Stage Auto-Fallback**:
28
+ - Added support for mobile browser targets: Android Chrome (`chrome131_android`, `chrome99_android`, alias `android`) and iOS Safari (`safari260_ios`, `safari180_ios`, `safari184_ios`, `safari172_ios`, alias `ios`, `mobile`).
29
+ - Enhanced `:auto` adaptive mode with automatic fallback cascade `[:impersonate, :android, :ios, :curl]` to bypass modern anti-bot systems (e.g. Akamai Bot Manager on Adidas HK) without requiring JavaScript telemetry solving.
30
+ - Added execution modes: `:mobile_first`, `:android_first`, `:ios_first`, `:mobile_only`, `:android_only`, `:ios_only`.
31
+ - **Binary-Safe Response Parsing & File Saving**:
32
+ - Fixed binary handling in `ResponseParser#split_headers_and_body` to prevent `ArgumentError: invalid byte sequence in UTF-8` on images, PDFs, compressed files, and binary streams.
33
+ - Added `response.binary?` and `response.save_to_file(path)` / `response.save(path)` helpers.
34
+ - **Built-in HTML / Scraping Helpers**:
35
+ - Added `response.title`, `response.og_image`, `response.meta_description`, and `response.extract_images(base_url: nil)`.
36
+
8
37
  ## [0.3.1] - 2026-08-25
9
38
 
10
39
  ### Added
data/README.md CHANGED
@@ -21,7 +21,7 @@ It also includes **Webdrivers-like automatic driver management**, automatically
21
21
  - Edge & Tor support: `edge101`, `edge99`, `tor145`.
22
22
  - **HTTParty-Style API**:
23
23
  - Direct module methods: `HttpMimic.get`, `HttpMimic.post`, etc.
24
- - Class mixin via `include HttpMimic` (`base_uri`, `headers`, `default_params`, `default_timeout`, `impersonate`, `proxy`, `cookies`).
24
+ - Class mixin via `include HttpMimic` (`base_uri`, `headers`, `default_params`, `default_timeout`, `impersonate`, `proxy`, `cookies`, `persist_cookies`).
25
25
  - Reusable instance client: `HttpMimic::Client.new(...)`.
26
26
  - **Zero Shell Injection Risk**:
27
27
  - Executes commands with array arguments via `Open3.capture3(*cmd_array)`.
@@ -161,28 +161,65 @@ response = client.post('/v1/users', json: { username: 'bob' })
161
161
 
162
162
  ## 🧠 Smart Adaptive Modes (Zero-Configuration Scraping)
163
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:
164
+ `HttpMimic` provides built-in multi-strategy orchestration so you can fetch protected websites without worrying about which specific WAF (Akamai Botman, Cloudflare Turnstile, DataDome, Kasada) protects them:
165
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).
166
+ - **`:auto` (Default & Recommended)**: Tries desktop `curl-impersonate` (`chrome131`) first. If blocked by WAFs like Akamai with `403`/`429`/`503` (which require JS telemetry for desktop browsers), it **automatically cascades through mobile profiles (`android`, `ios`) and server `curl` headers**, reliably retrieving `200 OK`.
167
+ - **`:mobile_first` / `:android_first` / `:ios_first`**: Prefers mobile TLS/HTTP2 fingerprints and falls back to desktop or curl if blocked.
168
+ - **`:impersonate_first`**: Prefers desktop `curl-impersonate` and automatically falls back to mobile and curl if blocked.
169
+ - **`:curl_first`**: Prefers standard server `curl` and automatically upgrades to impersonate profiles if blocked.
170
+ - **`:impersonate_only` / `:mobile_only` / `:curl_only`**: Strictly uses the chosen profile (no fallback).
171
171
 
172
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')
173
+ # 1. No-Brain Auto Mode (Works automatically for Cloudflare, Akamai, etc.):
174
+ response = HttpMimic.get('https://www.adidas.com.hk/en/KI8139.html')
175
175
  puts response.code # => 200
176
- puts response.mode_used # => :curl
176
+ puts response.mode_used # => :android
177
177
  puts response.fallback_triggered? # => true
178
178
 
179
- # 2. Per-request mode override:
180
- response = HttpMimic.get(url, mode: :curl_first)
181
- response = HttpMimic.get(url, mode: :impersonate_only)
179
+ # 2. Extract images and metadata with built-in helpers:
180
+ puts response.title # => "SAMBA OG SHOES - Brown | adidas Hong Kong"
181
+ images = response.extract_images # => ["https://assets.adidas.com/images/.../KI8139_01_00_standard.jpg", ...]
182
+
183
+ # 3. Fetch and save binary images directly:
184
+ HttpMimic.get(images.first).save('samba_og.jpg')
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 🍪 Persistent Host CookieStore
190
+
191
+ `HttpMimic` supports persistent host-based cookie caching. Once enabled, session and verification cookies are automatically saved to `~/.http_mimic/cookies/<host>.json` and reused in subsequent requests to the same host:
192
+
193
+ ```ruby
194
+ # 1. Enable per-request
195
+ response = HttpMimic.get('https://example.com/items', persist_cookies: true)
196
+
197
+ # 2. Or enable in class mixin
198
+ class Scraper
199
+ include HttpMimic
200
+ persist_cookies true
201
+ end
202
+
203
+ # 3. Manual CookieStore helpers
204
+ HttpMimic.load_cookies('example.com') # => Hash of active cookies
205
+ HttpMimic.save_cookies('example.com', { session: '123' }) # Save cookies with default TTL
206
+ HttpMimic.clear_cookies!('example.com') # Clear host or all cookies
182
207
  ```
183
208
 
184
209
  ---
185
210
 
211
+ ## 🧭 Target-Specific Navigation Headers & Client Hints
212
+
213
+ `HttpMimic` automatically constructs browser-accurate HTTP navigation headers on `GET` and `HEAD` requests:
214
+
215
+ - **Chrome Desktop / Mobile**: Generates `sec-ch-ua`, `sec-ch-ua-mobile`, `sec-ch-ua-platform`, and standard Chrome `Accept` headers.
216
+ - **Firefox & Safari / iOS**: **Strictly omits** Chromium-only `sec-ch-ua*` headers to prevent anti-bot detection and fingerprint mismatch anomalies.
217
+ - **Edge**: Generates Microsoft Edge brand `sec-ch-ua` headers.
218
+
219
+ Can be disabled globally with `config.navigation_headers = false` or per request via `navigation_headers: false`.
220
+
221
+ ---
222
+
186
223
  ## ⚙️ Global Configuration
187
224
 
188
225
  Configure global defaults in an initializer (e.g., `config/initializers/http_mimic.rb`):
@@ -204,6 +241,16 @@ HttpMimic.configure do |config|
204
241
  config.raise_on_error = false # Raise exceptions on HTTP errors / non-zero exits
205
242
  config.debug = false # Print debug logs
206
243
 
244
+ # Navigation headers & Client Hints simulation (enabled by default)
245
+ config.navigation_headers = true # Browser-accurate sec-ch-ua, sec-fetch-*, Accept headers
246
+
247
+ # Host CookieStore Persistence (disabled by default)
248
+ config.persist_cookies = false # Auto-persist cookies per host
249
+ config.cookie_store_dir = File.expand_path('~/.http_mimic/cookies') # Directory for cookie store
250
+
251
+ # WAF challenge solving (enabled by default)
252
+ config.auto_solve_waf = true # Automatically solve detected WAF JS challenges
253
+
207
254
  # Webdrivers-like auto-download settings (enabled by default)
208
255
  config.auto_download = true # Auto-download missing binary
209
256
  config.driver_version = 'v2.1.1' # Target release version
@@ -222,12 +269,15 @@ end
222
269
  | `:auto_fallback` | Boolean | Whether to automatically retry with alternative profile on blocked status (default `true`) |
223
270
  | `:retry_statuses`| Array | Status codes that trigger auto-fallback (default `[403, 429, 503]`) |
224
271
  | `:impersonate` | String | Target browser to mimic (e.g., `'chrome131'`, `'chrome120'`, `'firefox135'`, `'safari180'`, `'tor145'`) |
272
+ | `:navigation_headers` | Boolean | Automatically generate browser-accurate navigation headers (`sec-ch-ua`, `sec-fetch-*`, `Accept`) (default `true`) |
273
+ | `:solve_waf` | Boolean | Automatically detect and solve WAF challenges (e.g. Akamai) via embedded QuickJS (default `true`) |
225
274
  | `:binary` | String | Path to a custom `curl-impersonate` executable |
226
275
  | `:query` / `:params` | Hash | URL query parameters (supports nested parameters and encoding) |
227
276
  | `:headers` | Hash | Custom HTTP request headers |
228
277
  | `:json` | Hash / Array | Serialized to JSON with `Content-Type: application/json` |
229
278
  | `:body` | Hash / String | Form payload (Hash) or raw request body string |
230
279
  | `:cookies` | Hash / String | Request cookies |
280
+ | `:persist_cookies` | Boolean | Automatically load and save cookies for the host across requests |
231
281
  | `:cookie_jar` | String | Path to save cookies (`-c`) |
232
282
  | `:cookie_file` | String | Path to read cookies (`-b`) |
233
283
  | `:timeout` | Integer / Float | Maximum execution timeout in seconds (`--max-time`) |
@@ -257,18 +307,26 @@ response.redirect? # => false (3xx)
257
307
  response.client_error? # => false (4xx)
258
308
  response.server_error? # => false (5xx)
259
309
 
260
- # Response body
261
- response.body # => Raw Body (String)
310
+ # Response body & File operations
311
+ response.body # => Raw Body (String / binary bytes)
312
+ response.binary? # => true if content is an image, pdf, or binary stream
313
+ response.save('image.jpg') # => Directly saves response body to file
262
314
  response.parsed_response# => Auto-parsed JSON Hash / Array
263
315
  response['key'] # => Direct key access to parsed_response
264
316
 
317
+ # HTML & Scraping Helpers
318
+ response.title # => HTML page title
319
+ response.og_image # => OpenGraph image URL
320
+ response.meta_description # => Meta description
321
+ response.extract_images # => Array of all resolved image URLs in HTML
322
+
265
323
  # Headers & Cookies
266
324
  response.headers['content-type'] # => Case-insensitive header access
267
325
  response.cookies['session_id'] # => Parsed Set-Cookie store
268
326
  response.history # => Array of redirect history metadata
269
327
 
270
328
  # Underlying execution & multi-strategy details
271
- response.mode_used # => :impersonate or :curl
329
+ response.mode_used # => :impersonate, :android, :ios, or :curl
272
330
  response.fallback_triggered? # => true if smart fallback was executed
273
331
  response.attempts # => Array of execution metadata for each attempt
274
332
  response.exit_code # => Process exit status (0 for success)
@@ -14,40 +14,70 @@ module HttpMimic
14
14
  ].freeze
15
15
 
16
16
  TARGET_BINARY_MAP = {
17
- 'chrome' => %w[curl_chrome131 curl_chrome124 curl_chrome120 curl_chrome116 curl_chrome110 curl-impersonate-chrome curl_chrome curl-impersonate],
18
- 'chrome116' => %w[curl_chrome116 curl-impersonate-chrome curl_chrome curl-impersonate],
19
- 'chrome120' => %w[curl_chrome120 curl-impersonate-chrome curl_chrome curl-impersonate],
20
- 'chrome123' => %w[curl_chrome123 curl-impersonate-chrome curl_chrome curl-impersonate],
21
- 'chrome124' => %w[curl_chrome124 curl-impersonate-chrome curl_chrome curl-impersonate],
22
- 'chrome131' => %w[curl_chrome131 curl-impersonate-chrome curl_chrome curl-impersonate],
23
- 'chrome133a' => %w[curl_chrome133a curl-impersonate-chrome curl_chrome curl-impersonate],
24
- 'chrome136' => %w[curl_chrome136 curl-impersonate-chrome curl_chrome curl-impersonate],
25
- 'chrome142' => %w[curl_chrome142 curl-impersonate-chrome curl_chrome curl-impersonate],
26
- 'chrome110' => %w[curl_chrome110 curl-impersonate-chrome curl_chrome curl-impersonate],
27
- 'chrome107' => %w[curl_chrome107 curl-impersonate-chrome curl_chrome curl-impersonate],
28
- 'chrome104' => %w[curl_chrome104 curl-impersonate-chrome curl_chrome curl-impersonate],
29
- 'chrome101' => %w[curl_chrome101 curl-impersonate-chrome curl_chrome curl-impersonate],
30
- 'chrome100' => %w[curl_chrome100 curl-impersonate-chrome curl_chrome curl-impersonate],
31
- 'chrome99' => %w[curl_chrome99 curl-impersonate-chrome curl_chrome curl-impersonate],
32
- 'firefox' => %w[curl_firefox135 curl_firefox133 curl_ff117 curl_firefox117 curl_ff109 curl-impersonate-ff curl_firefox curl-impersonate],
33
- 'firefox135' => %w[curl_firefox135 curl-impersonate-ff curl_firefox curl-impersonate],
34
- 'firefox133' => %w[curl_firefox133 curl-impersonate-ff curl_firefox curl-impersonate],
35
- 'firefox117' => %w[curl_ff117 curl_firefox117 curl-impersonate-ff curl_firefox curl-impersonate],
36
- 'firefox109' => %w[curl_ff109 curl_firefox109 curl-impersonate-ff curl_firefox curl-impersonate],
37
- 'firefox102' => %w[curl_ff102 curl_firefox102 curl-impersonate-ff curl_firefox curl-impersonate],
38
- 'firefox98' => %w[curl_ff98 curl_firefox98 curl-impersonate-ff curl_firefox curl-impersonate],
39
- 'safari' => %w[curl_safari180 curl_safari170 curl_safari155 curl_safari153 curl-impersonate-safari curl_safari curl-impersonate],
40
- 'safari180' => %w[curl_safari180 curl_safari18_0 curl-impersonate-safari curl_safari curl-impersonate],
41
- 'safari170' => %w[curl_safari170 curl_safari17_0 curl-impersonate-safari curl_safari curl-impersonate],
42
- 'safari155' => %w[curl_safari155 curl_safari15_5 curl-impersonate-safari curl_safari curl-impersonate],
43
- 'safari15_5' => %w[curl_safari155 curl_safari15_5 curl-impersonate-safari curl_safari curl-impersonate],
44
- 'safari153' => %w[curl_safari153 curl_safari15_3 curl-impersonate-safari curl_safari curl-impersonate],
45
- 'safari15_3' => %w[curl_safari153 curl_safari15_3 curl-impersonate-safari curl_safari curl-impersonate],
46
- 'edge' => %w[curl_edge101 curl_edge99 curl-impersonate-edge curl_edge curl-impersonate],
47
- 'edge101' => %w[curl_edge101 curl-impersonate-edge curl_edge curl-impersonate],
48
- 'edge99' => %w[curl_edge99 curl-impersonate-edge curl_edge curl-impersonate],
49
- 'tor' => %w[curl_tor145 curl_tor curl-impersonate],
50
- 'tor145' => %w[curl_tor145 curl_tor curl-impersonate]
17
+ # Chrome Desktop
18
+ 'chrome' => %w[curl_chrome131 curl_chrome124 curl_chrome120 curl_chrome116 curl_chrome110 curl-impersonate-chrome curl_chrome curl-impersonate],
19
+ 'chrome116' => %w[curl_chrome116 curl-impersonate-chrome curl_chrome curl-impersonate],
20
+ 'chrome120' => %w[curl_chrome120 curl-impersonate-chrome curl_chrome curl-impersonate],
21
+ 'chrome123' => %w[curl_chrome123 curl-impersonate-chrome curl_chrome curl-impersonate],
22
+ 'chrome124' => %w[curl_chrome124 curl-impersonate-chrome curl_chrome curl-impersonate],
23
+ 'chrome131' => %w[curl_chrome131 curl-impersonate-chrome curl_chrome curl-impersonate],
24
+ 'chrome133a' => %w[curl_chrome133a curl-impersonate-chrome curl_chrome curl-impersonate],
25
+ 'chrome136' => %w[curl_chrome136 curl-impersonate-chrome curl_chrome curl-impersonate],
26
+ 'chrome142' => %w[curl_chrome142 curl-impersonate-chrome curl_chrome curl-impersonate],
27
+ 'chrome145' => %w[curl_chrome145 curl_chrome146 curl_chrome150 curl_chrome131 curl-impersonate],
28
+ 'chrome146' => %w[curl_chrome146 curl_chrome150 curl_chrome145 curl_chrome131 curl-impersonate],
29
+ 'chrome150' => %w[curl_chrome150 curl_chrome146 curl_chrome145 curl_chrome131 curl-impersonate],
30
+ 'chrome110' => %w[curl_chrome110 curl-impersonate-chrome curl_chrome curl-impersonate],
31
+ 'chrome107' => %w[curl_chrome107 curl-impersonate-chrome curl_chrome curl-impersonate],
32
+ 'chrome104' => %w[curl_chrome104 curl-impersonate-chrome curl_chrome curl-impersonate],
33
+ 'chrome101' => %w[curl_chrome101 curl-impersonate-chrome curl_chrome curl-impersonate],
34
+ 'chrome100' => %w[curl_chrome100 curl-impersonate-chrome curl_chrome curl-impersonate],
35
+ 'chrome99' => %w[curl_chrome99 curl-impersonate-chrome curl_chrome curl-impersonate],
36
+
37
+ # Mobile / Android
38
+ 'chrome131_android' => %w[curl_chrome131_android curl_chrome131 curl-impersonate-chrome curl-impersonate],
39
+ 'chrome99_android' => %w[curl_chrome99_android curl_chrome99 curl-impersonate-chrome curl-impersonate],
40
+ 'android' => %w[curl_chrome131_android curl_safari180_ios curl_chrome99_android curl_chrome131 curl-impersonate],
41
+ 'chrome_android' => %w[curl_chrome131_android curl_chrome99_android curl_chrome131 curl-impersonate],
42
+
43
+ # Firefox
44
+ 'firefox' => %w[curl_firefox135 curl_firefox133 curl_ff117 curl_firefox117 curl_ff109 curl-impersonate-ff curl_firefox curl-impersonate],
45
+ 'firefox147' => %w[curl_firefox147 curl_firefox144 curl_firefox135 curl-impersonate-ff curl-impersonate],
46
+ 'firefox144' => %w[curl_firefox144 curl_firefox147 curl_firefox135 curl-impersonate-ff curl-impersonate],
47
+ 'firefox135' => %w[curl_firefox135 curl-impersonate-ff curl_firefox curl-impersonate],
48
+ 'firefox133' => %w[curl_firefox133 curl-impersonate-ff curl_firefox curl-impersonate],
49
+ 'firefox117' => %w[curl_ff117 curl_firefox117 curl-impersonate-ff curl_firefox curl-impersonate],
50
+ 'firefox109' => %w[curl_ff109 curl_firefox109 curl-impersonate-ff curl_firefox curl-impersonate],
51
+ 'firefox102' => %w[curl_ff102 curl_firefox102 curl-impersonate-ff curl_firefox curl-impersonate],
52
+ 'firefox98' => %w[curl_ff98 curl_firefox98 curl-impersonate-ff curl_firefox curl-impersonate],
53
+
54
+ # Safari Desktop
55
+ 'safari' => %w[curl_safari180 curl_safari170 curl_safari184 curl_safari260 curl_safari155 curl_safari153 curl-impersonate-safari curl_safari curl-impersonate],
56
+ 'safari2601' => %w[curl_safari2601 curl_safari260 curl_safari180 curl-impersonate-safari curl-impersonate],
57
+ 'safari260' => %w[curl_safari260 curl_safari2601 curl_safari180 curl-impersonate-safari curl-impersonate],
58
+ 'safari184' => %w[curl_safari184 curl_safari180 curl-impersonate-safari curl-impersonate],
59
+ 'safari180' => %w[curl_safari180 curl_safari18_0 curl-impersonate-safari curl_safari curl-impersonate],
60
+ 'safari170' => %w[curl_safari170 curl_safari17_0 curl-impersonate-safari curl_safari curl-impersonate],
61
+ 'safari155' => %w[curl_safari155 curl_safari15_5 curl-impersonate-safari curl_safari curl-impersonate],
62
+ 'safari15_5' => %w[curl_safari155 curl_safari15_5 curl-impersonate-safari curl_safari curl-impersonate],
63
+ 'safari153' => %w[curl_safari153 curl_safari15_3 curl-impersonate-safari curl_safari curl-impersonate],
64
+ 'safari15_3' => %w[curl_safari153 curl_safari15_3 curl-impersonate-safari curl_safari curl-impersonate],
65
+
66
+ # Safari iOS / Mobile
67
+ 'safari180_ios' => %w[curl_safari180_ios curl_safari184_ios curl_safari260_ios curl_safari172_ios curl_safari180 curl-impersonate],
68
+ 'safari184_ios' => %w[curl_safari184_ios curl_safari180_ios curl_safari260_ios curl_safari172_ios curl_safari180 curl-impersonate],
69
+ 'safari260_ios' => %w[curl_safari260_ios curl_safari184_ios curl_safari180_ios curl_safari172_ios curl_safari180 curl-impersonate],
70
+ 'safari172_ios' => %w[curl_safari172_ios curl_safari180_ios curl_safari184_ios curl_safari180 curl-impersonate],
71
+ 'ios' => %w[curl_safari180_ios curl_safari184_ios curl_safari260_ios curl_safari172_ios curl_safari180 curl-impersonate],
72
+ 'safari_ios' => %w[curl_safari180_ios curl_safari184_ios curl_safari260_ios curl_safari172_ios curl_safari180 curl-impersonate],
73
+ 'mobile' => %w[curl_safari180_ios curl_chrome131_android curl_safari184_ios curl_safari260_ios curl-impersonate],
74
+
75
+ # Edge & Tor
76
+ 'edge' => %w[curl_edge101 curl_edge99 curl-impersonate-edge curl_edge curl-impersonate],
77
+ 'edge101' => %w[curl_edge101 curl-impersonate-edge curl_edge curl-impersonate],
78
+ 'edge99' => %w[curl_edge99 curl-impersonate-edge curl_edge curl-impersonate],
79
+ 'tor' => %w[curl_tor145 curl_tor curl-impersonate],
80
+ 'tor145' => %w[curl_tor145 curl_tor curl-impersonate]
51
81
  }.freeze
52
82
 
53
83
  attr_reader :method, :url, :options, :config
@@ -178,6 +208,13 @@ module HttpMimic
178
208
  headers_to_send['Accept'] ||= '*/*'
179
209
  headers_to_send['Accept-Encoding'] ||= 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3'
180
210
  headers_to_send['Connection'] ||= 'close'
211
+ else
212
+ # Automatic Navigation Headers for browser profiles on GET/HEAD
213
+ nav_headers_enabled = options.fetch(:navigation_headers, config.navigation_headers)
214
+ if nav_headers_enabled && %i[get head].include?(method.to_s.downcase.to_sym)
215
+ target = resolve_target
216
+ apply_navigation_headers(headers_to_send, target)
217
+ end
181
218
  end
182
219
 
183
220
  # Append headers
@@ -202,7 +239,20 @@ module HttpMimic
202
239
  [binary, args, stdin_data, final_url]
203
240
  end
204
241
 
205
- def resolve_binary
242
+ def resolve_target
243
+ case options[:profile]
244
+ when :android
245
+ (options[:android_impersonate] || 'chrome131_android').to_s.downcase
246
+ when :ios
247
+ (options[:ios_impersonate] || 'safari260_ios').to_s.downcase
248
+ when :mobile
249
+ (options[:mobile_impersonate] || 'chrome131_android').to_s.downcase
250
+ else
251
+ (options[:impersonate] || config.default_impersonate).to_s.downcase
252
+ end
253
+ end
254
+
255
+ def resolve_binary(target = resolve_target)
206
256
  # 0. If profile is explicitly set to :curl, use system curl
207
257
  if options[:profile] == :curl
208
258
  curl_path = find_executable('curl') || 'curl'
@@ -218,7 +268,6 @@ module HttpMimic
218
268
  raise BinaryNotFoundError, "Specified executable does not exist or is not executable: #{explicit}"
219
269
  end
220
270
 
221
- target = (options[:impersonate] || config.default_impersonate).to_s.downcase
222
271
  candidate_names = TARGET_BINARY_MAP[target] || ["curl_#{target}", target]
223
272
 
224
273
  # 2. Check local installation directory (~/.http_mimic/bin)
@@ -265,6 +314,81 @@ module HttpMimic
265
314
 
266
315
  private
267
316
 
317
+ def apply_navigation_headers(headers_to_send, target)
318
+ target_str = target.to_s.downcase
319
+
320
+ if target_str.start_with?('firefox', 'ff')
321
+ # Firefox (Gecko): Mozilla does not send Client Hints (sec-ch-ua*)
322
+ headers_to_send['sec-fetch-dest'] ||= 'document'
323
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
324
+ headers_to_send['sec-fetch-site'] ||= 'none'
325
+ headers_to_send['sec-fetch-user'] ||= '?1'
326
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
327
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
328
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.5'
329
+
330
+ elsif target_str.start_with?('safari') || target_str == 'ios' || target_str == 'safari_ios' || target_str.include?('_ios')
331
+ # Safari / WebKit (Desktop & iOS): WebKit does not send Client Hints (sec-ch-ua*)
332
+ headers_to_send['sec-fetch-dest'] ||= 'document'
333
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
334
+ headers_to_send['sec-fetch-site'] ||= 'none'
335
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
336
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
337
+
338
+ elsif target_str.start_with?('tor')
339
+ # Tor Browser: No sec-ch-ua headers
340
+ headers_to_send['sec-fetch-dest'] ||= 'document'
341
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
342
+ headers_to_send['sec-fetch-site'] ||= 'none'
343
+ headers_to_send['sec-fetch-user'] ||= '?1'
344
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
345
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
346
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.5'
347
+
348
+ elsif target_str.start_with?('edge')
349
+ # Edge (Chromium): Microsoft Edge sec-ch-ua
350
+ ver = target_str[/\d+/] || '101'
351
+ headers_to_send['sec-ch-ua'] ||= "\"Microsoft Edge\";v=\"#{ver}\", \"Chromium\";v=\"#{ver}\", \"Not_A Brand\";v=\"24\""
352
+ headers_to_send['sec-ch-ua-mobile'] ||= '?0'
353
+ headers_to_send['sec-ch-ua-platform'] ||= '"Windows"'
354
+ headers_to_send['sec-fetch-dest'] ||= 'document'
355
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
356
+ headers_to_send['sec-fetch-site'] ||= 'none'
357
+ headers_to_send['sec-fetch-user'] ||= '?1'
358
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
359
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'
360
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
361
+
362
+ elsif target_str.include?('android')
363
+ # Android Chrome
364
+ ver = target_str[/\d+/] || '131'
365
+ headers_to_send['sec-ch-ua'] ||= "\"Chromium\";v=\"#{ver}\", \"Not_A Brand\";v=\"24\", \"Google Chrome\";v=\"#{ver}\""
366
+ headers_to_send['sec-ch-ua-mobile'] ||= '?1'
367
+ headers_to_send['sec-ch-ua-platform'] ||= '"Android"'
368
+ headers_to_send['sec-fetch-dest'] ||= 'document'
369
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
370
+ headers_to_send['sec-fetch-site'] ||= 'none'
371
+ headers_to_send['sec-fetch-user'] ||= '?1'
372
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
373
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'
374
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
375
+
376
+ else
377
+ # Chrome Desktop (Default)
378
+ ver = target_str[/\d+/] || '131'
379
+ headers_to_send['sec-ch-ua'] ||= "\"Google Chrome\";v=\"#{ver}\", \"Chromium\";v=\"#{ver}\", \"Not_A Brand\";v=\"24\""
380
+ headers_to_send['sec-ch-ua-mobile'] ||= '?0'
381
+ headers_to_send['sec-ch-ua-platform'] ||= '"macOS"'
382
+ headers_to_send['sec-fetch-dest'] ||= 'document'
383
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
384
+ headers_to_send['sec-fetch-site'] ||= 'none'
385
+ headers_to_send['sec-fetch-user'] ||= '?1'
386
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
387
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'
388
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
389
+ end
390
+ end
391
+
268
392
  def find_in_download_dir(candidates)
269
393
  target_dir = File.expand_path(config.install_dir)
270
394
  return nil unless Dir.exist?(target_dir)
@@ -2,7 +2,9 @@
2
2
 
3
3
  module HttpMimic
4
4
  class Configuration
5
+ # Browser simulation & request defaults
5
6
  attr_accessor :default_impersonate
7
+ attr_accessor :default_mobile_impersonate
6
8
  attr_accessor :binary_path
7
9
  attr_accessor :fallback_to_curl
8
10
  attr_accessor :default_timeout
@@ -24,28 +26,57 @@ module HttpMimic
24
26
  attr_accessor :install_dir
25
27
  attr_accessor :github_repo
26
28
 
29
+ # QuickJS standalone binary management settings
30
+ attr_accessor :qjs_version
31
+ attr_accessor :qjs_github_repo
32
+
33
+ # WAF & JS challenge solving settings
34
+ attr_accessor :auto_solve_waf
35
+
36
+ # Shared Host-based CookieStore settings
37
+ attr_accessor :persist_cookies
38
+ attr_accessor :cookie_store_dir
39
+
40
+ # Modern Navigation Headers simulation
41
+ attr_accessor :navigation_headers
42
+
27
43
  def initialize
28
- @default_impersonate = 'chrome131'
29
- @binary_path = nil
30
- @fallback_to_curl = true
31
- @default_timeout = 30
32
- @default_connect_timeout = 10
33
- @follow_redirects = true
34
- @max_redirects = 10
35
- @raise_on_error = false
36
- @logger = nil
37
- @debug = false
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]
44
+ @default_impersonate = 'chrome131'
45
+ @default_mobile_impersonate = 'safari180_ios'
46
+ @binary_path = nil
47
+ @fallback_to_curl = true
48
+ @default_timeout = 30
49
+ @default_connect_timeout = 10
50
+ @follow_redirects = true
51
+ @max_redirects = 10
52
+ @raise_on_error = false
53
+ @logger = nil
54
+ @debug = false
55
+
56
+ # Multi-strategy defaults: :auto seamlessly falls back between impersonate, mobile, and curl
57
+ @mode = :auto
58
+ @auto_fallback = true
59
+ @retry_statuses = [403, 429, 503]
43
60
 
44
61
  # Enable automatic downloading of curl-impersonate driver (lexiforest) by default
45
- @auto_download = true
46
- @driver_version = 'v2.1.1'
47
- @install_dir = File.expand_path('~/.http_mimic/bin')
48
- @github_repo = 'lexiforest/curl-impersonate'
62
+ @auto_download = true
63
+ @driver_version = 'v2.1.1'
64
+ @install_dir = File.expand_path('~/.http_mimic/bin')
65
+ @github_repo = 'lexiforest/curl-impersonate'
66
+
67
+ # QuickJS defaults
68
+ @qjs_version = 'v0.16.2'
69
+ @qjs_github_repo = 'quickjs-ng/quickjs'
70
+
71
+ # WAF challenge solving defaults
72
+ @auto_solve_waf = true
73
+
74
+ # Shared Host CookieStore defaults
75
+ @persist_cookies = false
76
+ @cookie_store_dir = File.expand_path('~/.http_mimic/cookies')
77
+
78
+ # Navigation Headers defaults
79
+ @navigation_headers = true
49
80
  end
50
81
  end
51
82
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module HttpMimic
8
+ class CookieStore
9
+ DEFAULT_TTL = 7200 # 2 hours
10
+
11
+ class << self
12
+ def store_dir
13
+ File.expand_path(HttpMimic.configuration.cookie_store_dir || '~/.http_mimic/cookies')
14
+ end
15
+
16
+ def cookie_file_for(host)
17
+ clean_host = sanitize_host(host)
18
+ File.join(store_dir, "#{clean_host}.json")
19
+ end
20
+
21
+ # Load cookies for a given host
22
+ # Returns a Hash of cookies if valid and not expired, or empty Hash
23
+ def load(host, max_age: DEFAULT_TTL)
24
+ file = cookie_file_for(host)
25
+ return {} unless File.file?(file)
26
+
27
+ data = nil
28
+ File.open(file, File::RDONLY) do |f|
29
+ f.flock(File::LOCK_SH)
30
+ data = JSON.parse(f.read) rescue nil
31
+ end
32
+
33
+ return {} unless data.is_a?(Hash)
34
+
35
+ updated_at = data['updated_at'].to_i
36
+ if !max_age.nil?
37
+ return {} if (Time.now.to_i - updated_at) > max_age
38
+ end
39
+
40
+ data['cookies'] || {}
41
+ rescue StandardError => e
42
+ if HttpMimic.configuration.debug
43
+ puts "[HttpMimic::CookieStore] Error loading cookies for #{host}: #{e.message}"
44
+ end
45
+ {}
46
+ end
47
+
48
+ # Save cookies for a given host with atomic locking
49
+ def save(host, cookies, ttl: DEFAULT_TTL)
50
+ return if cookies.nil? || cookies.empty?
51
+
52
+ clean_host = sanitize_host(host)
53
+ dir = store_dir
54
+ FileUtils.mkdir_p(dir)
55
+
56
+ file = cookie_file_for(clean_host)
57
+ existing_cookies = load(clean_host, max_age: ttl) || {}
58
+
59
+ new_cookies = cookies.is_a?(Cookies) ? cookies.to_h : cookies.dup
60
+ merged = existing_cookies.merge(stringify_keys(new_cookies))
61
+
62
+ payload = {
63
+ 'host' => clean_host,
64
+ 'updated_at' => Time.now.to_i,
65
+ 'ttl' => ttl,
66
+ 'cookies' => merged
67
+ }
68
+
69
+ # Thread & Process safe atomic write
70
+ tmp_file = "#{file}.tmp.#{Process.pid}_#{Time.now.to_f}"
71
+ File.open(tmp_file, File::RDWR | File::CREAT, 0644) do |f|
72
+ f.flock(File::LOCK_EX)
73
+ f.write(JSON.pretty_generate(payload))
74
+ f.flush
75
+ end
76
+ File.rename(tmp_file, file)
77
+ rescue StandardError => e
78
+ FileUtils.rm_f(tmp_file) if tmp_file
79
+ if HttpMimic.configuration.debug
80
+ puts "[HttpMimic::CookieStore] Error saving cookies for #{host}: #{e.message}"
81
+ end
82
+ end
83
+
84
+ # Clear stored cookies for a host or all hosts
85
+ def clear(host = nil)
86
+ if host
87
+ file = cookie_file_for(host)
88
+ FileUtils.rm_f(file)
89
+ else
90
+ FileUtils.rm_rf(store_dir)
91
+ end
92
+ end
93
+
94
+ # Extract host from URL or host string
95
+ def extract_host(url_or_host)
96
+ str = url_or_host.to_s.strip
97
+ if str =~ %r{^https?://}i
98
+ URI.parse(str).host
99
+ else
100
+ str.split(':').first
101
+ end
102
+ rescue StandardError
103
+ str
104
+ end
105
+
106
+ private
107
+
108
+ def sanitize_host(host)
109
+ h = extract_host(host) || 'default'
110
+ h.gsub(/[^a-zA-Z0-9.\-_]/, '_')
111
+ end
112
+
113
+ def stringify_keys(hash)
114
+ hash.each_with_object({}) { |(k, v), h| h[k.to_s] = v.to_s }
115
+ end
116
+ end
117
+ end
118
+ end