http_mimic 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11e55edf4c06160f27621a651e4c16eae929ec08767b5b6eff6b007d8a3a1d4b
4
- data.tar.gz: e90137a02ee867cd744641c725cd3daef2994b5f4a7216304f7ca189e6d92303
3
+ metadata.gz: 9b0a9fff1b74c98ea24c294a491b00ba57e270d24e6c0bb7a669e6d2db264d8a
4
+ data.tar.gz: e11d3ae615b92abcfedda8547120290d82020acd82230353a127cc97dfc844f6
5
5
  SHA512:
6
- metadata.gz: 77dfc30ee87e63105f5cbf95e818cc4b01841f9862566fdceedad277cb22366c05c37ab14b3ac699ddb30d68a9ead35acc43114a1f882c034360d06807450677
7
- data.tar.gz: 4ab621140feebc4c3f9e8897d33edf510785ea6c5cbad5cf08b672ecfde52f6146b6f1a3d695f5129acce0d3c7b64cfdae275803480178500e9c683fb7c053a8
6
+ metadata.gz: 5e0a93bb67bb84582e24df07ea4d51d291a53f0b48ab77ad0bd4a63b666bede4b493bbb405ad34d76602e086eafd76a455668c2a491488d83dd2c0be1060fe66
7
+ data.tar.gz: c616cafefeb23d78af7320beab5ddc3b8f79aafda440fd1fac889f5a5990417b31716105e8c74cab566e6c0ed6eca04a82a397ddc4242eb59caed519e52739de
data/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-08-30
9
+
10
+ ### Added
11
+ - **Target-Specific Navigation Headers & Client Hints Simulation**:
12
+ - Automatically generates browser-accurate Navigation Headers (`sec-fetch-dest`, `sec-fetch-mode`, `sec-fetch-site`, `upgrade-insecure-requests`) and Client Hints (`sec-ch-ua`, `sec-ch-ua-mobile`, `sec-ch-ua-platform`) tailored to the selected impersonation target.
13
+ - Firefox (`firefox*`, `ff*`), Safari / iOS (`safari*`, `ios*`), and Tor targets strictly omit Chromium-only `sec-ch-ua*` headers to avoid anti-bot fingerprint anomalies and detection.
14
+ - Android Chrome targets properly send `sec-ch-ua-mobile: ?1` and `sec-ch-ua-platform: "Android"`.
15
+ - Edge targets properly send `"Microsoft Edge"` brand headers with `"Windows"` platform.
16
+ - **Unified Persistent CookieStore (`persist_cookies`)**:
17
+ - Added `:persist_cookies` request option and `persist_cookies` class-level DSL method to automatically load and persist session cookies across requests per host into `~/.http_mimic/cookies/`.
18
+ - Added helper methods: `HttpMimic.load_cookies(host, max_age: nil)`, `HttpMimic.save_cookies(host, cookies, ttl: nil)`, and `HttpMimic.clear_cookies!(host = nil)`.
19
+ - **WAF Challenge Solving & Fallback Performance Optimizations**:
20
+ - Added single-pass WAF solver guard (`waf_solve_attempted`) to prevent redundant JavaScript solver executions across fallback attempts.
21
+ - Added cross-attempt cookie accumulation to preserve session state between fallback profiles.
22
+ - Added `HttpMimic::Waf::Detector.challenge_page?` to detect interstitial challenges disguised under HTTP 200 responses.
23
+
8
24
  ## [0.3.2] - 2026-08-29
9
25
 
10
26
  ### Added
data/README.md CHANGED
@@ -21,7 +21,7 @@ It also includes **Webdrivers-like automatic driver management**, automatically
21
21
  - Edge & Tor support: `edge101`, `edge99`, `tor145`.
22
22
  - **HTTParty-Style API**:
23
23
  - Direct module methods: `HttpMimic.get`, `HttpMimic.post`, etc.
24
- - Class mixin via `include HttpMimic` (`base_uri`, `headers`, `default_params`, `default_timeout`, `impersonate`, `proxy`, `cookies`).
24
+ - Class mixin via `include HttpMimic` (`base_uri`, `headers`, `default_params`, `default_timeout`, `impersonate`, `proxy`, `cookies`, `persist_cookies`).
25
25
  - Reusable instance client: `HttpMimic::Client.new(...)`.
26
26
  - **Zero Shell Injection Risk**:
27
27
  - Executes commands with array arguments via `Open3.capture3(*cmd_array)`.
@@ -186,6 +186,40 @@ HttpMimic.get(images.first).save('samba_og.jpg')
186
186
 
187
187
  ---
188
188
 
189
+ ## 🍪 Persistent Host CookieStore
190
+
191
+ `HttpMimic` supports persistent host-based cookie caching. Once enabled, session and verification cookies are automatically saved to `~/.http_mimic/cookies/<host>.json` and reused in subsequent requests to the same host:
192
+
193
+ ```ruby
194
+ # 1. Enable per-request
195
+ response = HttpMimic.get('https://example.com/items', persist_cookies: true)
196
+
197
+ # 2. Or enable in class mixin
198
+ class Scraper
199
+ include HttpMimic
200
+ persist_cookies true
201
+ end
202
+
203
+ # 3. Manual CookieStore helpers
204
+ HttpMimic.load_cookies('example.com') # => Hash of active cookies
205
+ HttpMimic.save_cookies('example.com', { session: '123' }) # Save cookies with default TTL
206
+ HttpMimic.clear_cookies!('example.com') # Clear host or all cookies
207
+ ```
208
+
209
+ ---
210
+
211
+ ## 🧭 Target-Specific Navigation Headers & Client Hints
212
+
213
+ `HttpMimic` automatically constructs browser-accurate HTTP navigation headers on `GET` and `HEAD` requests:
214
+
215
+ - **Chrome Desktop / Mobile**: Generates `sec-ch-ua`, `sec-ch-ua-mobile`, `sec-ch-ua-platform`, and standard Chrome `Accept` headers.
216
+ - **Firefox & Safari / iOS**: **Strictly omits** Chromium-only `sec-ch-ua*` headers to prevent anti-bot detection and fingerprint mismatch anomalies.
217
+ - **Edge**: Generates Microsoft Edge brand `sec-ch-ua` headers.
218
+
219
+ Can be disabled globally with `config.navigation_headers = false` or per request via `navigation_headers: false`.
220
+
221
+ ---
222
+
189
223
  ## ⚙️ Global Configuration
190
224
 
191
225
  Configure global defaults in an initializer (e.g., `config/initializers/http_mimic.rb`):
@@ -207,6 +241,16 @@ HttpMimic.configure do |config|
207
241
  config.raise_on_error = false # Raise exceptions on HTTP errors / non-zero exits
208
242
  config.debug = false # Print debug logs
209
243
 
244
+ # Navigation headers & Client Hints simulation (enabled by default)
245
+ config.navigation_headers = true # Browser-accurate sec-ch-ua, sec-fetch-*, Accept headers
246
+
247
+ # Host CookieStore Persistence (disabled by default)
248
+ config.persist_cookies = false # Auto-persist cookies per host
249
+ config.cookie_store_dir = File.expand_path('~/.http_mimic/cookies') # Directory for cookie store
250
+
251
+ # WAF challenge solving (enabled by default)
252
+ config.auto_solve_waf = true # Automatically solve detected WAF JS challenges
253
+
210
254
  # Webdrivers-like auto-download settings (enabled by default)
211
255
  config.auto_download = true # Auto-download missing binary
212
256
  config.driver_version = 'v2.1.1' # Target release version
@@ -225,12 +269,15 @@ end
225
269
  | `:auto_fallback` | Boolean | Whether to automatically retry with alternative profile on blocked status (default `true`) |
226
270
  | `:retry_statuses`| Array | Status codes that trigger auto-fallback (default `[403, 429, 503]`) |
227
271
  | `:impersonate` | String | Target browser to mimic (e.g., `'chrome131'`, `'chrome120'`, `'firefox135'`, `'safari180'`, `'tor145'`) |
272
+ | `:navigation_headers` | Boolean | Automatically generate browser-accurate navigation headers (`sec-ch-ua`, `sec-fetch-*`, `Accept`) (default `true`) |
273
+ | `:solve_waf` | Boolean | Automatically detect and solve WAF challenges (e.g. Akamai) via embedded QuickJS (default `true`) |
228
274
  | `:binary` | String | Path to a custom `curl-impersonate` executable |
229
275
  | `:query` / `:params` | Hash | URL query parameters (supports nested parameters and encoding) |
230
276
  | `:headers` | Hash | Custom HTTP request headers |
231
277
  | `:json` | Hash / Array | Serialized to JSON with `Content-Type: application/json` |
232
278
  | `:body` | Hash / String | Form payload (Hash) or raw request body string |
233
279
  | `:cookies` | Hash / String | Request cookies |
280
+ | `:persist_cookies` | Boolean | Automatically load and save cookies for the host across requests |
234
281
  | `:cookie_jar` | String | Path to save cookies (`-c`) |
235
282
  | `:cookie_file` | String | Path to read cookies (`-b`) |
236
283
  | `:timeout` | Integer / Float | Maximum execution timeout in seconds (`--max-time`) |
@@ -208,6 +208,13 @@ module HttpMimic
208
208
  headers_to_send['Accept'] ||= '*/*'
209
209
  headers_to_send['Accept-Encoding'] ||= 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3'
210
210
  headers_to_send['Connection'] ||= 'close'
211
+ else
212
+ # Automatic Navigation Headers for browser profiles on GET/HEAD
213
+ nav_headers_enabled = options.fetch(:navigation_headers, config.navigation_headers)
214
+ if nav_headers_enabled && %i[get head].include?(method.to_s.downcase.to_sym)
215
+ target = resolve_target
216
+ apply_navigation_headers(headers_to_send, target)
217
+ end
211
218
  end
212
219
 
213
220
  # Append headers
@@ -232,7 +239,20 @@ module HttpMimic
232
239
  [binary, args, stdin_data, final_url]
233
240
  end
234
241
 
235
- def resolve_binary
242
+ def resolve_target
243
+ case options[:profile]
244
+ when :android
245
+ (options[:android_impersonate] || 'chrome131_android').to_s.downcase
246
+ when :ios
247
+ (options[:ios_impersonate] || 'safari260_ios').to_s.downcase
248
+ when :mobile
249
+ (options[:mobile_impersonate] || 'chrome131_android').to_s.downcase
250
+ else
251
+ (options[:impersonate] || config.default_impersonate).to_s.downcase
252
+ end
253
+ end
254
+
255
+ def resolve_binary(target = resolve_target)
236
256
  # 0. If profile is explicitly set to :curl, use system curl
237
257
  if options[:profile] == :curl
238
258
  curl_path = find_executable('curl') || 'curl'
@@ -248,16 +268,6 @@ module HttpMimic
248
268
  raise BinaryNotFoundError, "Specified executable does not exist or is not executable: #{explicit}"
249
269
  end
250
270
 
251
- target = case options[:profile]
252
- when :android
253
- (options[:android_impersonate] || 'chrome131_android').to_s.downcase
254
- when :ios
255
- (options[:ios_impersonate] || 'safari260_ios').to_s.downcase
256
- when :mobile
257
- (options[:mobile_impersonate] || 'chrome131_android').to_s.downcase
258
- else
259
- (options[:impersonate] || config.default_impersonate).to_s.downcase
260
- end
261
271
  candidate_names = TARGET_BINARY_MAP[target] || ["curl_#{target}", target]
262
272
 
263
273
  # 2. Check local installation directory (~/.http_mimic/bin)
@@ -304,6 +314,81 @@ module HttpMimic
304
314
 
305
315
  private
306
316
 
317
+ def apply_navigation_headers(headers_to_send, target)
318
+ target_str = target.to_s.downcase
319
+
320
+ if target_str.start_with?('firefox', 'ff')
321
+ # Firefox (Gecko): Mozilla does not send Client Hints (sec-ch-ua*)
322
+ headers_to_send['sec-fetch-dest'] ||= 'document'
323
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
324
+ headers_to_send['sec-fetch-site'] ||= 'none'
325
+ headers_to_send['sec-fetch-user'] ||= '?1'
326
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
327
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
328
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.5'
329
+
330
+ elsif target_str.start_with?('safari') || target_str == 'ios' || target_str == 'safari_ios' || target_str.include?('_ios')
331
+ # Safari / WebKit (Desktop & iOS): WebKit does not send Client Hints (sec-ch-ua*)
332
+ headers_to_send['sec-fetch-dest'] ||= 'document'
333
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
334
+ headers_to_send['sec-fetch-site'] ||= 'none'
335
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
336
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
337
+
338
+ elsif target_str.start_with?('tor')
339
+ # Tor Browser: No sec-ch-ua headers
340
+ headers_to_send['sec-fetch-dest'] ||= 'document'
341
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
342
+ headers_to_send['sec-fetch-site'] ||= 'none'
343
+ headers_to_send['sec-fetch-user'] ||= '?1'
344
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
345
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
346
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.5'
347
+
348
+ elsif target_str.start_with?('edge')
349
+ # Edge (Chromium): Microsoft Edge sec-ch-ua
350
+ ver = target_str[/\d+/] || '101'
351
+ headers_to_send['sec-ch-ua'] ||= "\"Microsoft Edge\";v=\"#{ver}\", \"Chromium\";v=\"#{ver}\", \"Not_A Brand\";v=\"24\""
352
+ headers_to_send['sec-ch-ua-mobile'] ||= '?0'
353
+ headers_to_send['sec-ch-ua-platform'] ||= '"Windows"'
354
+ headers_to_send['sec-fetch-dest'] ||= 'document'
355
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
356
+ headers_to_send['sec-fetch-site'] ||= 'none'
357
+ headers_to_send['sec-fetch-user'] ||= '?1'
358
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
359
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'
360
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
361
+
362
+ elsif target_str.include?('android')
363
+ # Android Chrome
364
+ ver = target_str[/\d+/] || '131'
365
+ headers_to_send['sec-ch-ua'] ||= "\"Chromium\";v=\"#{ver}\", \"Not_A Brand\";v=\"24\", \"Google Chrome\";v=\"#{ver}\""
366
+ headers_to_send['sec-ch-ua-mobile'] ||= '?1'
367
+ headers_to_send['sec-ch-ua-platform'] ||= '"Android"'
368
+ headers_to_send['sec-fetch-dest'] ||= 'document'
369
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
370
+ headers_to_send['sec-fetch-site'] ||= 'none'
371
+ headers_to_send['sec-fetch-user'] ||= '?1'
372
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
373
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'
374
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
375
+
376
+ else
377
+ # Chrome Desktop (Default)
378
+ ver = target_str[/\d+/] || '131'
379
+ headers_to_send['sec-ch-ua'] ||= "\"Google Chrome\";v=\"#{ver}\", \"Chromium\";v=\"#{ver}\", \"Not_A Brand\";v=\"24\""
380
+ headers_to_send['sec-ch-ua-mobile'] ||= '?0'
381
+ headers_to_send['sec-ch-ua-platform'] ||= '"macOS"'
382
+ headers_to_send['sec-fetch-dest'] ||= 'document'
383
+ headers_to_send['sec-fetch-mode'] ||= 'navigate'
384
+ headers_to_send['sec-fetch-site'] ||= 'none'
385
+ headers_to_send['sec-fetch-user'] ||= '?1'
386
+ headers_to_send['upgrade-insecure-requests'] ||= '1'
387
+ headers_to_send['accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'
388
+ headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
389
+ end
390
+ end
391
+
307
392
  def find_in_download_dir(candidates)
308
393
  target_dir = File.expand_path(config.install_dir)
309
394
  return nil unless Dir.exist?(target_dir)
@@ -26,6 +26,20 @@ module HttpMimic
26
26
  attr_accessor :install_dir
27
27
  attr_accessor :github_repo
28
28
 
29
+ # QuickJS standalone binary management settings
30
+ attr_accessor :qjs_version
31
+ attr_accessor :qjs_github_repo
32
+
33
+ # WAF & JS challenge solving settings
34
+ attr_accessor :auto_solve_waf
35
+
36
+ # Shared Host-based CookieStore settings
37
+ attr_accessor :persist_cookies
38
+ attr_accessor :cookie_store_dir
39
+
40
+ # Modern Navigation Headers simulation
41
+ attr_accessor :navigation_headers
42
+
29
43
  def initialize
30
44
  @default_impersonate = 'chrome131'
31
45
  @default_mobile_impersonate = 'safari180_ios'
@@ -45,10 +59,24 @@ module HttpMimic
45
59
  @retry_statuses = [403, 429, 503]
46
60
 
47
61
  # Enable automatic downloading of curl-impersonate driver (lexiforest) by default
48
- @auto_download = true
49
- @driver_version = 'v2.1.1'
50
- @install_dir = File.expand_path('~/.http_mimic/bin')
51
- @github_repo = 'lexiforest/curl-impersonate'
62
+ @auto_download = true
63
+ @driver_version = 'v2.1.1'
64
+ @install_dir = File.expand_path('~/.http_mimic/bin')
65
+ @github_repo = 'lexiforest/curl-impersonate'
66
+
67
+ # QuickJS defaults
68
+ @qjs_version = 'v0.16.2'
69
+ @qjs_github_repo = 'quickjs-ng/quickjs'
70
+
71
+ # WAF challenge solving defaults
72
+ @auto_solve_waf = true
73
+
74
+ # Shared Host CookieStore defaults
75
+ @persist_cookies = false
76
+ @cookie_store_dir = File.expand_path('~/.http_mimic/cookies')
77
+
78
+ # Navigation Headers defaults
79
+ @navigation_headers = true
52
80
  end
53
81
  end
54
82
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module HttpMimic
8
+ class CookieStore
9
+ DEFAULT_TTL = 7200 # 2 hours
10
+
11
+ class << self
12
+ def store_dir
13
+ File.expand_path(HttpMimic.configuration.cookie_store_dir || '~/.http_mimic/cookies')
14
+ end
15
+
16
+ def cookie_file_for(host)
17
+ clean_host = sanitize_host(host)
18
+ File.join(store_dir, "#{clean_host}.json")
19
+ end
20
+
21
+ # Load cookies for a given host
22
+ # Returns a Hash of cookies if valid and not expired, or empty Hash
23
+ def load(host, max_age: DEFAULT_TTL)
24
+ file = cookie_file_for(host)
25
+ return {} unless File.file?(file)
26
+
27
+ data = nil
28
+ File.open(file, File::RDONLY) do |f|
29
+ f.flock(File::LOCK_SH)
30
+ data = JSON.parse(f.read) rescue nil
31
+ end
32
+
33
+ return {} unless data.is_a?(Hash)
34
+
35
+ updated_at = data['updated_at'].to_i
36
+ if !max_age.nil?
37
+ return {} if (Time.now.to_i - updated_at) > max_age
38
+ end
39
+
40
+ data['cookies'] || {}
41
+ rescue StandardError => e
42
+ if HttpMimic.configuration.debug
43
+ puts "[HttpMimic::CookieStore] Error loading cookies for #{host}: #{e.message}"
44
+ end
45
+ {}
46
+ end
47
+
48
+ # Save cookies for a given host with atomic locking
49
+ def save(host, cookies, ttl: DEFAULT_TTL)
50
+ return if cookies.nil? || cookies.empty?
51
+
52
+ clean_host = sanitize_host(host)
53
+ dir = store_dir
54
+ FileUtils.mkdir_p(dir)
55
+
56
+ file = cookie_file_for(clean_host)
57
+ existing_cookies = load(clean_host, max_age: ttl) || {}
58
+
59
+ new_cookies = cookies.is_a?(Cookies) ? cookies.to_h : cookies.dup
60
+ merged = existing_cookies.merge(stringify_keys(new_cookies))
61
+
62
+ payload = {
63
+ 'host' => clean_host,
64
+ 'updated_at' => Time.now.to_i,
65
+ 'ttl' => ttl,
66
+ 'cookies' => merged
67
+ }
68
+
69
+ # Thread & Process safe atomic write
70
+ tmp_file = "#{file}.tmp.#{Process.pid}_#{Time.now.to_f}"
71
+ File.open(tmp_file, File::RDWR | File::CREAT, 0644) do |f|
72
+ f.flock(File::LOCK_EX)
73
+ f.write(JSON.pretty_generate(payload))
74
+ f.flush
75
+ end
76
+ File.rename(tmp_file, file)
77
+ rescue StandardError => e
78
+ FileUtils.rm_f(tmp_file) if tmp_file
79
+ if HttpMimic.configuration.debug
80
+ puts "[HttpMimic::CookieStore] Error saving cookies for #{host}: #{e.message}"
81
+ end
82
+ end
83
+
84
+ # Clear stored cookies for a host or all hosts
85
+ def clear(host = nil)
86
+ if host
87
+ file = cookie_file_for(host)
88
+ FileUtils.rm_f(file)
89
+ else
90
+ FileUtils.rm_rf(store_dir)
91
+ end
92
+ end
93
+
94
+ # Extract host from URL or host string
95
+ def extract_host(url_or_host)
96
+ str = url_or_host.to_s.strip
97
+ if str =~ %r{^https?://}i
98
+ URI.parse(str).host
99
+ else
100
+ str.split(':').first
101
+ end
102
+ rescue StandardError
103
+ str
104
+ end
105
+
106
+ private
107
+
108
+ def sanitize_host(host)
109
+ h = extract_host(host) || 'default'
110
+ h.gsub(/[^a-zA-Z0-9.\-_]/, '_')
111
+ end
112
+
113
+ def stringify_keys(hash)
114
+ hash.each_with_object({}) { |(k, v), h| h[k.to_s] = v.to_s }
115
+ end
116
+ end
117
+ end
118
+ end
@@ -47,6 +47,44 @@ module HttpMimic
47
47
  @store.values
48
48
  end
49
49
 
50
+ def empty?
51
+ @store.empty?
52
+ end
53
+
54
+ def size
55
+ @store.size
56
+ end
57
+ alias length size
58
+
59
+ def delete(name)
60
+ @cookie_items.delete(name.to_s)
61
+ @store.delete(name.to_s)
62
+ end
63
+
64
+ def clear
65
+ @cookie_items.clear
66
+ @store.clear
67
+ end
68
+
69
+ def merge(other)
70
+ dup_cookies = self.class.new(@store)
71
+ if other.is_a?(Cookies)
72
+ other.each { |k, v| dup_cookies[k] = v }
73
+ elsif other.is_a?(Hash)
74
+ other.each { |k, v| dup_cookies[k] = v }
75
+ end
76
+ dup_cookies
77
+ end
78
+
79
+ def merge!(other)
80
+ if other.is_a?(Cookies)
81
+ other.each { |k, v| self[k] = v }
82
+ elsif other.is_a?(Hash)
83
+ other.each { |k, v| self[k] = v }
84
+ end
85
+ self
86
+ end
87
+
50
88
  def to_h
51
89
  @store.dup
52
90
  end
@@ -13,6 +13,9 @@ module HttpMimic
13
13
  DEFAULT_GITHUB_REPO = 'lexiforest/curl-impersonate'
14
14
  DEFAULT_VERSION = 'v2.1.1'
15
15
 
16
+ DEFAULT_QJS_REPO = 'quickjs-ng/quickjs'
17
+ DEFAULT_QJS_VERSION = 'v0.16.2'
18
+
16
19
  class DownloadError < HttpMimic::Error; end
17
20
  class UnsupportedPlatformError < HttpMimic::Error; end
18
21
 
@@ -63,6 +66,105 @@ module HttpMimic
63
66
  true
64
67
  end
65
68
 
69
+ def download_qjs!(version: nil, install_dir: nil, repo: nil, force: false)
70
+ target_version = normalize_version(version || HttpMimic.configuration.qjs_version || DEFAULT_QJS_VERSION)
71
+ target_dir = File.expand_path(install_dir || HttpMimic.configuration.install_dir)
72
+ target_repo = repo || HttpMimic.configuration.qjs_github_repo || DEFAULT_QJS_REPO
73
+
74
+ FileUtils.mkdir_p(target_dir)
75
+
76
+ dest_binary = File.join(target_dir, binary_name_for_platform('qjs'))
77
+
78
+ if !force && qjs_installed?(version: target_version, install_dir: target_dir)
79
+ log_info("qjs #{target_version} is already installed in #{target_dir}")
80
+ return dest_binary
81
+ end
82
+
83
+ asset_name = qjs_platform_asset
84
+ download_url = "https://github.com/#{target_repo}/releases/download/#{target_version}/#{asset_name}"
85
+
86
+ log_info("Downloading QuickJS (#{target_version}) [#{asset_name}]...")
87
+ binary_data = fetch_binary(download_url)
88
+
89
+ File.binwrite(dest_binary, binary_data)
90
+ File.chmod(0755, dest_binary)
91
+
92
+ File.write(qjs_version_file_path(target_dir), target_version)
93
+
94
+ log_info("QuickJS #{target_version} installation complete! (#{dest_binary})")
95
+ dest_binary
96
+ end
97
+
98
+ def qjs_installed?(version: nil, install_dir: nil)
99
+ target_dir = File.expand_path(install_dir || HttpMimic.configuration.install_dir)
100
+ qjs_bin = File.join(target_dir, binary_name_for_platform('qjs'))
101
+ return false unless File.file?(qjs_bin) && File.executable?(qjs_bin)
102
+
103
+ if version
104
+ target_version = normalize_version(version)
105
+ v_file = qjs_version_file_path(target_dir)
106
+ return false unless File.file?(v_file)
107
+ return File.read(v_file).strip == target_version
108
+ end
109
+
110
+ true
111
+ end
112
+
113
+ def qjs_path(install_dir: nil)
114
+ target_dir = File.expand_path(install_dir || HttpMimic.configuration.install_dir)
115
+ candidate = File.join(target_dir, binary_name_for_platform('qjs'))
116
+ return candidate if File.file?(candidate) && File.executable?(candidate)
117
+
118
+ # Fallback to system PATH
119
+ sys_qjs = `which qjs 2>/dev/null`.strip
120
+ return sys_qjs if !sys_qjs.empty? && File.executable?(sys_qjs)
121
+
122
+ nil
123
+ end
124
+
125
+ def qjs_platform_asset
126
+ os = host_os
127
+ cpu = host_cpu
128
+
129
+ case os
130
+ when :macos
131
+ case cpu
132
+ when :arm64 then 'qjs-darwin-arm64'
133
+ when :x86_64 then 'qjs-darwin-x86_64'
134
+ else
135
+ raise UnsupportedPlatformError, "Unsupported macOS CPU architecture for QuickJS: #{cpu}"
136
+ end
137
+ when :linux
138
+ case cpu
139
+ when :x86_64
140
+ 'qjs-linux-x86_64'
141
+ when :aarch64, :arm64
142
+ 'qjs-linux-aarch64'
143
+ when :arm
144
+ 'qjs-linux-armv7'
145
+ when :i386, :i686
146
+ 'qjs-linux-x86'
147
+ when :riscv64
148
+ 'qjs-linux-riscv64'
149
+ else
150
+ raise UnsupportedPlatformError, "Unsupported Linux CPU architecture for QuickJS: #{cpu}"
151
+ end
152
+ when :windows
153
+ case cpu
154
+ when :x86_64 then 'qjs-windows-x86_64.exe'
155
+ when :i386, :i686 then 'qjs-windows-x86.exe'
156
+ else
157
+ raise UnsupportedPlatformError, "Unsupported Windows CPU architecture for QuickJS: #{cpu}"
158
+ end
159
+ else
160
+ raise UnsupportedPlatformError, "Unsupported operating system for QuickJS: #{RbConfig::CONFIG['host_os']}"
161
+ end
162
+ end
163
+
164
+ def qjs_version_file_path(dir)
165
+ File.join(dir, '.qjs_version')
166
+ end
167
+
66
168
  def binary_path(name, install_dir: nil)
67
169
  target_dir = File.expand_path(install_dir || HttpMimic.configuration.install_dir)
68
170
  candidate = File.join(target_dir, binary_name_for_platform(name))
@@ -31,4 +31,19 @@ module HttpMimic
31
31
 
32
32
  # Raised when response parsing fails
33
33
  class ResponseParseError < Error; end
34
+
35
+ # Raised when JavaScript execution fails in JSRuntime
36
+ class JSError < Error
37
+ attr_reader :stderr, :exit_code
38
+
39
+ def initialize(message, stderr: nil, exit_code: nil)
40
+ super(message)
41
+ @stderr = stderr
42
+ @exit_code = exit_code
43
+ end
44
+ end
45
+
46
+ # Raised when JavaScript execution times out
47
+ class JSTimeoutError < JSError; end
34
48
  end
49
+
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'json'
5
+ require 'timeout'
6
+
7
+ module HttpMimic
8
+ class JSRuntime
9
+ STDIN_RUNNER = <<~JS
10
+ const input = std.in.readAsString();
11
+ const result = eval(input);
12
+ if (result !== undefined) {
13
+ if (typeof result === "object" && result !== null) {
14
+ console.log(JSON.stringify(result));
15
+ } else {
16
+ console.log(result);
17
+ }
18
+ }
19
+ JS
20
+
21
+ class << self
22
+ def eval(js_code, timeout: nil, install_dir: nil, auto_download: nil)
23
+ ensure_qjs_installed!(install_dir: install_dir, auto_download: auto_download)
24
+
25
+ qjs_bin = Downloader.qjs_path(install_dir: install_dir)
26
+ raise BinaryNotFoundError, "QuickJS binary 'qjs' not found. Run HttpMimic.download_qjs! to install." unless qjs_bin
27
+
28
+ cmd = [qjs_bin, '--std', '-e', STDIN_RUNNER]
29
+ effective_timeout = timeout || HttpMimic.configuration.default_timeout
30
+
31
+ stdout = nil
32
+ stderr = nil
33
+ status = nil
34
+
35
+ begin
36
+ if effective_timeout && effective_timeout > 0
37
+ Timeout.timeout(effective_timeout) do
38
+ stdout, stderr, status = Open3.capture3(*cmd, stdin_data: js_code.to_s)
39
+ end
40
+ else
41
+ stdout, stderr, status = Open3.capture3(*cmd, stdin_data: js_code.to_s)
42
+ end
43
+ rescue Timeout::Error
44
+ raise JSTimeoutError.new("JavaScript execution timed out after #{effective_timeout}s")
45
+ end
46
+
47
+ unless status && status.success?
48
+ raise JSError.new(
49
+ "JavaScript execution failed: #{stderr.to_s.strip}",
50
+ stderr: stderr,
51
+ exit_code: status ? status.exitstatus : nil
52
+ )
53
+ end
54
+
55
+ stdout.to_s.strip
56
+ end
57
+
58
+ def eval_json(js_code, timeout: nil, install_dir: nil, auto_download: nil)
59
+ output = eval(js_code, timeout: timeout, install_dir: install_dir, auto_download: auto_download)
60
+ return nil if output.nil? || output.empty?
61
+
62
+ JSON.parse(output)
63
+ rescue JSON::ParserError => e
64
+ raise ResponseParseError, "Failed to parse JavaScript JSON output: #{e.message} (Output: #{output.inspect})"
65
+ end
66
+
67
+ private
68
+
69
+ def ensure_qjs_installed!(install_dir: nil, auto_download: nil)
70
+ should_download = auto_download.nil? ? HttpMimic.configuration.auto_download : auto_download
71
+ return unless should_download
72
+ return if Downloader.qjs_installed?(install_dir: install_dir)
73
+
74
+ Downloader.download_qjs!(install_dir: install_dir)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -41,6 +41,11 @@ module HttpMimic
41
41
  default_options[:cookies].merge!(c)
42
42
  end
43
43
 
44
+ def persist_cookies(enabled = nil)
45
+ return default_options[:persist_cookies] if enabled.nil?
46
+ default_options[:persist_cookies] = enabled
47
+ end
48
+
44
49
  def mode(m = nil)
45
50
  return default_options[:mode] if m.nil?
46
51
  default_options[:mode] = m
@@ -18,8 +18,22 @@ module HttpMimic
18
18
  auto_fallback = options.fetch(:auto_fallback, config.auto_fallback)
19
19
  retry_statuses = options[:retry_statuses] || config.retry_statuses || [403, 429, 503]
20
20
 
21
+ # Persistent CookieStore integration
22
+ should_persist_cookie = options.fetch(:persist_cookies, config.persist_cookies)
23
+ host = CookieStore.extract_host(url)
24
+
25
+ if should_persist_cookie && host
26
+ stored_cookies = CookieStore.load(host)
27
+ if stored_cookies && !stored_cookies.empty?
28
+ log_debug("[HttpMimic::CookieStore] Loaded #{stored_cookies.size} persistent cookies for #{host}")
29
+ user_cookies = options[:cookies] ? (options[:cookies].is_a?(Hash) ? options[:cookies] : options[:cookies].to_h) : {}
30
+ options[:cookies] = stored_cookies.merge(user_cookies)
31
+ end
32
+ end
33
+
21
34
  attempts = []
22
35
  profiles_to_try = determine_profiles(mode, auto_fallback)
36
+ waf_solve_attempted = false
23
37
 
24
38
  response = nil
25
39
  final_status = nil
@@ -66,7 +80,32 @@ module HttpMimic
66
80
  final_stderr = stderr
67
81
  final_command = full_command
68
82
 
69
- is_blocked = (status.exitstatus != 0) || retry_statuses.include?(response.code)
83
+ # Forward any received cookies to subsequent attempts
84
+ if response.cookies && !response.cookies.empty?
85
+ existing_cookies = options[:cookies] ? (options[:cookies].is_a?(Hash) ? options[:cookies] : options[:cookies].to_h) : {}
86
+ options[:cookies] = response.cookies.to_h.merge(existing_cookies)
87
+ end
88
+
89
+ auto_solve_waf = options.fetch(:solve_waf, config.auto_solve_waf)
90
+ is_blocked = (status.exitstatus != 0) || retry_statuses.include?(response.code) || Waf::Detector.challenge_page?(response)
91
+
92
+ # Only attempt WAF resolution once per request to avoid unnecessary latency on subsequent fallbacks
93
+ if is_blocked && auto_solve_waf && !waf_solve_attempted && method.to_s.upcase == 'GET'
94
+ waf_type = Waf::Detector.detect(response)
95
+ if waf_type
96
+ waf_solve_attempted = true
97
+ log_debug("[HttpMimic] Detected #{waf_type.to_s.capitalize} WAF challenge. Attempting to solve with QuickJS...")
98
+ solved_resp = Waf.solve(url, response, current_opts)
99
+ if solved_resp
100
+ response = solved_resp
101
+ is_blocked = (response.code != 0 && retry_statuses.include?(response.code))
102
+ if response.cookies && !response.cookies.empty?
103
+ options[:cookies] = (options[:cookies] || {}).merge(response.cookies.to_h)
104
+ end
105
+ end
106
+ end
107
+ end
108
+
70
109
  if !is_blocked || (index == profiles_to_try.size - 1)
71
110
  break
72
111
  end
@@ -74,6 +113,12 @@ module HttpMimic
74
113
  log_debug("[HttpMimic] Attempt #{index + 1} with #{profile} resulted in status #{response.code}. Triggering smart fallback to next profile...")
75
114
  end
76
115
 
116
+ # Persist cookies back to store if enabled
117
+ if should_persist_cookie && host && response && response.cookies && !response.cookies.empty?
118
+ CookieStore.save(host, response.cookies)
119
+ log_debug("[HttpMimic::CookieStore] Saved #{response.cookies.size} cookies for #{host}")
120
+ end
121
+
77
122
  handle_errors(final_status, final_stderr, final_command, response)
78
123
  response
79
124
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HttpMimic
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'json'
5
+
6
+ module HttpMimic
7
+ module Waf
8
+ class AkamaiSolver
9
+ CONTEXT_JS_PATH = File.expand_path('browser_context.js', __dir__)
10
+
11
+ class << self
12
+ def solve(target_url, initial_response, options = {})
13
+ cookies = initial_response.cookies.dup
14
+ impersonate = options[:impersonate] || HttpMimic.configuration.default_impersonate || 'chrome131'
15
+
16
+ sensor_script_url = extract_sensor_script_url(target_url, initial_response.body)
17
+ return nil unless sensor_script_url
18
+
19
+ # 1. Fetch the dynamic sensor script
20
+ script_resp = HttpMimic.get(
21
+ sensor_script_url,
22
+ impersonate: impersonate,
23
+ cookies: cookies,
24
+ headers: { 'Referer' => target_url },
25
+ auto_fallback: false,
26
+ solve_waf: false
27
+ )
28
+ return nil unless script_resp.success?
29
+
30
+ sensor_js = script_resp.body
31
+ context_js = File.read(CONTEXT_JS_PATH)
32
+ cookie_str = cookies.to_cookie_string
33
+
34
+ doc_title = initial_response.title.to_s
35
+ user_agent = options[:user_agent]
36
+ referer = (options[:headers] || {})['Referer'] || (options[:headers] || {})['referer'] || target_url
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};" : ""}
45
+
46
+ #{context_js}
47
+ #{sensor_js}
48
+
49
+ if (typeof globalThis.__simulateHumanInteractions === "function") {
50
+ globalThis.__simulateHumanInteractions();
51
+ }
52
+ if (typeof globalThis.__drainEventLoop === "function") {
53
+ globalThis.__drainEventLoop(100);
54
+ }
55
+
56
+ JSON.stringify({
57
+ sensor_posts: globalThis.__sensor_posts
58
+ });
59
+ JS
60
+
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
67
+ sensor_posts.each do |post|
68
+ post_body = post['body']
69
+ next if post_body.nil? || post_body.empty?
70
+
71
+ post_resp = HttpMimic.post(
72
+ sensor_script_url,
73
+ impersonate: impersonate,
74
+ cookies: updated_cookies,
75
+ headers: {
76
+ 'Content-Type' => 'text/plain;charset=UTF-8',
77
+ 'Referer' => target_url,
78
+ 'Origin' => URI.parse(target_url).tap { |u| u.path = ''; u.query = nil }.to_s,
79
+ 'Accept' => '*/*'
80
+ },
81
+ body: post_body,
82
+ auto_fallback: false,
83
+ solve_waf: false
84
+ )
85
+
86
+ post_resp.cookies.each { |k, v| updated_cookies[k] = v }
87
+ end
88
+
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
105
+ end
106
+
107
+ def extract_sensor_script_url(target_url, html)
108
+ return nil if html.nil? || html.empty?
109
+
110
+ match = html.match(/<script[^>]*src=["\x27]([^"\x27]*\/[a-zA-Z0-9_-]{10,}\?[a-zA-Z0-9_=-]+)["\x27]/i)
111
+ 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)
113
+
114
+ return nil unless match
115
+
116
+ URI.join(target_url, match[1]).to_s
117
+ rescue StandardError
118
+ nil
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HttpMimic
4
+ module Waf
5
+ class Detector
6
+ class << self
7
+ def detect(response)
8
+ return nil unless response
9
+
10
+ if akamai?(response)
11
+ :akamai
12
+ elsif cloudflare?(response)
13
+ :cloudflare
14
+ elsif datadome?(response)
15
+ :datadome
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ def challenge_page?(response)
22
+ return false unless response
23
+ body = response.body.to_s
24
+ return true if body.include?('sec-if-cpt-container') || body.include?('sec-bc-button-parent')
25
+ return true if body.include?('challenges.cloudflare.com') || body.include?('cf-turnstile')
26
+ return true if body.include?('datadome.captcha') || body.include?('geo.captcha-delivery.com')
27
+ false
28
+ end
29
+
30
+ def akamai?(response)
31
+ return true if response.headers['set-cookie']&.include?('_abck=')
32
+ return true if response.cookies.key?('_abck') || response.cookies.key?('bm_sz') || response.cookies.key?('ak_bmsc')
33
+ return true if response.headers['x-akamai-transformed'] || response.headers['x-reference-error']
34
+
35
+ body = response.body.to_s
36
+ return true if body.include?('Reference Error:') && body.include?('Akamai')
37
+ return true if body.include?('bmak') || body.include?('sensor_data')
38
+ return true if body.include?('sec-if-cpt-container') || body.include?('sec-bc-button-parent')
39
+ return true if body =~ /<script[^>]*src=["\x27]([^"\x27]*\/[a-zA-Z0-9_-]{10,}\?[a-zA-Z0-9_=-]+)["\x27]/
40
+
41
+ false
42
+ end
43
+
44
+ def cloudflare?(response)
45
+ return true if response.headers['server']&.downcase&.include?('cloudflare')
46
+ return true if response.headers['cf-ray'] || response.cookies.key?('cf_clearance')
47
+ return true if response.body.to_s.include?('challenges.cloudflare.com')
48
+
49
+ false
50
+ end
51
+
52
+ def datadome?(response)
53
+ return true if response.headers['x-datadome'] || response.cookies.key?('datadome')
54
+ return true if response.headers['server']&.downcase&.include?('datadome')
55
+
56
+ false
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'http_mimic/waf/detector'
4
+ require 'http_mimic/waf/akamai_solver'
5
+
6
+ module HttpMimic
7
+ module Waf
8
+ class << self
9
+ def solve(url, response, options = {})
10
+ waf_type = Detector.detect(response)
11
+ return nil unless waf_type
12
+
13
+ case waf_type
14
+ when :akamai
15
+ AkamaiSolver.solve(url, response, options)
16
+ else
17
+ nil
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/http_mimic.rb CHANGED
@@ -16,6 +16,9 @@ require 'http_mimic/command_builder'
16
16
  require 'http_mimic/request'
17
17
  require 'http_mimic/client'
18
18
  require 'http_mimic/module_methods'
19
+ require 'http_mimic/js_runtime'
20
+ require 'http_mimic/waf'
21
+ require 'http_mimic/cookie_store'
19
22
 
20
23
  module HttpMimic
21
24
  extend ModuleMethods
@@ -43,6 +46,54 @@ module HttpMimic
43
46
  Downloader.installed?(version: version)
44
47
  end
45
48
 
49
+ # Manually download and install QuickJS binary driver
50
+ def download_qjs!(version: nil, force: false)
51
+ Downloader.download_qjs!(version: version, force: force)
52
+ end
53
+
54
+ # Check if QuickJS driver is installed locally
55
+ def qjs_installed?(version: nil)
56
+ Downloader.qjs_installed?(version: version)
57
+ end
58
+
59
+ # Return local path to QuickJS binary
60
+ def qjs_path
61
+ Downloader.qjs_path
62
+ end
63
+
64
+ # Evaluate JavaScript code using QuickJS
65
+ def eval_js(code, options = {})
66
+ JSRuntime.eval(code, timeout: options[:timeout], install_dir: options[:install_dir], auto_download: options[:auto_download])
67
+ end
68
+
69
+ # Evaluate JavaScript and auto-parse JSON result
70
+ def eval_js_json(code, options = {})
71
+ JSRuntime.eval_json(code, timeout: options[:timeout], install_dir: options[:install_dir], auto_download: options[:auto_download])
72
+ end
73
+
74
+ # Automatically detect WAF type from response
75
+ def detect_waf(response)
76
+ Waf::Detector.detect(response)
77
+ end
78
+
79
+ # Attempt to solve WAF challenge for a blocked response
80
+ def solve_waf(url, response, options = {})
81
+ Waf.solve(url, response, options)
82
+ end
83
+
84
+ # Persistent Host CookieStore helpers
85
+ def load_cookies(host, max_age: nil)
86
+ CookieStore.load(host, max_age: max_age)
87
+ end
88
+
89
+ def save_cookies(host, cookies, ttl: nil)
90
+ CookieStore.save(host, cookies, ttl: ttl || CookieStore::DEFAULT_TTL)
91
+ end
92
+
93
+ def clear_cookies!(host = nil)
94
+ CookieStore.clear(host)
95
+ end
96
+
46
97
  def included(base)
47
98
  base.extend(ModuleMethods)
48
99
  base.send(:include, InstanceMethods)
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.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - anxgang
@@ -65,15 +65,20 @@ files:
65
65
  - lib/http_mimic/client.rb
66
66
  - lib/http_mimic/command_builder.rb
67
67
  - lib/http_mimic/configuration.rb
68
+ - lib/http_mimic/cookie_store.rb
68
69
  - lib/http_mimic/cookies.rb
69
70
  - lib/http_mimic/downloader.rb
70
71
  - lib/http_mimic/exceptions.rb
71
72
  - lib/http_mimic/headers.rb
73
+ - lib/http_mimic/js_runtime.rb
72
74
  - lib/http_mimic/module_methods.rb
73
75
  - lib/http_mimic/request.rb
74
76
  - lib/http_mimic/response.rb
75
77
  - lib/http_mimic/response_parser.rb
76
78
  - lib/http_mimic/version.rb
79
+ - lib/http_mimic/waf.rb
80
+ - lib/http_mimic/waf/akamai_solver.rb
81
+ - lib/http_mimic/waf/detector.rb
77
82
  homepage: https://github.com/anxgang/http_mimic
78
83
  licenses:
79
84
  - MIT