http_mimic 0.5.1 โ†’ 0.5.3

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: 1b0b4192818428818cca7cde26b4ce90d6fa847db6e80e2b63c163dd8171bdd9
4
+ data.tar.gz: de6a71df66c710a30e7cb8fcbe6976653ec6242a6d29fd6959016513da4513be
5
5
  SHA512:
6
- metadata.gz: a4d17399d5f85df9b508151f371b0910e074d70e3c05f11cb291f30f01b71c20d22e998ec5c5466728c77dd2f37b3de4aecc18883a844fb69952ae71a3a5c5be
7
- data.tar.gz: 7b0675e724e1c138cd57ea4af5e5a6ce2f057bb50981271f9b81cb894bf7179c2ba760137f0672d742a63b602783bc6b455a71b9da4f778c0637db96ee723470
6
+ metadata.gz: 3b4ba6f1355da36e9f1064e62b1b988200f483a58b49524e3dd5350b0c99660d0b93ab650fc9ea8b3b7aa2204f41b7e2ca51c6b439128c267d7660be77b2b644
7
+ data.tar.gz: dd25b3fbf25dca0d935b910cb2b0981ab77d9dbfe45cfa5b522a99bbca7081016e6323af3fc59c389ef925c6df0b4d0bbf7321c7a93df5154d0d41e328962d76
data/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ 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.3] - 2026-09-05
9
+
10
+ ### Added
11
+ - **Automatic Free Proxy Pool (`auto_proxy`)**:
12
+ - Built-in `HttpMimic::ProxyPool` automatically gathers and caches hundreds of public HTTP proxies from maintained sources (`monosans`, `proxyscrape`, `TheSpeedX`).
13
+ - Disabled by default (`auto_proxy: false`) with per-request and global configuration opt-in.
14
+ - 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).
15
+ - Explicit manual `:proxy` option always takes precedence over `auto_proxy`.
16
+ - Seamless integration with Obscura and WAF solver handshakes.
17
+ - Added DSL helpers (`HttpMimic.auto_proxy`, `HttpMimic.proxy_pool`, `HttpMimic.refresh_proxies!`).
18
+ - **Streamlined Documentation**:
19
+ - Refined README with concise, high-level highlights focusing on core anti-detect capabilities and developer experience.
20
+
21
+ ## [0.5.2] - 2026-09-05
22
+
23
+ ### Added
24
+ - **Anti-Poisoning Failure Protection for CookieStore (`persist_on_failure`, `clear_on_failure`)**:
25
+ - `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.
26
+ - `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.
27
+ - Added class-level DSL and configuration support (`HttpMimic.persist_on_failure`, `HttpMimic.clear_on_failure`).
28
+ - **Two-Phase Telemetry Handshake in `AkamaiSolver`**:
29
+ - Automatic multi-round telemetry loop sending second-stage interaction sensor posts after initial cookie acquisition to flip `_abck` tokens from `~-1~` to verified `~0~`.
30
+ - **Intelligent Profile Fallback & WAF Protection**:
31
+ - `: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.
32
+ - Added `:firefox` and `:safari` profile support in `CommandBuilder`.
33
+ - Added Best Response Preservation: prevents successful/200 progress from being wiped out by later failed fallback attempts.
34
+
8
35
  ## [0.5.1] - 2026-08-30
9
36
 
10
37
  ### 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:
@@ -248,6 +248,10 @@ module HttpMimic
248
248
  (options[:ios_impersonate] || 'safari260_ios').to_s.downcase
249
249
  when :mobile
250
250
  (options[:mobile_impersonate] || 'chrome131_android').to_s.downcase
251
+ when :firefox
252
+ (options[:firefox_impersonate] || 'firefox135').to_s.downcase
253
+ when :safari
254
+ (options[:safari_impersonate] || 'safari180').to_s.downcase
251
255
  else
252
256
  (options[:impersonate] || config.default_impersonate).to_s.downcase
253
257
  end
@@ -42,10 +42,19 @@ 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
59
  @default_impersonate = 'chrome131'
51
60
  @default_mobile_impersonate = 'safari180_ios'
@@ -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
@@ -16,6 +16,16 @@ module HttpMimic
16
16
  def perform
17
17
  mode = (options[:mode] || config.mode || :auto).to_sym
18
18
 
19
+ # Automatic Free Proxy Pool integration
20
+ should_auto_proxy = options.fetch(:auto_proxy, config.auto_proxy)
21
+ if should_auto_proxy && !options[:proxy]
22
+ selected_proxy = ProxyPool.get
23
+ if selected_proxy
24
+ options[:proxy] = selected_proxy
25
+ log_debug("[HttpMimic::ProxyPool] Assigned proxy from pool: #{selected_proxy}")
26
+ end
27
+ end
28
+
19
29
  # Delegate directly to Obscura headless SPA renderer if requested
20
30
  if options[:render] == :spa || options[:render] == :obscura || mode == :spa || mode == :obscura
21
31
  return Obscura.render(url, options)
@@ -40,6 +50,8 @@ module HttpMimic
40
50
  attempts = []
41
51
  profiles_to_try = determine_profiles(mode, auto_fallback)
42
52
  waf_solve_attempted = false
53
+ best_response = nil
54
+ proxy_retries_left = should_auto_proxy ? (options[:proxy_retries] || config.proxy_retries || 3) : 0
43
55
 
44
56
  response = nil
45
57
  final_status = nil
@@ -47,6 +59,12 @@ module HttpMimic
47
59
  final_command = nil
48
60
 
49
61
  profiles_to_try.each_with_index do |profile, index|
62
+ # Never fall back to plain curl if a WAF challenge has already been detected/attempted
63
+ if profile == :curl && waf_solve_attempted
64
+ log_debug("[HttpMimic] Skipping plain :curl fallback for protected WAF target.")
65
+ next
66
+ end
67
+
50
68
  current_opts = options.merge(profile: profile)
51
69
 
52
70
  builder = CommandBuilder.new(method, url, current_opts, config)
@@ -61,6 +79,25 @@ module HttpMimic
61
79
  log_debug("Curl exit status: #{status.exitstatus}")
62
80
  log_debug("Curl stderr: #{stderr}") unless stderr.empty?
63
81
 
82
+ # Handle proxy failure retry when auto_proxy is enabled
83
+ if should_auto_proxy && current_opts[:proxy] && (status.exitstatus != 0)
84
+ is_proxy_err = [5, 7, 28, 35, 56].include?(status.exitstatus) ||
85
+ stderr.include?('Failed to connect to') ||
86
+ stderr.include?('tunneling failed') ||
87
+ stderr.downcase.include?('proxy')
88
+
89
+ if is_proxy_err
90
+ ProxyPool.mark_dead(current_opts[:proxy])
91
+ if proxy_retries_left > 0
92
+ proxy_retries_left -= 1
93
+ new_proxy = ProxyPool.get
94
+ log_debug("[HttpMimic::ProxyPool] Proxy #{current_opts[:proxy]} failed (exit #{status.exitstatus}). Retrying with new proxy #{new_proxy} (#{proxy_retries_left} retries left)...")
95
+ options[:proxy] = new_proxy
96
+ redo
97
+ end
98
+ end
99
+ end
100
+
64
101
  response = ResponseParser.new(
65
102
  stdout,
66
103
  exit_status: status,
@@ -69,6 +106,10 @@ module HttpMimic
69
106
  request_url: final_url
70
107
  ).parse
71
108
 
109
+ if should_auto_proxy && current_opts[:proxy] && response.success?
110
+ ProxyPool.mark_alive(current_opts[:proxy])
111
+ end
112
+
72
113
  response.mode_used = profile
73
114
  response.fallback_triggered = (index > 0)
74
115
 
@@ -86,6 +127,17 @@ module HttpMimic
86
127
  final_stderr = stderr
87
128
  final_command = full_command
88
129
 
130
+ # Track best response so a 200 (or challenge page) isn't wiped out by a failed fallback
131
+ if response
132
+ if best_response.nil?
133
+ best_response = response
134
+ elsif response.success? && !best_response.success?
135
+ best_response = response
136
+ elsif response.code == 200 && best_response.code != 200
137
+ best_response = response
138
+ end
139
+ end
140
+
89
141
  # Forward any received cookies to subsequent attempts
90
142
  if response.cookies && !response.cookies.empty?
91
143
  existing_cookies = options[:cookies] ? (options[:cookies].is_a?(Hash) ? options[:cookies] : options[:cookies].to_h) : {}
@@ -104,10 +156,13 @@ module HttpMimic
104
156
  solved_resp = Waf.solve(url, response, current_opts)
105
157
  if solved_resp
106
158
  response = solved_resp
107
- is_blocked = (response.code != 0 && retry_statuses.include?(response.code))
159
+ is_blocked = (response.code != 0 && retry_statuses.include?(response.code)) || Waf::Detector.challenge_page?(response)
108
160
  if response.cookies && !response.cookies.empty?
109
161
  options[:cookies] = (options[:cookies] || {}).merge(response.cookies.to_h)
110
162
  end
163
+ if response.success? || (response.code == 200 && best_response&.code != 200)
164
+ best_response = response
165
+ end
111
166
  end
112
167
  end
113
168
  end
@@ -119,33 +174,61 @@ module HttpMimic
119
174
  log_debug("[HttpMimic] Attempt #{index + 1} with #{profile} resulted in status #{response.code}. Triggering smart fallback to next profile...")
120
175
  end
121
176
 
122
- # Automatic SPA Detection & Obscura rendering fallback
177
+ # Preserve best response if the last attempt resulted in a regression (e.g. 403 / error after getting 200)
178
+ if best_response && (response.nil? || response.error? || (final_status && final_status.exitstatus != 0))
179
+ if best_response.success? || (best_response.code == 200 && response&.code != 200)
180
+ response = best_response
181
+ end
182
+ end
183
+
184
+ # Automatic SPA Detection & Obscura rendering fallback for SPA shells & Behavioral Challenges
123
185
  auto_render_spa = options.fetch(:auto_render_spa, config.auto_render_spa)
124
- if auto_render_spa && response && response.success? && method.to_s.upcase == 'GET'
125
- if SpaDetector.spa?(response)
126
- log_debug("[HttpMimic] Detected unhydrated SPA shell on #{url}. Automatically rendering with Obscura...")
127
- begin
128
- spa_opts = options.dup
129
- # Forward all validated cookies from Tier 1 (Mode 2: Two-Phase Pipeline)
130
- if response.cookies && !response.cookies.empty?
131
- tier1_cookies = response.cookies.to_h
132
- existing_cookies = spa_opts[:cookies].is_a?(Hash) ? spa_opts[:cookies] : {}
133
- spa_opts[:cookies] = existing_cookies.merge(tier1_cookies)
134
- end
135
- rendered_resp = Obscura.render(url, spa_opts)
136
- if rendered_resp && rendered_resp.success?
137
- response = rendered_resp
138
- end
139
- rescue StandardError => e
140
- log_debug("[HttpMimic] Automatic Obscura SPA render failed (#{e.message}), keeping Tier 1 response.")
186
+ auto_solve_waf = options.fetch(:solve_waf, config.auto_solve_waf)
187
+ should_render_spa = auto_render_spa && response && response.success? && SpaDetector.spa?(response)
188
+ should_render_cpt = auto_solve_waf && response && Waf::Detector.challenge_page?(response)
189
+
190
+ if (should_render_spa || should_render_cpt) && method.to_s.upcase == 'GET'
191
+ target_reason = should_render_cpt ? 'WAF challenge page' : 'unhydrated SPA shell'
192
+ log_debug("[HttpMimic] Detected #{target_reason} on #{url}. Automatically rendering with Obscura...")
193
+ begin
194
+ spa_opts = options.dup
195
+ # Forward all validated cookies from Tier 1 (Mode 2: Two-Phase Pipeline)
196
+ if response.cookies && !response.cookies.empty?
197
+ tier1_cookies = response.cookies.to_h
198
+ existing_cookies = spa_opts[:cookies].is_a?(Hash) ? spa_opts[:cookies] : {}
199
+ spa_opts[:cookies] = existing_cookies.merge(tier1_cookies)
200
+ end
201
+ spa_opts[:wait_until] ||= 'load' if should_render_cpt
202
+ rendered_resp = Obscura.render(url, spa_opts)
203
+ if rendered_resp && rendered_resp.success?
204
+ response = rendered_resp
141
205
  end
206
+ rescue StandardError => e
207
+ log_debug("[HttpMimic] Automatic Obscura render failed (#{e.message}), keeping Tier 1 response.")
142
208
  end
143
209
  end
144
210
 
145
211
  # Persist cookies back to store if enabled
146
- if should_persist_cookie && host && response && response.cookies && !response.cookies.empty?
147
- CookieStore.save(host, response.cookies)
148
- log_debug("[HttpMimic::CookieStore] Saved #{response.cookies.size} cookies for #{host}")
212
+ if should_persist_cookie && host
213
+ is_success = response && (response.success? || response.redirect?) && !Waf::Detector.challenge_page?(response) && (final_status.nil? || final_status.exitstatus == 0)
214
+ verification_failed = !is_success && response && (
215
+ [401, 403].include?(response.code) ||
216
+ retry_statuses.include?(response.code) ||
217
+ Waf::Detector.challenge_page?(response)
218
+ )
219
+
220
+ persist_on_failure = options.fetch(:persist_on_failure, config.persist_on_failure)
221
+ clear_on_failure = options.fetch(:clear_on_failure, config.clear_on_failure)
222
+
223
+ if is_success || persist_on_failure
224
+ if response && response.cookies && !response.cookies.empty?
225
+ CookieStore.save(host, response.cookies)
226
+ log_debug("[HttpMimic::CookieStore] Saved #{response.cookies.size} cookies for #{host}")
227
+ end
228
+ elsif clear_on_failure && verification_failed
229
+ CookieStore.clear(host)
230
+ log_debug("[HttpMimic::CookieStore] Verification failed for #{host} (status: #{response&.code}). Cleared stored cookies.")
231
+ end
149
232
  end
150
233
 
151
234
  handle_errors(final_status, final_stderr, final_command, response)
@@ -157,7 +240,7 @@ module HttpMimic
157
240
  def determine_profiles(mode, auto_fallback)
158
241
  case mode
159
242
  when :curl, :curl_first
160
- auto_fallback ? [:curl, :impersonate, :android, :ios] : [:curl]
243
+ auto_fallback ? [:curl, :impersonate, :android, :ios, :firefox] : [:curl]
161
244
  when :curl_only
162
245
  [:curl]
163
246
  when :impersonate_only
@@ -168,18 +251,22 @@ module HttpMimic
168
251
  [:android]
169
252
  when :ios_only
170
253
  [:ios]
254
+ when :firefox_only
255
+ [:firefox]
256
+ when :safari_only
257
+ [:safari]
171
258
  when :mobile_first
172
- auto_fallback ? [:android, :ios, :impersonate, :curl] : [:mobile]
259
+ auto_fallback ? [:android, :ios, :impersonate, :firefox] : [:mobile]
173
260
  when :android_first
174
- auto_fallback ? [:android, :ios, :impersonate, :curl] : [:android]
261
+ auto_fallback ? [:android, :ios, :impersonate, :firefox] : [:android]
175
262
  when :ios_first
176
- auto_fallback ? [:ios, :android, :impersonate, :curl] : [:ios]
263
+ auto_fallback ? [:ios, :android, :impersonate, :firefox] : [:ios]
177
264
  when :impersonate_first
178
- auto_fallback ? [:impersonate, :android, :ios, :curl] : [:impersonate]
265
+ auto_fallback ? [:impersonate, :android, :ios, :firefox] : [:impersonate]
179
266
  when :auto, :smart
180
- auto_fallback ? [:impersonate, :android, :ios, :curl] : [:impersonate]
267
+ auto_fallback ? [:impersonate, :android, :ios, :firefox] : [:impersonate]
181
268
  else
182
- auto_fallback ? [:impersonate, :android, :ios, :curl] : [:impersonate]
269
+ auto_fallback ? [:impersonate, :android, :ios, :firefox] : [:impersonate]
183
270
  end
184
271
  end
185
272
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HttpMimic
4
- VERSION = "0.5.1"
4
+ VERSION = "0.5.3"
5
5
  end
@@ -22,6 +22,7 @@ module HttpMimic
22
22
  impersonate: impersonate,
23
23
  cookies: cookies,
24
24
  headers: { 'Referer' => target_url },
25
+ proxy: options[:proxy],
25
26
  auto_fallback: false,
26
27
  solve_waf: false
27
28
  )
@@ -29,22 +30,81 @@ module HttpMimic
29
30
 
30
31
  sensor_js = script_resp.body
31
32
  context_js = File.read(CONTEXT_JS_PATH)
32
- cookie_str = cookies.to_cookie_string
33
33
 
34
34
  doc_title = initial_response.title.to_s
35
35
  user_agent = options[:user_agent]
36
36
  referer = (options[:headers] || {})['Referer'] || (options[:headers] || {})['referer'] || target_url
37
37
 
38
- # 2. Run virtual browser simulation inside QuickJS
39
- driver_script = <<~JS
40
- globalThis.__TARGET_URL__ = #{target_url.to_json};
41
- globalThis.__DOCUMENT_TITLE__ = #{doc_title.to_json};
42
- globalThis.__INITIAL_COOKIES__ = #{cookie_str.to_json};
43
- globalThis.__REFERRER__ = #{referer.to_json};
44
- #{user_agent ? "globalThis.__USER_AGENT__ = #{user_agent.to_json};" : ""}
38
+ # 2. Phase 1: Virtual browser simulation inside QuickJS (initial telemetry)
39
+ driver_script_1 = build_driver_script(
40
+ target_url: target_url,
41
+ doc_title: doc_title,
42
+ cookie_str: cookies.to_cookie_string,
43
+ referer: referer,
44
+ user_agent: user_agent,
45
+ context_js: context_js,
46
+ sensor_js: sensor_js,
47
+ phase: 1
48
+ )
49
+
50
+ result1 = JSRuntime.eval_json(driver_script_1)
51
+ sensor_posts_1 = result1 && result1['sensor_posts']
52
+ return nil if sensor_posts_1.nil? || sensor_posts_1.empty?
53
+
54
+ # 3. Post Phase 1 sensor data to Akamai endpoint
55
+ updated_cookies = cookies.dup
56
+ post_sensor_data(sensor_script_url, target_url, impersonate, updated_cookies, sensor_posts_1, proxy: options[:proxy])
57
+
58
+ # 4. Phase 2: If _abck is not verified yet (~0~), perform second round after short pause
59
+ if !verified_abck?(updated_cookies)
60
+ sleep 0.8
61
+ driver_script_2 = build_driver_script(
62
+ target_url: target_url,
63
+ doc_title: doc_title,
64
+ cookie_str: updated_cookies.to_cookie_string,
65
+ referer: referer,
66
+ user_agent: user_agent,
67
+ context_js: context_js,
68
+ sensor_js: sensor_js,
69
+ phase: 2
70
+ )
71
+
72
+ result2 = JSRuntime.eval_json(driver_script_2)
73
+ sensor_posts_2 = result2 && result2['sensor_posts']
74
+ if sensor_posts_2 && !sensor_posts_2.empty?
75
+ post_sensor_data(sensor_script_url, target_url, impersonate, updated_cookies, sensor_posts_2, proxy: options[:proxy])
76
+ end
77
+ end
78
+
79
+ # 5. Retry original target URL with the updated cookies
80
+ retry_headers = (options[:headers] || {}).merge('Referer' => target_url)
81
+ HttpMimic.get(
82
+ target_url,
83
+ options.merge(
84
+ cookies: updated_cookies,
85
+ headers: retry_headers,
86
+ auto_fallback: false,
87
+ solve_waf: false
88
+ )
89
+ )
90
+ rescue StandardError => e
91
+ if HttpMimic.configuration.debug
92
+ puts "[HttpMimic::Waf::AkamaiSolver] Error solving Akamai challenge: #{e.message}"
93
+ end
94
+ nil
95
+ end
96
+
97
+ def build_driver_script(params)
98
+ <<~JS
99
+ globalThis.__TARGET_URL__ = #{params[:target_url].to_json};
100
+ globalThis.__DOCUMENT_TITLE__ = #{params[:doc_title].to_json};
101
+ globalThis.__INITIAL_COOKIES__ = #{params[:cookie_str].to_json};
102
+ globalThis.__REFERRER__ = #{params[:referer].to_json};
103
+ #{params[:user_agent] ? "globalThis.__USER_AGENT__ = #{params[:user_agent].to_json};" : ""}
104
+ globalThis.__TELEMETRY_PHASE__ = #{params[:phase] || 1};
45
105
 
46
- #{context_js}
47
- #{sensor_js}
106
+ #{params[:context_js]}
107
+ #{params[:sensor_js]}
48
108
 
49
109
  if (typeof globalThis.__simulateHumanInteractions === "function") {
50
110
  globalThis.__simulateHumanInteractions();
@@ -57,63 +117,61 @@ module HttpMimic
57
117
  sensor_posts: globalThis.__sensor_posts
58
118
  });
59
119
  JS
120
+ end
60
121
 
61
- result = JSRuntime.eval_json(driver_script)
62
- sensor_posts = result && result['sensor_posts']
63
- return nil if sensor_posts.nil? || sensor_posts.empty?
64
-
65
- # 3. Post sensor data to Akamai endpoint
66
- updated_cookies = cookies.dup
122
+ def post_sensor_data(sensor_script_url, target_url, impersonate, updated_cookies, sensor_posts, proxy: nil)
123
+ origin_url = URI.parse(target_url).tap { |u| u.path = ''; u.query = nil }.to_s rescue target_url
67
124
  sensor_posts.each do |post|
68
125
  post_body = post['body']
69
126
  next if post_body.nil? || post_body.empty?
70
127
 
128
+ post_url = if post['url'].to_s.empty?
129
+ target_url
130
+ elsif post['url'].start_with?('http')
131
+ post['url']
132
+ else
133
+ URI.join(target_url, post['url']).to_s
134
+ end
135
+
136
+ content_type = (post['headers'] && (post['headers']['Content-Type'] || post['headers']['content-type'])) || 'application/json'
137
+
71
138
  post_resp = HttpMimic.post(
72
- sensor_script_url,
139
+ post_url,
73
140
  impersonate: impersonate,
74
141
  cookies: updated_cookies,
75
142
  headers: {
76
- 'Content-Type' => 'text/plain;charset=UTF-8',
143
+ 'Content-Type' => content_type,
77
144
  'Referer' => target_url,
78
- 'Origin' => URI.parse(target_url).tap { |u| u.path = ''; u.query = nil }.to_s,
145
+ 'Origin' => origin_url,
79
146
  'Accept' => '*/*'
80
147
  },
148
+ proxy: proxy,
81
149
  body: post_body,
82
150
  auto_fallback: false,
83
151
  solve_waf: false
84
152
  )
85
153
 
86
- post_resp.cookies.each { |k, v| updated_cookies[k] = v }
154
+ post_resp.cookies.each { |k, v| updated_cookies[k] = v } if post_resp&.cookies
87
155
  end
156
+ end
88
157
 
89
- # 4. Retry original target URL with the updated cookies
90
- retry_headers = (options[:headers] || {}).merge('Referer' => target_url)
91
- HttpMimic.get(
92
- target_url,
93
- options.merge(
94
- cookies: updated_cookies,
95
- headers: retry_headers,
96
- auto_fallback: false,
97
- solve_waf: false
98
- )
99
- )
100
- rescue StandardError => e
101
- if HttpMimic.configuration.debug
102
- puts "[HttpMimic::Waf::AkamaiSolver] Error solving Akamai challenge: #{e.message}"
103
- end
104
- nil
158
+ def verified_abck?(cookies)
159
+ return false unless cookies
160
+ abck = cookies['_abck'] || (cookies.respond_to?(:key?) && cookies['_abck'])
161
+ abck.to_s.include?('~0~')
105
162
  end
106
163
 
107
164
  def extract_sensor_script_url(target_url, html)
108
165
  return nil if html.nil? || html.empty?
109
166
 
110
- match = html.match(/<script[^>]*src=["\x27]([^"\x27]*\/[a-zA-Z0-9_-]{10,}\?[a-zA-Z0-9_=-]+)["\x27]/i)
167
+ match = html.match(/<script[^>]*src=["\x27]([^"\x27]*\/[a-zA-Z0-9_\-\/]+\?[^"\x27\s>]+)["\x27]/i)
111
168
  match ||= html.match(/<script[^>]*src=["\x27]([^"\x27]*akam[^"\x27]*)["\x27]/i)
112
- match ||= html.match(/<script[^>]*src=["\x27]([^"\x27]*\/[a-zA-Z0-9_\-\/]+\?v=[a-zA-Z0-9_-]+)["\x27]/i)
169
+ match ||= html.match(/<script[^>]*src=["\x27]([^"\x27]*\/[a-zA-Z0-9_-]{10,}\?[a-zA-Z0-9_=-]+)["\x27]/i)
113
170
 
114
171
  return nil unless match
115
172
 
116
- URI.join(target_url, match[1]).to_s
173
+ raw_url = match[1].gsub('&amp;', '&')
174
+ URI.join(target_url, raw_url).to_s
117
175
  rescue StandardError
118
176
  nil
119
177
  end
data/lib/http_mimic.rb CHANGED
@@ -21,6 +21,7 @@ require 'http_mimic/waf'
21
21
  require 'http_mimic/cookie_store'
22
22
  require 'http_mimic/obscura'
23
23
  require 'http_mimic/spa_detector'
24
+ require 'http_mimic/proxy_pool'
24
25
 
25
26
  module HttpMimic
26
27
  extend ModuleMethods
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http_mimic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - anxgang
@@ -73,6 +73,7 @@ files:
73
73
  - lib/http_mimic/js_runtime.rb
74
74
  - lib/http_mimic/module_methods.rb
75
75
  - lib/http_mimic/obscura.rb
76
+ - lib/http_mimic/proxy_pool.rb
76
77
  - lib/http_mimic/request.rb
77
78
  - lib/http_mimic/response.rb
78
79
  - lib/http_mimic/response_parser.rb