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 +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +89 -1
- data/lib/http_mimic/command_builder.rb +109 -12
- data/lib/http_mimic/configuration.rb +47 -4
- data/lib/http_mimic/cookie_store.rb +118 -0
- data/lib/http_mimic/cookies.rb +38 -0
- data/lib/http_mimic/downloader.rb +199 -0
- data/lib/http_mimic/exceptions.rb +15 -0
- data/lib/http_mimic/js_runtime.rb +78 -0
- data/lib/http_mimic/module_methods.rb +16 -0
- data/lib/http_mimic/obscura.rb +139 -0
- data/lib/http_mimic/request.rb +75 -1
- data/lib/http_mimic/spa_detector.rb +95 -0
- data/lib/http_mimic/version.rb +1 -1
- data/lib/http_mimic/waf/akamai_solver.rb +123 -0
- data/lib/http_mimic/waf/detector.rb +73 -0
- data/lib/http_mimic/waf/google_solver.rb +124 -0
- data/lib/http_mimic/waf.rb +25 -0
- data/lib/http_mimic.rb +77 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b09e014134575dac987366c71b0cb6644a13a5b17088947621474f6cfccec5cf
|
|
4
|
+
data.tar.gz: 748d8d3dadcb69d39312a6c7137c32cca1290b7eaee4d8641fa7bb58bfdd0f80
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
49
|
-
@driver_version
|
|
50
|
-
@install_dir
|
|
51
|
-
@github_repo
|
|
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
|
data/lib/http_mimic/cookies.rb
CHANGED
|
@@ -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
|