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 +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +74 -16
- data/lib/http_mimic/command_builder.rb +160 -36
- data/lib/http_mimic/configuration.rb +50 -19
- data/lib/http_mimic/cookie_store.rb +118 -0
- data/lib/http_mimic/cookies.rb +38 -0
- data/lib/http_mimic/downloader.rb +102 -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 +5 -0
- data/lib/http_mimic/request.rb +62 -5
- data/lib/http_mimic/response.rb +84 -0
- data/lib/http_mimic/response_parser.rb +45 -21
- 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 +61 -0
- data/lib/http_mimic/waf.rb +22 -0
- data/lib/http_mimic.rb +51 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b0a9fff1b74c98ea24c294a491b00ba57e270d24e6c0bb7a669e6d2db264d8a
|
|
4
|
+
data.tar.gz: e11d3ae615b92abcfedda8547120290d82020acd82230353a127cc97dfc844f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
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` (
|
|
167
|
-
- **`:
|
|
168
|
-
- **`:
|
|
169
|
-
- **`:
|
|
170
|
-
- **`:curl_only`**: Strictly uses
|
|
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
|
|
174
|
-
response = HttpMimic.get('https://www.
|
|
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 # => :
|
|
176
|
+
puts response.mode_used # => :android
|
|
177
177
|
puts response.fallback_triggered? # => true
|
|
178
178
|
|
|
179
|
-
# 2.
|
|
180
|
-
response
|
|
181
|
-
|
|
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
|
-
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
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
|
|
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
|
|
29
|
-
@
|
|
30
|
-
@
|
|
31
|
-
@
|
|
32
|
-
@
|
|
33
|
-
@
|
|
34
|
-
@
|
|
35
|
-
@
|
|
36
|
-
@
|
|
37
|
-
@
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@
|
|
42
|
-
@
|
|
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
|
|
46
|
-
@driver_version
|
|
47
|
-
@install_dir
|
|
48
|
-
@github_repo
|
|
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
|