http_mimic 0.3.2 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11e55edf4c06160f27621a651e4c16eae929ec08767b5b6eff6b007d8a3a1d4b
4
- data.tar.gz: e90137a02ee867cd744641c725cd3daef2994b5f4a7216304f7ca189e6d92303
3
+ metadata.gz: b09e014134575dac987366c71b0cb6644a13a5b17088947621474f6cfccec5cf
4
+ data.tar.gz: 748d8d3dadcb69d39312a6c7137c32cca1290b7eaee4d8641fa7bb58bfdd0f80
5
5
  SHA512:
6
- metadata.gz: 77dfc30ee87e63105f5cbf95e818cc4b01841f9862566fdceedad277cb22366c05c37ab14b3ac699ddb30d68a9ead35acc43114a1f882c034360d06807450677
7
- data.tar.gz: 4ab621140feebc4c3f9e8897d33edf510785ea6c5cbad5cf08b672ecfde52f6146b6f1a3d695f5129acce0d3c7b64cfdae275803480178500e9c683fb7c053a8
6
+ metadata.gz: a4d17399d5f85df9b508151f371b0910e074d70e3c05f11cb291f30f01b71c20d22e998ec5c5466728c77dd2f37b3de4aecc18883a844fb69952ae71a3a5c5be
7
+ data.tar.gz: 7b0675e724e1c138cd57ea4af5e5a6ce2f057bb50981271f9b81cb894bf7179c2ba760137f0672d742a63b602783bc6b455a71b9da4f778c0637db96ee723470
data/CHANGELOG.md CHANGED
@@ -5,6 +5,59 @@ 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.1] - 2026-08-30
9
+
10
+ ### Added
11
+ - **Two-Phase Cookie & State Pipeline (Tier 1 ➔ Tier 2)**:
12
+ - Automatically forward validated cookies (`NID`, `AEC`, `_abck`, `cf_clearance`) and custom headers from Tier 1 HTTP / WAF resolution directly into Obscura (`--cookie` flag) during `auto_render_spa` transitions.
13
+ - **Obscura Stealth BoringSSL Integration**:
14
+ - Downloader now defaults to `*-stealth.tar.gz` release assets, equipping Obscura with native BoringSSL Chrome TLS / JA4 fingerprint impersonation.
15
+ - **Upgraded Obscura to `anxgang/obscura` v0.2.3**:
16
+ - WebGL context, `REAL_FONT_METRICS` font measurements, and persistent Web Worker `importScripts` support.
17
+
18
+ ## [0.5.0] - 2026-08-30
19
+
20
+ ### Added
21
+ - **Tier 2: Obscura Headless SPA Rendering Support**:
22
+ - Native integration with [`h4ckf0r0day/obscura`](https://github.com/h4ckf0r0day/obscura)—a lightweight (<100MB) Rust + V8 headless browser engine with built-in stealth anti-detection.
23
+ - On-demand automatic driver management (`HttpMimic.download_obscura!`, `HttpMimic.obscura_installed?`, `HttpMimic.obscura_path`) across macOS (ARM64/x86_64), Linux (x86_64/aarch64), and Windows.
24
+ - New rendering helpers: `HttpMimic.render(url, options = {})` and `HttpMimic.spa(url, options = {})`.
25
+ - **Automatic SPA Detection & Transition (`auto_render_spa`)**:
26
+ - Intelligent `HttpMimic::SpaDetector` detects unhydrated client-side SPA shells (React `#root`, Vue `#app`, Angular `<app-root>`, Google Dynamic SERP, `<noscript>` prompts).
27
+ - When `auto_render_spa: true` (or `config.auto_render_spa = true`) is enabled, `HttpMimic.get` automatically transitions from Tier 1 (HTTP) to Tier 2 (Obscura) to render the full dynamic DOM.
28
+ - Configurable options: `:wait_until` (`networkidle0`, `domcontentloaded`, `load`), `:timeout`, `:proxy`, `:eval`, `:dump`, `:selector`, `:user_agent`, and `:stealth`.
29
+
30
+ ## [0.4.1] - 2026-08-30
31
+
32
+ ### Added
33
+ - **Google Search Guard & BotGuard VM Detection**:
34
+ - Added `HttpMimic::Waf::GoogleSolver` and `HttpMimic::Waf::Detector.google?` for detecting Google Search Guard (`knitsail`, `enablejs`, `SG_SS`) challenges.
35
+ - Added automatic Google Search referral context (`Referer: https://www.google.com/`, `sec-fetch-site: same-origin`) in `CommandBuilder#apply_navigation_headers`.
36
+ - **Authentic Hardware & WebGL Pipeline Emulation in QuickJS Context**:
37
+ - Full WebGL shader compilation and rasterization pipeline (`createShader`, `compileShader`, `getShaderPrecisionFormat`, `createProgram`, `linkProgram`, `readPixels` simulated gradient buffer).
38
+ - Dynamic font measurement (`REAL_FONT_METRICS`) with exact macOS Chrome `offsetWidth` / `offsetHeight` dimensions across 17 font families and 7 font sizes.
39
+ - Authentic Chromium `OfflineAudioContext` audio buffer rendering curve.
40
+ - Physics-based Cubic Bézier Spline mouse movement simulation with velocity decay, natural micro-jitter, and complete click event chains.
41
+ - Polyfills for `trustedTypes`, `sessionStorage`, `localStorage`, and `document.currentScript`.
42
+ - **Automatic Response Decompression**:
43
+ - Added `--compressed` flag to CommandBuilder to ensure automatic transparent decompression of `gzip`, `deflate`, `br` (Brotli), and `zstd` payloads.
44
+
45
+ ## [0.4.0] - 2026-08-30
46
+
47
+ ### Added
48
+ - **Target-Specific Navigation Headers & Client Hints Simulation**:
49
+ - 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.
50
+ - 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.
51
+ - Android Chrome targets properly send `sec-ch-ua-mobile: ?1` and `sec-ch-ua-platform: "Android"`.
52
+ - Edge targets properly send `"Microsoft Edge"` brand headers with `"Windows"` platform.
53
+ - **Unified Persistent CookieStore (`persist_cookies`)**:
54
+ - 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/`.
55
+ - Added helper methods: `HttpMimic.load_cookies(host, max_age: nil)`, `HttpMimic.save_cookies(host, cookies, ttl: nil)`, and `HttpMimic.clear_cookies!(host = nil)`.
56
+ - **WAF Challenge Solving & Fallback Performance Optimizations**:
57
+ - Added single-pass WAF solver guard (`waf_solve_attempted`) to prevent redundant JavaScript solver executions across fallback attempts.
58
+ - Added cross-attempt cookie accumulation to preserve session state between fallback profiles.
59
+ - Added `HttpMimic::Waf::Detector.challenge_page?` to detect interstitial challenges disguised under HTTP 200 responses.
60
+
8
61
  ## [0.3.2] - 2026-08-29
9
62
 
10
63
  ### 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)`.
@@ -186,6 +186,40 @@ HttpMimic.get(images.first).save('samba_og.jpg')
186
186
 
187
187
  ---
188
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
207
+ ```
208
+
209
+ ---
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
+
189
223
  ## ⚙️ Global Configuration
190
224
 
191
225
  Configure global defaults in an initializer (e.g., `config/initializers/http_mimic.rb`):
@@ -207,6 +241,16 @@ HttpMimic.configure do |config|
207
241
  config.raise_on_error = false # Raise exceptions on HTTP errors / non-zero exits
208
242
  config.debug = false # Print debug logs
209
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
+
210
254
  # Webdrivers-like auto-download settings (enabled by default)
211
255
  config.auto_download = true # Auto-download missing binary
212
256
  config.driver_version = 'v2.1.1' # Target release version
@@ -225,12 +269,15 @@ end
225
269
  | `:auto_fallback` | Boolean | Whether to automatically retry with alternative profile on blocked status (default `true`) |
226
270
  | `:retry_statuses`| Array | Status codes that trigger auto-fallback (default `[403, 429, 503]`) |
227
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`) |
228
274
  | `:binary` | String | Path to a custom `curl-impersonate` executable |
229
275
  | `:query` / `:params` | Hash | URL query parameters (supports nested parameters and encoding) |
230
276
  | `:headers` | Hash | Custom HTTP request headers |
231
277
  | `:json` | Hash / Array | Serialized to JSON with `Content-Type: application/json` |
232
278
  | `:body` | Hash / String | Form payload (Hash) or raw request body string |
233
279
  | `:cookies` | Hash / String | Request cookies |
280
+ | `:persist_cookies` | Boolean | Automatically load and save cookies for the host across requests |
234
281
  | `:cookie_jar` | String | Path to save cookies (`-c`) |
235
282
  | `:cookie_file` | String | Path to read cookies (`-b`) |
236
283
  | `:timeout` | Integer / Float | Maximum execution timeout in seconds (`--max-time`) |
@@ -240,6 +287,47 @@ end
240
287
  | `:bearer_token` | String | Appends `Authorization: Bearer <token>` header |
241
288
  | `:insecure` | Boolean | Disable SSL certificate verification (`-k`) |
242
289
  | `:curl_options` | Array / String | Additional raw curl arguments (e.g., `['--http2', '--compressed']`) |
290
+ | `:auto_render_spa` | Boolean | Automatically detect unhydrated SPA shells and render via Obscura |
291
+ | `:render` | Symbol | `:spa` or `:obscura` to explicitly render page via Obscura |
292
+
293
+ ---
294
+
295
+ ## ⚡ Obscura SPA Rendering (Tier 2 Engine)
296
+
297
+ `http_mimic` provides native support for [`h4ckf0r0day/obscura`](https://github.com/h4ckf0r0day/obscura)—a lightweight, single-binary (<100MB) headless browser engine written in Rust with an embedded V8 JavaScript runtime and built-in stealth anti-detection capabilities.
298
+
299
+ Obscura is auto-downloaded on-demand into `~/.http_mimic/bin/obscura` when SPA rendering is requested.
300
+
301
+ ### 1. Automatic SPA Detection & Rendering (`auto_render_spa`)
302
+
303
+ Similar to `auto_solve_waf`, when `auto_render_spa: true` is enabled, `HttpMimic` first sends a microsecond-fast HTTP request (Tier 1). If the response is detected as an unhydrated SPA shell (empty React `#root`, Vue `#app`, Angular `<app-root>`, Google Dynamic SERP, or `<noscript>` prompt), it automatically transitions to Obscura (Tier 2) to render the full DOM tree:
304
+
305
+ ```ruby
306
+ # Auto-detects SPA shell and seamlessly renders with Obscura
307
+ response = HttpMimic.get('https://example.com/spa', auto_render_spa: true)
308
+
309
+ puts response.code # => 200
310
+ puts response.title # => Fully hydrated DOM title
311
+ puts response.body # => Fully rendered HTML with client-side injected DOM nodes
312
+ ```
313
+
314
+ ### 2. Direct SPA Rendering
315
+
316
+ ```ruby
317
+ # Direct SPA render
318
+ response = HttpMimic.render('https://example.com/spa')
319
+
320
+ # or via alias:
321
+ response = HttpMimic.spa('https://example.com/spa')
322
+ ```
323
+
324
+ **Supported Options**:
325
+ - `:wait_until` - `'load'`, `'domcontentloaded'`, `'networkidle0'` (default: `'networkidle0'`)
326
+ - `:timeout` - execution deadline in seconds (default: 30)
327
+ - `:proxy` - proxy address (`socks5://...` or `http://...`)
328
+ - `:eval` - JavaScript expression to evaluate in page context
329
+ - `:dump` - output format: `'html'` (default), `'text'`, `'links'`, `'markdown'`, `'cookies'`
330
+ - `:stealth` - whether stealth anti-detection is enabled (default: `true`)
243
331
 
244
332
  ---
245
333
 
@@ -94,9 +94,10 @@ module HttpMimic
94
94
  args = []
95
95
  stdin_data = nil
96
96
 
97
- # Base flags: silent mode, include HTTP headers
97
+ # Base flags: silent mode, include HTTP headers, auto-decompress responses
98
98
  args << '-s'
99
99
  args << '-i'
100
+ args << '--compressed'
100
101
 
101
102
  # HTTP Method
102
103
  if @method == 'HEAD'
@@ -208,6 +209,13 @@ module HttpMimic
208
209
  headers_to_send['Accept'] ||= '*/*'
209
210
  headers_to_send['Accept-Encoding'] ||= 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3'
210
211
  headers_to_send['Connection'] ||= 'close'
212
+ else
213
+ # Automatic Navigation Headers for browser profiles on GET/HEAD
214
+ nav_headers_enabled = options.fetch(:navigation_headers, config.navigation_headers)
215
+ if nav_headers_enabled && %i[get head].include?(method.to_s.downcase.to_sym)
216
+ target = resolve_target
217
+ apply_navigation_headers(headers_to_send, target)
218
+ end
211
219
  end
212
220
 
213
221
  # Append headers
@@ -232,7 +240,20 @@ module HttpMimic
232
240
  [binary, args, stdin_data, final_url]
233
241
  end
234
242
 
235
- def resolve_binary
243
+ def resolve_target
244
+ case options[:profile]
245
+ when :android
246
+ (options[:android_impersonate] || 'chrome131_android').to_s.downcase
247
+ when :ios
248
+ (options[:ios_impersonate] || 'safari260_ios').to_s.downcase
249
+ when :mobile
250
+ (options[:mobile_impersonate] || 'chrome131_android').to_s.downcase
251
+ else
252
+ (options[:impersonate] || config.default_impersonate).to_s.downcase
253
+ end
254
+ end
255
+
256
+ def resolve_binary(target = resolve_target)
236
257
  # 0. If profile is explicitly set to :curl, use system curl
237
258
  if options[:profile] == :curl
238
259
  curl_path = find_executable('curl') || 'curl'
@@ -248,16 +269,6 @@ module HttpMimic
248
269
  raise BinaryNotFoundError, "Specified executable does not exist or is not executable: #{explicit}"
249
270
  end
250
271
 
251
- target = case options[:profile]
252
- when :android
253
- (options[:android_impersonate] || 'chrome131_android').to_s.downcase
254
- when :ios
255
- (options[:ios_impersonate] || 'safari260_ios').to_s.downcase
256
- when :mobile
257
- (options[:mobile_impersonate] || 'chrome131_android').to_s.downcase
258
- else
259
- (options[:impersonate] || config.default_impersonate).to_s.downcase
260
- end
261
272
  candidate_names = TARGET_BINARY_MAP[target] || ["curl_#{target}", target]
262
273
 
263
274
  # 2. Check local installation directory (~/.http_mimic/bin)
@@ -304,6 +315,92 @@ module HttpMimic
304
315
 
305
316
  private
306
317
 
318
+ def apply_navigation_headers(headers_to_send, target)
319
+ target_str = target.to_s.downcase
320
+
321
+ if target_str.start_with?('firefox', 'ff')
322
+ # Firefox (Gecko): Mozilla does not send Client Hints (sec-ch-ua*)
323
+ headers_to_send['sec-fetch-dest'] ||= 'document'
324
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
325
+ headers_to_send['sec-fetch-site'] ||= 'none'
326
+ headers_to_send['sec-fetch-user'] ||= '?1'
327
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
328
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
329
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.5'
330
+
331
+ elsif target_str.start_with?('safari') || target_str == 'ios' || target_str == 'safari_ios' || target_str.include?('_ios')
332
+ # Safari / WebKit (Desktop & iOS): WebKit does not send Client Hints (sec-ch-ua*)
333
+ headers_to_send['sec-fetch-dest'] ||= 'document'
334
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
335
+ headers_to_send['sec-fetch-site'] ||= 'none'
336
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
337
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
338
+
339
+ elsif target_str.start_with?('tor')
340
+ # Tor Browser: No sec-ch-ua headers
341
+ headers_to_send['sec-fetch-dest'] ||= 'document'
342
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
343
+ headers_to_send['sec-fetch-site'] ||= 'none'
344
+ headers_to_send['sec-fetch-user'] ||= '?1'
345
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
346
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
347
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.5'
348
+
349
+ elsif target_str.start_with?('edge')
350
+ # Edge (Chromium): Microsoft Edge sec-ch-ua
351
+ ver = target_str[/\d+/] || '101'
352
+ headers_to_send['sec-ch-ua'] ||= "\"Microsoft Edge\";v=\"#{ver}\", \"Chromium\";v=\"#{ver}\", \"Not_A Brand\";v=\"24\""
353
+ headers_to_send['sec-ch-ua-mobile'] ||= '?0'
354
+ headers_to_send['sec-ch-ua-platform'] ||= '"Windows"'
355
+ headers_to_send['sec-fetch-dest'] ||= 'document'
356
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
357
+ headers_to_send['sec-fetch-site'] ||= 'none'
358
+ headers_to_send['sec-fetch-user'] ||= '?1'
359
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
360
+ 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'
361
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
362
+
363
+ elsif target_str.include?('android')
364
+ # Android Chrome
365
+ ver = target_str[/\d+/] || '131'
366
+ headers_to_send['sec-ch-ua'] ||= "\"Chromium\";v=\"#{ver}\", \"Not_A Brand\";v=\"24\", \"Google Chrome\";v=\"#{ver}\""
367
+ headers_to_send['sec-ch-ua-mobile'] ||= '?1'
368
+ headers_to_send['sec-ch-ua-platform'] ||= '"Android"'
369
+ headers_to_send['sec-fetch-dest'] ||= 'document'
370
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
371
+ headers_to_send['sec-fetch-site'] ||= 'none'
372
+ headers_to_send['sec-fetch-user'] ||= '?1'
373
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
374
+ 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'
375
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
376
+
377
+ else
378
+ # Chrome Desktop (Default)
379
+ ver = target_str[/\d+/] || '131'
380
+ headers_to_send['sec-ch-ua'] ||= "\"Google Chrome\";v=\"#{ver}\", \"Chromium\";v=\"#{ver}\", \"Not_A Brand\";v=\"24\""
381
+ headers_to_send['sec-ch-ua-mobile'] ||= '?0'
382
+ headers_to_send['sec-ch-ua-platform'] ||= '"macOS"'
383
+ headers_to_send['sec-fetch-dest'] ||= 'document'
384
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
385
+ headers_to_send['sec-fetch-site'] ||= 'none'
386
+ headers_to_send['sec-fetch-user'] ||= '?1'
387
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
388
+ 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'
389
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
390
+ end
391
+
392
+ # Smart Search referral context (e.g. searching from Google homepage)
393
+ begin
394
+ parsed_uri = URI.parse(url)
395
+ if parsed_uri && parsed_uri.host =~ /google\./i && parsed_uri.path =~ /\/search/i
396
+ headers_to_send['referer'] ||= "#{parsed_uri.scheme || 'https'}://#{parsed_uri.host}/"
397
+ headers_to_send['sec-fetch-site'] ||= 'same-origin'
398
+ end
399
+ rescue StandardError
400
+ # ignore
401
+ end
402
+ end
403
+
307
404
  def find_in_download_dir(candidates)
308
405
  target_dir = File.expand_path(config.install_dir)
309
406
  return nil unless Dir.exist?(target_dir)
@@ -26,6 +26,26 @@ module HttpMimic
26
26
  attr_accessor :install_dir
27
27
  attr_accessor :github_repo
28
28
 
29
+ # QuickJS standalone binary management settings
30
+ attr_accessor :qjs_version
31
+ attr_accessor :qjs_github_repo
32
+
33
+ # Obscura headless SPA engine settings
34
+ attr_accessor :obscura_version
35
+ attr_accessor :obscura_github_repo
36
+ attr_accessor :obscura_path
37
+ attr_accessor :auto_render_spa
38
+
39
+ # WAF & JS challenge solving settings
40
+ attr_accessor :auto_solve_waf
41
+
42
+ # Shared Host-based CookieStore settings
43
+ attr_accessor :persist_cookies
44
+ attr_accessor :cookie_store_dir
45
+
46
+ # Modern Navigation Headers simulation
47
+ attr_accessor :navigation_headers
48
+
29
49
  def initialize
30
50
  @default_impersonate = 'chrome131'
31
51
  @default_mobile_impersonate = 'safari180_ios'
@@ -45,10 +65,33 @@ module HttpMimic
45
65
  @retry_statuses = [403, 429, 503]
46
66
 
47
67
  # Enable automatic downloading of curl-impersonate driver (lexiforest) by default
48
- @auto_download = true
49
- @driver_version = 'v2.1.1'
50
- @install_dir = File.expand_path('~/.http_mimic/bin')
51
- @github_repo = 'lexiforest/curl-impersonate'
68
+ @auto_download = true
69
+ @driver_version = 'v2.1.1'
70
+ @install_dir = File.expand_path('~/.http_mimic/bin')
71
+ @github_repo = 'lexiforest/curl-impersonate'
72
+
73
+ # QuickJS defaults
74
+ @qjs_version = 'v0.16.2'
75
+ @qjs_github_repo = 'quickjs-ng/quickjs'
76
+
77
+ # Obscura defaults (Lightweight Rust+V8 headless SPA engine)
78
+ # Temporarily points to 'anxgang/obscura' (v0.2.3) for Web Worker importScripts support.
79
+ # Will switch back to 'h4ckf0r0day/obscura' once upstream Pull Request is merged.
80
+ # @obscura_github_repo = 'h4ckf0r0day/obscura'
81
+ @obscura_version = 'v0.2.3'
82
+ @obscura_github_repo = 'anxgang/obscura'
83
+ @obscura_path = nil
84
+ @auto_render_spa = false
85
+
86
+ # WAF challenge solving defaults
87
+ @auto_solve_waf = true
88
+
89
+ # Shared Host CookieStore defaults
90
+ @persist_cookies = false
91
+ @cookie_store_dir = File.expand_path('~/.http_mimic/cookies')
92
+
93
+ # Navigation Headers defaults
94
+ @navigation_headers = true
52
95
  end
53
96
  end
54
97
  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
@@ -47,6 +47,44 @@ module HttpMimic
47
47
  @store.values
48
48
  end
49
49
 
50
+ def empty?
51
+ @store.empty?
52
+ end
53
+
54
+ def size
55
+ @store.size
56
+ end
57
+ alias length size
58
+
59
+ def delete(name)
60
+ @cookie_items.delete(name.to_s)
61
+ @store.delete(name.to_s)
62
+ end
63
+
64
+ def clear
65
+ @cookie_items.clear
66
+ @store.clear
67
+ end
68
+
69
+ def merge(other)
70
+ dup_cookies = self.class.new(@store)
71
+ if other.is_a?(Cookies)
72
+ other.each { |k, v| dup_cookies[k] = v }
73
+ elsif other.is_a?(Hash)
74
+ other.each { |k, v| dup_cookies[k] = v }
75
+ end
76
+ dup_cookies
77
+ end
78
+
79
+ def merge!(other)
80
+ if other.is_a?(Cookies)
81
+ other.each { |k, v| self[k] = v }
82
+ elsif other.is_a?(Hash)
83
+ other.each { |k, v| self[k] = v }
84
+ end
85
+ self
86
+ end
87
+
50
88
  def to_h
51
89
  @store.dup
52
90
  end