http_mimic 0.5.1 → 0.5.5
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 +57 -0
- data/README.md +76 -33
- data/http_mimic.gemspec +1 -1
- data/lib/http_mimic/command_builder.rb +9 -3
- data/lib/http_mimic/configuration.rb +19 -1
- data/lib/http_mimic/module_methods.rb +28 -0
- data/lib/http_mimic/obscura.rb +21 -2
- data/lib/http_mimic/proxy_pool.rb +224 -0
- data/lib/http_mimic/request.rb +176 -36
- data/lib/http_mimic/response.rb +22 -3
- data/lib/http_mimic/response_parser.rb +6 -0
- data/lib/http_mimic/spa_detector.rb +1 -1
- data/lib/http_mimic/version.rb +2 -1
- data/lib/http_mimic/waf/akamai_solver.rb +252 -67
- data/lib/http_mimic/waf/browser_context.js +1297 -0
- data/lib/http_mimic.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f042a9d602736a448c5341e306e8a46c68fcc38e5159cc881a157a52845d994
|
|
4
|
+
data.tar.gz: 03f41604be51bc6050e1778cd4411a0c3749ad04ef823c7d99839036ec2a1784
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53fa3674cd113da005dfa6a91b46ec2481dbe0c0f8e5fefb3594bb0e84d4b7258569c519d818bcb14a10034de61308155878cfb06cd8965f3e9c7e82613a338c
|
|
7
|
+
data.tar.gz: 47696a2a7f80d0ed2a8ca81236cd9a00f1de6fa8e5ae02bfae3aa2ef2b2965169497e49ee3aec585757c7fae12ac6a63da4398b3c57b720e3ccf6917416e433b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,63 @@ 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.5] - 2026-09-05
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **QuickJS CPT & Bot Manager Telemetry Resolution (Tier 1 WAF Bypass)**:
|
|
12
|
+
- **Standard `document.cookie` CookieJar**: Replaced the plain property in `browser_context.js` with a compliant `Map`-backed CookieJar. When Akamai CPT writes `document.cookie = "bm_lso=..."`, existing session cookies (`bm_sz`, `_abck`) are preserved rather than overwritten. This fixes the critical bug where main sensor telemetry fell back to default hash `8888888` instead of the session cookie hash.
|
|
13
|
+
- **`HTMLScriptElement.src` Property Reflection**: Implemented bidirectional synchronization between `el.src` and `el.getAttribute("src")`. CPT scripts can now successfully inspect `document.currentScript.getAttribute("src")` to locate their challenge parameters and relative post endpoints.
|
|
14
|
+
- **Accurate `document.currentScript` Execution Pipeline**: `AkamaiSolver.build_driver_script` now assigns `document.currentScript` before evaluating each script block, matching real browser multi-script execution order.
|
|
15
|
+
- **Hybrid `XMLHttpRequest` with `responseURL` Support**: Retained instance-level methods (`open`, `send`) required by obfuscated Akamai scripts while maintaining prototype-level methods for DOM script hooks, and populating `responseURL` to allow `challenge.html` reload checks to succeed.
|
|
16
|
+
- **Challenge Query String Preservation in Telemetry POST**: When sending CPT PoW solutions, `AkamaiSolver` now preserves query parameters (e.g. `v=...&t=...`) from the original challenge script URL so that edge servers correctly issue validated `bm_sc` session tokens.
|
|
17
|
+
- **End-to-End Akamai Verification**:
|
|
18
|
+
- `HttpMimic.get('https://www.adidas.com/om/en/samba-og-shoes/KK2371.html')` now successfully solves the CPT challenge within QuickJS and returns the complete 400KB+ product HTML without invoking Tier 2 Obscura.
|
|
19
|
+
|
|
20
|
+
## [0.5.4] - 2026-09-05
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **Challenge Page Regression Prevention**:
|
|
25
|
+
- `HttpMimic::Request` no longer preserves un-bypassed WAF challenge pages (such as Akamai 200 CPT skeletons) as `best_response`. If all resolution and fallback attempts fail, true status codes (e.g. 403) are preserved instead of overwriting with skeleton 200 responses.
|
|
26
|
+
- **Explicit Profile Forwarding in Fallbacks & Retries**:
|
|
27
|
+
- `determine_profiles` now respects `explicit_profile = options[:profile]`, ensuring targeted profiles (e.g., `:android`) remain active when `auto_fallback: false` and are prioritized during fallback loops.
|
|
28
|
+
- `AkamaiSolver` now explicitly forwards `profile` and concrete `impersonate` targets during final URL retries to prevent TLS/fingerprint mismatches.
|
|
29
|
+
- **Mobile/Android Emulation Fidelity in QuickJS Browser Polyfill**:
|
|
30
|
+
- Fixed typo in Android platform identifier (`Linux armv8l` instead of `Linux armv81`).
|
|
31
|
+
- Mobile environments now correctly emulate zero-length plugin and mimeType lists (removing desktop-only PDF plugins).
|
|
32
|
+
- Aligned mobile screen dimensions (412x915 portrait-primary) and WebGL renderer parameters (Qualcomm Adreno 640).
|
|
33
|
+
- **Default Impersonate Profile Upgraded to `chrome150`**:
|
|
34
|
+
- Upgraded default client TLS fingerprint to `chrome150` to satisfy modern Akamai edge HTTP/2 and TLS cipher requirements.
|
|
35
|
+
- **Modern CORS & Fetch Headers in Sensor POSTs**:
|
|
36
|
+
- Automatically injected `Sec-Fetch-Dest: empty`, `Sec-Fetch-Mode: cors`, `Sec-Fetch-Site: same-origin`, and client hints (`sec-ch-ua`, `sec-ch-ua-mobile`, `sec-ch-ua-platform`).
|
|
37
|
+
|
|
38
|
+
## [0.5.3] - 2026-09-05
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- **Automatic Free Proxy Pool (`auto_proxy`)**:
|
|
42
|
+
- Built-in `HttpMimic::ProxyPool` automatically gathers and caches hundreds of public HTTP proxies from maintained sources (`monosans`, `proxyscrape`, `TheSpeedX`).
|
|
43
|
+
- Disabled by default (`auto_proxy: false`) with per-request and global configuration opt-in.
|
|
44
|
+
- Transparent proxy failure retry: automatically detects dead proxies (exit codes 5, 7, 28, 35, 56 or connection errors), marks them dead in the pool, and transparently retries with fresh proxies up to `proxy_retries` (default: 3).
|
|
45
|
+
- Explicit manual `:proxy` option always takes precedence over `auto_proxy`.
|
|
46
|
+
- Seamless integration with Obscura and WAF solver handshakes.
|
|
47
|
+
- Added DSL helpers (`HttpMimic.auto_proxy`, `HttpMimic.proxy_pool`, `HttpMimic.refresh_proxies!`).
|
|
48
|
+
- **Streamlined Documentation**:
|
|
49
|
+
- Refined README with concise, high-level highlights focusing on core anti-detect capabilities and developer experience.
|
|
50
|
+
|
|
51
|
+
## [0.5.2] - 2026-09-05
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
- **Anti-Poisoning Failure Protection for CookieStore (`persist_on_failure`, `clear_on_failure`)**:
|
|
55
|
+
- `persist_on_failure` (default: `false`): Ensures failed/blocked requests (403, 401, retry statuses, or WAF challenge pages) do not save invalid or bot-flagged cookies to disk.
|
|
56
|
+
- `clear_on_failure` (default: `true`): Automatically purges stored host cookies when a request fails verification or is blocked by WAF, preventing poisoned sessions from breaking subsequent requests.
|
|
57
|
+
- Added class-level DSL and configuration support (`HttpMimic.persist_on_failure`, `HttpMimic.clear_on_failure`).
|
|
58
|
+
- **Two-Phase Telemetry Handshake in `AkamaiSolver`**:
|
|
59
|
+
- Automatic multi-round telemetry loop sending second-stage interaction sensor posts after initial cookie acquisition to flip `_abck` tokens from `~-1~` to verified `~0~`.
|
|
60
|
+
- **Intelligent Profile Fallback & WAF Protection**:
|
|
61
|
+
- `:auto` mode now rotates between distinct browser engines (`:impersonate` [Chrome], `:android`, `:ios`, `:firefox`) and skips plain `:curl` fallback on WAF targets to avoid IP flagging.
|
|
62
|
+
- Added `:firefox` and `:safari` profile support in `CommandBuilder`.
|
|
63
|
+
- Added Best Response Preservation: prevents successful/200 progress from being wiped out by later failed fallback attempts.
|
|
64
|
+
|
|
8
65
|
## [0.5.1] - 2026-08-30
|
|
9
66
|
|
|
10
67
|
### Added
|
data/README.md
CHANGED
|
@@ -1,41 +1,27 @@
|
|
|
1
|
-
# HttpMimic
|
|
1
|
+
# HttpMimic 🎭
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
> **Next-Generation Anti-Detect HTTP Client for Ruby**
|
|
4
|
+
> An elegant, high-performance HTTP client combining authentic browser TLS/JA4 fingerprint impersonation, built-in anti-bot challenge solving, lightweight headless SPA rendering, and an automatic free proxy pool into an intuitive HTTParty-style API.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
`http_mimic` provides a complete, modern toolchain for accessing protected endpoints and web scraping:
|
|
7
|
+
- **🎭 Authentic Browser Fingerprints**: Simulates authentic Chrome, Safari (macOS & iOS), Android, and Firefox TLS 1.3 / HTTP/2 handshakes and JA3/JA4 signatures.
|
|
8
|
+
- **🛡️ Built-in Anti-Bot Defense**: Automatically detects and solves advanced WAF challenges at microsecond speeds without the overhead of heavy browsers.
|
|
9
|
+
- **⚡ Dual-Tier Engine with Headless SPA Support**: Fast HTTP-first pipeline with seamless escalation to an embedded, lightweight (<100MB) Rust+V8 headless engine (`auto_render_spa`) when dynamic DOM rendering is needed.
|
|
10
|
+
- **🌐 Automatic Free Proxy Pool (`auto_proxy`)**: Built-in, zero-dependency proxy pool with health tracking, auto-rotation, and transparent failure retry.
|
|
11
|
+
- **🍪 Resilient Session Management**: Host-based persistent cookie caching with automatic anti-poisoning protection to prevent tainted sessions.
|
|
12
|
+
- **📦 Zero-Setup Driver Management**: Automatically provisions and manages required native binaries across macOS, Linux, and Windows on demand.
|
|
13
|
+
- **🚀 Intuitive Ruby DSL**: Clean, idiomatic syntax supporting class mixins (`include HttpMimic`), direct module calls (`HttpMimic.get`), and reusable client instances.
|
|
7
14
|
|
|
8
15
|
---
|
|
9
16
|
|
|
10
|
-
## 🌟
|
|
11
|
-
|
|
12
|
-
- **
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- **
|
|
18
|
-
- Chrome support: `chrome131` (default), `chrome124`, `chrome120`, `chrome133a`, `chrome136`, `chrome142`, `chrome99-110`.
|
|
19
|
-
- Firefox support: `firefox135`, `firefox133`, `firefox144`, `firefox117`, `firefox109`, `firefox102`, `firefox98`.
|
|
20
|
-
- Safari support: `safari180`, `safari170`, `safari155`, `safari153`.
|
|
21
|
-
- Edge & Tor support: `edge101`, `edge99`, `tor145`.
|
|
22
|
-
- **HTTParty-Style API**:
|
|
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`, `persist_cookies`).
|
|
25
|
-
- Reusable instance client: `HttpMimic::Client.new(...)`.
|
|
26
|
-
- **Zero Shell Injection Risk**:
|
|
27
|
-
- Executes commands with array arguments via `Open3.capture3(*cmd_array)`.
|
|
28
|
-
- Uses stdin streaming (`-d @-`) to safely handle large payloads without hitting OS command-line limits.
|
|
29
|
-
- **Smart Response Parsing**:
|
|
30
|
-
- HTTP status helpers: `response.code`, `response.success?`, `response.redirect?`, `response.client_error?`, `response.server_error?`.
|
|
31
|
-
- Case-insensitive header access: `response.headers['Content-Type']`.
|
|
32
|
-
- Automatic `Set-Cookie` header parsing: `response.cookies['session_id']`.
|
|
33
|
-
- Auto-parsed JSON with object delegation: `response['key']`, `response.parsed_response`.
|
|
34
|
-
- Complete 3xx redirect history tracking: `response.history`.
|
|
35
|
-
- **Comprehensive Request Options**:
|
|
36
|
-
- Supports `query`, `headers`, `json`, `body` (form data), `cookies`, `timeout`, `connect_timeout`, `proxy`, `basic_auth`, `digest_auth`, `bearer_token`, `insecure`, custom `curl_options`, and more.
|
|
37
|
-
- **Graceful Fallback**:
|
|
38
|
-
- If a specific binary is unavailable and auto-download is disabled, automatically falls back to system standard `curl`.
|
|
17
|
+
## 🌟 Highlights
|
|
18
|
+
|
|
19
|
+
- **Authentic TLS & HTTP/2 Impersonation**: Native support for `chrome131` (default), `chrome120-142`, `safari180` (macOS/iOS), `firefox135`, `edge101`, and `tor145`.
|
|
20
|
+
- **Intelligent Challenge Resolution**: Automatic WAF challenge mitigation and smart profile fallback to keep requests succeeding.
|
|
21
|
+
- **Client-Side SPA Hydration**: High-speed HTTP execution by default, with optional V8 DOM hydration for complex JavaScript apps (`auto_render_spa: true`).
|
|
22
|
+
- **Free Proxy Pool & Auto-Rotation**: Opt-in proxy management (`auto_proxy: true`) with automatic dead-proxy detection and retry.
|
|
23
|
+
- **Safe & Reliable**: Zero shell injection risk via `Open3.capture3(*cmd_array)`, safe stdin streaming for large payloads, and smart response parsing.
|
|
24
|
+
- **Flexible Integration**: Works seamlessly as a one-off client, a reusable instance, or a class-level DSL.
|
|
39
25
|
|
|
40
26
|
---
|
|
41
27
|
|
|
@@ -198,6 +184,8 @@ response = HttpMimic.get('https://example.com/items', persist_cookies: true)
|
|
|
198
184
|
class Scraper
|
|
199
185
|
include HttpMimic
|
|
200
186
|
persist_cookies true
|
|
187
|
+
# persist_on_failure false # (default: do not save cookies on 403 / verification failure)
|
|
188
|
+
# clear_on_failure true # (default: auto-clear cached cookies if verification fails)
|
|
201
189
|
end
|
|
202
190
|
|
|
203
191
|
# 3. Manual CookieStore helpers
|
|
@@ -206,6 +194,9 @@ HttpMimic.save_cookies('example.com', { session: '123' }) # Save cookies with de
|
|
|
206
194
|
HttpMimic.clear_cookies!('example.com') # Clear host or all cookies
|
|
207
195
|
```
|
|
208
196
|
|
|
197
|
+
> **🛡️ Anti-Poisoning Failure Protection:**
|
|
198
|
+
> By default, `persist_on_failure` is set to `false`, meaning cookies returned with HTTP error codes (e.g. 403, 401) or unverified WAF challenge pages are **never** persisted to disk. Furthermore, `clear_on_failure: true` automatically wipes tainted host cookies upon verification failure, preventing blocked sessions from poisoning subsequent requests.
|
|
199
|
+
|
|
209
200
|
---
|
|
210
201
|
|
|
211
202
|
## 🧭 Target-Specific Navigation Headers & Client Hints
|
|
@@ -247,6 +238,8 @@ HttpMimic.configure do |config|
|
|
|
247
238
|
# Host CookieStore Persistence (disabled by default)
|
|
248
239
|
config.persist_cookies = false # Auto-persist cookies per host
|
|
249
240
|
config.cookie_store_dir = File.expand_path('~/.http_mimic/cookies') # Directory for cookie store
|
|
241
|
+
config.persist_on_failure = false # Do not save cookies if request/verification fails
|
|
242
|
+
config.clear_on_failure = true # Purge cached host cookies on 403 / verification failure
|
|
250
243
|
|
|
251
244
|
# WAF challenge solving (enabled by default)
|
|
252
245
|
config.auto_solve_waf = true # Automatically solve detected WAF JS challenges
|
|
@@ -278,11 +271,14 @@ end
|
|
|
278
271
|
| `:body` | Hash / String | Form payload (Hash) or raw request body string |
|
|
279
272
|
| `:cookies` | Hash / String | Request cookies |
|
|
280
273
|
| `:persist_cookies` | Boolean | Automatically load and save cookies for the host across requests |
|
|
274
|
+
| `:persist_on_failure` | Boolean | Save cookies even if request or verification fails (default `false`) |
|
|
275
|
+
| `:clear_on_failure` | Boolean | Automatically clear stored cookies on verification failure / 403 / WAF block (default `true`) |
|
|
281
276
|
| `:cookie_jar` | String | Path to save cookies (`-c`) |
|
|
282
277
|
| `:cookie_file` | String | Path to read cookies (`-b`) |
|
|
283
278
|
| `:timeout` | Integer / Float | Maximum execution timeout in seconds (`--max-time`) |
|
|
284
279
|
| `:connect_timeout` | Integer / Float | Connection timeout in seconds (`--connect-timeout`) |
|
|
285
280
|
| `:proxy` | String | Proxy address (e.g., `'http://127.0.0.1:8888'`) |
|
|
281
|
+
| `:auto_proxy` | Boolean | Automatically fetch and rotate through a pool of free HTTP proxies (default: `false`) |
|
|
286
282
|
| `:basic_auth` | Hash | `{ username: 'admin', password: 'secret' }` |
|
|
287
283
|
| `:bearer_token` | String | Appends `Authorization: Bearer <token>` header |
|
|
288
284
|
| `:insecure` | Boolean | Disable SSL certificate verification (`-k`) |
|
|
@@ -331,6 +327,53 @@ response = HttpMimic.spa('https://example.com/spa')
|
|
|
331
327
|
|
|
332
328
|
---
|
|
333
329
|
|
|
330
|
+
## 🌐 Automatic Free Proxy Pool (`auto_proxy`)
|
|
331
|
+
|
|
332
|
+
`http_mimic` provides a built-in, zero-dependency Free Proxy Pool manager. When enabled, `HttpMimic` automatically fetches and pools hundreds of free HTTP proxies from maintained public sources (monosans, ProxyScrape, TheSpeedX), automatically marks dead proxies, and transparently retries requests on network or proxy failure.
|
|
333
|
+
|
|
334
|
+
By default, `auto_proxy` is **`false`** so users can choose when to opt-in.
|
|
335
|
+
|
|
336
|
+
### 1. Per-Request Opt-in
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
# Automatically obtain a proxy from the pool and execute request
|
|
340
|
+
response = HttpMimic.get('https://httpbin.org/ip', auto_proxy: true)
|
|
341
|
+
|
|
342
|
+
# Explicit proxy always takes precedence over auto_proxy:
|
|
343
|
+
response = HttpMimic.get('https://httpbin.org/ip', auto_proxy: true, proxy: 'http://my-dedicated-proxy:8080')
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### 2. Global Configuration
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
HttpMimic.configure do |config|
|
|
350
|
+
config.auto_proxy = true # Enable free proxy pool by default
|
|
351
|
+
config.proxy_retries = 3 # Maximum proxy retry attempts on connection failure
|
|
352
|
+
config.proxy_pool_ttl = 1800 # Cache duration for fetched proxy list (seconds)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Or via class-level DSL:
|
|
356
|
+
HttpMimic.auto_proxy = true
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### 3. Proxy Pool Management
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
# Check available pool size
|
|
363
|
+
HttpMimic.proxy_pool.size
|
|
364
|
+
|
|
365
|
+
# Sample a random proxy from the pool
|
|
366
|
+
HttpMimic.proxy_pool.sample # => "http://185.200.188.234:10001"
|
|
367
|
+
|
|
368
|
+
# Manually refresh the proxy pool
|
|
369
|
+
HttpMimic.refresh_proxies!
|
|
370
|
+
|
|
371
|
+
# Load custom proxies into the pool
|
|
372
|
+
HttpMimic.proxy_pool.load(['http://1.2.3.4:8080', 'http://5.6.7.8:3128'])
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
334
377
|
## 📄 Response Object
|
|
335
378
|
|
|
336
379
|
The `Response` object wraps the HTTP response with convenient methods:
|
data/http_mimic.gemspec
CHANGED
|
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
|
|
22
22
|
spec.required_ruby_version = ">= 2.3.0"
|
|
23
23
|
|
|
24
|
-
spec.files = Dir["lib/**/*.rb", "README.md", "CHANGELOG.md", "LICENSE.txt", "http_mimic.gemspec"]
|
|
24
|
+
spec.files = Dir["lib/**/*.{rb,js}", "README.md", "CHANGELOG.md", "LICENSE.txt", "http_mimic.gemspec"]
|
|
25
25
|
spec.require_paths = ["lib"]
|
|
26
26
|
|
|
27
27
|
spec.add_development_dependency "bundler", ">= 1.17"
|
|
@@ -15,7 +15,7 @@ module HttpMimic
|
|
|
15
15
|
|
|
16
16
|
TARGET_BINARY_MAP = {
|
|
17
17
|
# Chrome Desktop
|
|
18
|
-
'chrome' => %w[curl_chrome131 curl_chrome124 curl_chrome120 curl_chrome116 curl_chrome110 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
18
|
+
'chrome' => %w[curl_chrome150 curl_chrome146 curl_chrome145 curl_chrome136 curl_chrome133a curl_chrome131 curl_chrome124 curl_chrome120 curl_chrome116 curl_chrome110 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
19
19
|
'chrome116' => %w[curl_chrome116 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
20
20
|
'chrome120' => %w[curl_chrome120 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
21
21
|
'chrome123' => %w[curl_chrome123 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
@@ -118,10 +118,12 @@ module HttpMimic
|
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# Timeouts
|
|
121
|
-
|
|
121
|
+
default_max_time = options[:auto_proxy] ? 12 : config.default_timeout
|
|
122
|
+
timeout = options[:timeout] || options[:read_timeout] || default_max_time
|
|
122
123
|
args << '--max-time' << timeout.to_s if timeout
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
default_conn_timeout = options[:auto_proxy] ? 4 : config.default_connect_timeout
|
|
126
|
+
connect_timeout = options[:connect_timeout] || default_conn_timeout
|
|
125
127
|
args << '--connect-timeout' << connect_timeout.to_s if connect_timeout
|
|
126
128
|
|
|
127
129
|
# SSL / Insecure
|
|
@@ -248,6 +250,10 @@ module HttpMimic
|
|
|
248
250
|
(options[:ios_impersonate] || 'safari260_ios').to_s.downcase
|
|
249
251
|
when :mobile
|
|
250
252
|
(options[:mobile_impersonate] || 'chrome131_android').to_s.downcase
|
|
253
|
+
when :firefox
|
|
254
|
+
(options[:firefox_impersonate] || 'firefox135').to_s.downcase
|
|
255
|
+
when :safari
|
|
256
|
+
(options[:safari_impersonate] || 'safari180').to_s.downcase
|
|
251
257
|
else
|
|
252
258
|
(options[:impersonate] || config.default_impersonate).to_s.downcase
|
|
253
259
|
end
|
|
@@ -42,12 +42,21 @@ module HttpMimic
|
|
|
42
42
|
# Shared Host-based CookieStore settings
|
|
43
43
|
attr_accessor :persist_cookies
|
|
44
44
|
attr_accessor :cookie_store_dir
|
|
45
|
+
attr_accessor :persist_on_failure
|
|
46
|
+
attr_accessor :clear_on_failure
|
|
45
47
|
|
|
46
48
|
# Modern Navigation Headers simulation
|
|
47
49
|
attr_accessor :navigation_headers
|
|
48
50
|
|
|
51
|
+
# Automatic Free Proxy Pool settings
|
|
52
|
+
attr_accessor :auto_proxy
|
|
53
|
+
attr_accessor :proxy_sources
|
|
54
|
+
attr_accessor :proxy_retries
|
|
55
|
+
attr_accessor :proxy_pool_ttl
|
|
56
|
+
attr_accessor :proxy_timeout
|
|
57
|
+
|
|
49
58
|
def initialize
|
|
50
|
-
@default_impersonate = '
|
|
59
|
+
@default_impersonate = 'chrome150'
|
|
51
60
|
@default_mobile_impersonate = 'safari180_ios'
|
|
52
61
|
@binary_path = nil
|
|
53
62
|
@fallback_to_curl = true
|
|
@@ -89,9 +98,18 @@ module HttpMimic
|
|
|
89
98
|
# Shared Host CookieStore defaults
|
|
90
99
|
@persist_cookies = false
|
|
91
100
|
@cookie_store_dir = File.expand_path('~/.http_mimic/cookies')
|
|
101
|
+
@persist_on_failure = false
|
|
102
|
+
@clear_on_failure = true
|
|
92
103
|
|
|
93
104
|
# Navigation Headers defaults
|
|
94
105
|
@navigation_headers = true
|
|
106
|
+
|
|
107
|
+
# Automatic Free Proxy Pool defaults
|
|
108
|
+
@auto_proxy = false
|
|
109
|
+
@proxy_sources = nil
|
|
110
|
+
@proxy_retries = 3
|
|
111
|
+
@proxy_pool_ttl = 1800
|
|
112
|
+
@proxy_timeout = 5
|
|
95
113
|
end
|
|
96
114
|
end
|
|
97
115
|
end
|
|
@@ -35,6 +35,24 @@ module HttpMimic
|
|
|
35
35
|
default_options[:proxy] = proxy_str
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
def auto_proxy(enabled = nil)
|
|
39
|
+
return default_options[:auto_proxy] if enabled.nil?
|
|
40
|
+
default_options[:auto_proxy] = enabled
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def proxy_pool
|
|
44
|
+
ProxyPool.instance
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def proxy_sources(sources = nil)
|
|
48
|
+
return configuration.proxy_sources if sources.nil?
|
|
49
|
+
configuration.proxy_sources = sources
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def refresh_proxies!(force: true)
|
|
53
|
+
ProxyPool.refresh!(force: force)
|
|
54
|
+
end
|
|
55
|
+
|
|
38
56
|
def cookies(c = nil)
|
|
39
57
|
return default_options[:cookies] if c.nil?
|
|
40
58
|
default_options[:cookies] ||= {}
|
|
@@ -46,6 +64,16 @@ module HttpMimic
|
|
|
46
64
|
default_options[:persist_cookies] = enabled
|
|
47
65
|
end
|
|
48
66
|
|
|
67
|
+
def persist_on_failure(enabled = nil)
|
|
68
|
+
return default_options[:persist_on_failure] if enabled.nil?
|
|
69
|
+
default_options[:persist_on_failure] = enabled
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def clear_on_failure(enabled = nil)
|
|
73
|
+
return default_options[:clear_on_failure] if enabled.nil?
|
|
74
|
+
default_options[:clear_on_failure] = enabled
|
|
75
|
+
end
|
|
76
|
+
|
|
49
77
|
def auto_render_spa(enabled = nil)
|
|
50
78
|
return default_options[:auto_render_spa] if enabled.nil?
|
|
51
79
|
default_options[:auto_render_spa] = enabled
|
data/lib/http_mimic/obscura.rb
CHANGED
|
@@ -82,13 +82,32 @@ module HttpMimic
|
|
|
82
82
|
|
|
83
83
|
code = (status && status.success?) ? 200 : (status ? status.exitstatus : 500)
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
# Detect if the rendered output contains an explicit HTTP error block page
|
|
86
|
+
if code == 200 && stdout
|
|
87
|
+
if stdout.include?('HTTP 403 - Forbidden') ||
|
|
88
|
+
(stdout.include?('Reference Error:') && stdout.include?('Akamai')) ||
|
|
89
|
+
stdout.include?('"page_name": "403 ERROR"')
|
|
90
|
+
code = 403
|
|
91
|
+
elsif stdout.include?('404 Not Found') && stdout.include?('<title>404')
|
|
92
|
+
code = 404
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
status_msg = if code == 200
|
|
97
|
+
'OK (Obscura SPA Rendered)'
|
|
98
|
+
elsif code == 403
|
|
99
|
+
'Forbidden (WAF Blocked)'
|
|
100
|
+
else
|
|
101
|
+
'Error'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
raw_headers = "HTTP/2 #{code} #{status_msg}\r\ncontent-type: text/html; charset=utf-8\r\nx-rendered-by: obscura\r\n\r\n"
|
|
86
105
|
headers = Headers.new({ 'content-type' => 'text/html; charset=utf-8', 'x-rendered-by' => 'obscura' })
|
|
87
106
|
|
|
88
107
|
Response.new(
|
|
89
108
|
code: code,
|
|
90
109
|
http_version: 'HTTP/2',
|
|
91
|
-
status_message:
|
|
110
|
+
status_message: status_msg,
|
|
92
111
|
headers: headers,
|
|
93
112
|
cookies: Cookies.new,
|
|
94
113
|
body: stdout,
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'set'
|
|
6
|
+
|
|
7
|
+
module HttpMimic
|
|
8
|
+
class ProxyPool
|
|
9
|
+
DEFAULT_SOURCES = [
|
|
10
|
+
'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt',
|
|
11
|
+
'https://api.proxyscrape.com/v2/?request=displayproxies&protocol=http&timeout=5000&country=all&ssl=all&anonymity=all',
|
|
12
|
+
'https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/http.txt'
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def instance
|
|
17
|
+
@instance ||= new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset_instance!
|
|
21
|
+
@instance = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get
|
|
25
|
+
instance.get
|
|
26
|
+
end
|
|
27
|
+
alias sample get
|
|
28
|
+
alias next_proxy get
|
|
29
|
+
|
|
30
|
+
def mark_dead(proxy)
|
|
31
|
+
instance.mark_dead(proxy)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def mark_alive(proxy)
|
|
35
|
+
instance.mark_alive(proxy)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def refresh!(force: true)
|
|
39
|
+
instance.refresh!(force: force)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def load(proxies)
|
|
43
|
+
instance.load(proxies)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def all
|
|
47
|
+
instance.all
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def available
|
|
51
|
+
instance.available
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def dead_proxies
|
|
55
|
+
instance.dead_proxies
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def size
|
|
59
|
+
instance.size
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def clear!
|
|
63
|
+
instance.clear!
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
attr_reader :sources, :ttl, :timeout
|
|
68
|
+
attr_accessor :proxies
|
|
69
|
+
|
|
70
|
+
def initialize(options = {})
|
|
71
|
+
config = HttpMimic.configuration rescue nil
|
|
72
|
+
@sources = options[:sources] || config&.proxy_sources || DEFAULT_SOURCES.dup
|
|
73
|
+
@ttl = options[:ttl] || config&.proxy_pool_ttl || 1800
|
|
74
|
+
@timeout = options[:timeout] || config&.proxy_timeout || 5
|
|
75
|
+
@proxies = []
|
|
76
|
+
@dead_proxies = Set.new
|
|
77
|
+
@last_fetched_at = nil
|
|
78
|
+
@mutex = Mutex.new
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Retrieve an available proxy from the pool
|
|
82
|
+
#
|
|
83
|
+
# @return [String, nil] Proxy URL (e.g. 'http://1.2.3.4:8080') or nil if none available
|
|
84
|
+
def get
|
|
85
|
+
@mutex.synchronize do
|
|
86
|
+
refresh_unlocked(force: false) if should_refresh_unlocked?
|
|
87
|
+
avail = @proxies - @dead_proxies.to_a
|
|
88
|
+
if avail.empty? && !@proxies.empty?
|
|
89
|
+
# If all current proxies are exhausted/dead, clear dead set and try one more refresh
|
|
90
|
+
@dead_proxies.clear
|
|
91
|
+
refresh_unlocked(force: true)
|
|
92
|
+
avail = @proxies - @dead_proxies.to_a
|
|
93
|
+
end
|
|
94
|
+
avail.sample
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
alias sample get
|
|
98
|
+
alias next_proxy get
|
|
99
|
+
|
|
100
|
+
# Mark a proxy as dead/unusable
|
|
101
|
+
#
|
|
102
|
+
# @param proxy [String]
|
|
103
|
+
def mark_dead(proxy)
|
|
104
|
+
return unless proxy
|
|
105
|
+
@mutex.synchronize do
|
|
106
|
+
@dead_proxies.add(normalize_proxy(proxy))
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Mark a proxy as active/usable
|
|
111
|
+
#
|
|
112
|
+
# @param proxy [String]
|
|
113
|
+
def mark_alive(proxy)
|
|
114
|
+
return unless proxy
|
|
115
|
+
@mutex.synchronize do
|
|
116
|
+
@dead_proxies.delete(normalize_proxy(proxy))
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Explicitly refresh proxy pool from configured sources
|
|
121
|
+
def refresh!(force: true)
|
|
122
|
+
@mutex.synchronize do
|
|
123
|
+
refresh_unlocked(force: force)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Manually load a custom array of proxies
|
|
128
|
+
#
|
|
129
|
+
# @param proxy_list [Array<String>]
|
|
130
|
+
def load(proxy_list)
|
|
131
|
+
@mutex.synchronize do
|
|
132
|
+
@proxies = Array(proxy_list).map { |p| normalize_proxy(p) }.compact.uniq
|
|
133
|
+
@dead_proxies.clear
|
|
134
|
+
@last_fetched_at = Time.now
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns all loaded proxies
|
|
139
|
+
def all
|
|
140
|
+
@mutex.synchronize { @proxies.dup }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Returns all non-dead available proxies
|
|
144
|
+
def available
|
|
145
|
+
@mutex.synchronize { (@proxies - @dead_proxies.to_a).dup }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns dead proxies
|
|
149
|
+
def dead_proxies
|
|
150
|
+
@mutex.synchronize { @dead_proxies.to_a }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Number of available proxies
|
|
154
|
+
def size
|
|
155
|
+
@mutex.synchronize { (@proxies - @dead_proxies.to_a).size }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Reset proxy pool
|
|
159
|
+
def clear!
|
|
160
|
+
@mutex.synchronize do
|
|
161
|
+
@proxies.clear
|
|
162
|
+
@dead_proxies.clear
|
|
163
|
+
@last_fetched_at = nil
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def should_refresh_unlocked?
|
|
170
|
+
@proxies.empty? || @last_fetched_at.nil? || (Time.now - @last_fetched_at > @ttl)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def refresh_unlocked(force: false)
|
|
174
|
+
return if !force && !should_refresh_unlocked?
|
|
175
|
+
|
|
176
|
+
new_proxies = fetch_from_sources
|
|
177
|
+
if !new_proxies.empty?
|
|
178
|
+
@proxies = new_proxies
|
|
179
|
+
@last_fetched_at = Time.now
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def fetch_from_sources
|
|
184
|
+
collected = []
|
|
185
|
+
sources_to_try = Array(@sources).empty? ? DEFAULT_SOURCES : @sources
|
|
186
|
+
|
|
187
|
+
sources_to_try.each do |source_url|
|
|
188
|
+
begin
|
|
189
|
+
uri = URI.parse(source_url)
|
|
190
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
191
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
192
|
+
http.open_timeout = @timeout
|
|
193
|
+
http.read_timeout = @timeout
|
|
194
|
+
|
|
195
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
196
|
+
req['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
|
197
|
+
|
|
198
|
+
res = http.request(req)
|
|
199
|
+
if res.is_a?(Net::HTTPSuccess) && res.body
|
|
200
|
+
# Extract IP:PORT matches
|
|
201
|
+
ips = res.body.scan(/\b(?:\d{1,3}\.){3}\d{1,3}:\d{2,5}\b/)
|
|
202
|
+
if !ips.empty?
|
|
203
|
+
collected.concat(ips.take(150))
|
|
204
|
+
# Stop early if we have collected enough proxies
|
|
205
|
+
break if collected.size >= 50
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
if HttpMimic.configuration.debug
|
|
210
|
+
puts "[HttpMimic::ProxyPool] Failed to fetch proxy list from #{source_url}: #{e.message}"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
collected.uniq.map { |p| normalize_proxy(p) }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def normalize_proxy(proxy)
|
|
219
|
+
p = proxy.to_s.strip
|
|
220
|
+
return nil if p.empty?
|
|
221
|
+
p.start_with?('http://', 'https://', 'socks5://', 'socks4://') ? p : "http://#{p}"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|