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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b09e014134575dac987366c71b0cb6644a13a5b17088947621474f6cfccec5cf
4
- data.tar.gz: 748d8d3dadcb69d39312a6c7137c32cca1290b7eaee4d8641fa7bb58bfdd0f80
3
+ metadata.gz: 9f042a9d602736a448c5341e306e8a46c68fcc38e5159cc881a157a52845d994
4
+ data.tar.gz: 03f41604be51bc6050e1778cd4411a0c3749ad04ef823c7d99839036ec2a1784
5
5
  SHA512:
6
- metadata.gz: a4d17399d5f85df9b508151f371b0910e074d70e3c05f11cb291f30f01b71c20d22e998ec5c5466728c77dd2f37b3de4aecc18883a844fb69952ae71a3a5c5be
7
- data.tar.gz: 7b0675e724e1c138cd57ea4af5e5a6ce2f057bb50981271f9b81cb894bf7179c2ba760137f0672d742a63b602783bc6b455a71b9da4f778c0637db96ee723470
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
- `http_mimic` is a Ruby HTTP Client gem built on top of `Open3.capture3` and `curl-impersonate`.
4
- It provides an elegant, concise, and intuitive **HTTParty-style** API while leveraging [`lexiforest/curl-impersonate`](https://github.com/lexiforest/curl-impersonate) to simulate authentic Chrome, Firefox, Safari, Edge, and Tor TLS / HTTP2 fingerprints (JA3, JA4, Akamai) and handshakes.
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
- It also includes **Webdrivers-like automatic driver management**, automatically downloading and managing `curl-impersonate` binaries across macOS, Linux, and Windows without manual setup.
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
- ## 🌟 Features
11
-
12
- - **Webdrivers-Style Driver Management**:
13
- - Automatically downloads and unpacks official binaries from [`lexiforest/curl-impersonate`](https://github.com/lexiforest/curl-impersonate) to `~/.http_mimic/bin`.
14
- - **Automatic platform & architecture detection** (macOS ARM/Intel, Linux x86_64/aarch64/musl, Windows x86_64/arm64, FreeBSD).
15
- - Zero configuration required—installs on first request automatically.
16
- - Manual driver management helpers: `HttpMimic.download_driver!`, `HttpMimic.driver_installed?`, `HttpMimic::Downloader.available_binaries`.
17
- - **Authentic Browser Handshakes & Fingerprints**:
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
- timeout = options[:timeout] || options[:read_timeout] || config.default_timeout
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
- connect_timeout = options[:connect_timeout] || config.default_connect_timeout
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 = 'chrome131'
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
@@ -82,13 +82,32 @@ module HttpMimic
82
82
 
83
83
  code = (status && status.success?) ? 200 : (status ? status.exitstatus : 500)
84
84
 
85
- raw_headers = "HTTP/2 200 OK\r\ncontent-type: text/html; charset=utf-8\r\nx-rendered-by: obscura\r\n\r\n"
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: status&.success? ? 'OK (Obscura SPA Rendered)' : 'Error',
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