http_mimic 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +41 -0
- data/lib/http_mimic/command_builder.rb +13 -1
- data/lib/http_mimic/configuration.rb +15 -0
- data/lib/http_mimic/downloader.rb +97 -0
- data/lib/http_mimic/module_methods.rb +11 -0
- data/lib/http_mimic/obscura.rb +139 -0
- data/lib/http_mimic/request.rb +29 -0
- data/lib/http_mimic/spa_detector.rb +95 -0
- data/lib/http_mimic/version.rb +1 -1
- data/lib/http_mimic/waf/detector.rb +12 -0
- data/lib/http_mimic/waf/google_solver.rb +124 -0
- data/lib/http_mimic/waf.rb +3 -0
- data/lib/http_mimic.rb +26 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b09e014134575dac987366c71b0cb6644a13a5b17088947621474f6cfccec5cf
|
|
4
|
+
data.tar.gz: 748d8d3dadcb69d39312a6c7137c32cca1290b7eaee4d8641fa7bb58bfdd0f80
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4d17399d5f85df9b508151f371b0910e074d70e3c05f11cb291f30f01b71c20d22e998ec5c5466728c77dd2f37b3de4aecc18883a844fb69952ae71a3a5c5be
|
|
7
|
+
data.tar.gz: 7b0675e724e1c138cd57ea4af5e5a6ce2f057bb50981271f9b81cb894bf7179c2ba760137f0672d742a63b602783bc6b455a71b9da4f778c0637db96ee723470
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ 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.1] - 2026-08-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Two-Phase Cookie & State Pipeline (Tier 1 ➔ Tier 2)**:
|
|
12
|
+
- Automatically forward validated cookies (`NID`, `AEC`, `_abck`, `cf_clearance`) and custom headers from Tier 1 HTTP / WAF resolution directly into Obscura (`--cookie` flag) during `auto_render_spa` transitions.
|
|
13
|
+
- **Obscura Stealth BoringSSL Integration**:
|
|
14
|
+
- Downloader now defaults to `*-stealth.tar.gz` release assets, equipping Obscura with native BoringSSL Chrome TLS / JA4 fingerprint impersonation.
|
|
15
|
+
- **Upgraded Obscura to `anxgang/obscura` v0.2.3**:
|
|
16
|
+
- WebGL context, `REAL_FONT_METRICS` font measurements, and persistent Web Worker `importScripts` support.
|
|
17
|
+
|
|
18
|
+
## [0.5.0] - 2026-08-30
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Tier 2: Obscura Headless SPA Rendering Support**:
|
|
22
|
+
- Native integration with [`h4ckf0r0day/obscura`](https://github.com/h4ckf0r0day/obscura)—a lightweight (<100MB) Rust + V8 headless browser engine with built-in stealth anti-detection.
|
|
23
|
+
- On-demand automatic driver management (`HttpMimic.download_obscura!`, `HttpMimic.obscura_installed?`, `HttpMimic.obscura_path`) across macOS (ARM64/x86_64), Linux (x86_64/aarch64), and Windows.
|
|
24
|
+
- New rendering helpers: `HttpMimic.render(url, options = {})` and `HttpMimic.spa(url, options = {})`.
|
|
25
|
+
- **Automatic SPA Detection & Transition (`auto_render_spa`)**:
|
|
26
|
+
- Intelligent `HttpMimic::SpaDetector` detects unhydrated client-side SPA shells (React `#root`, Vue `#app`, Angular `<app-root>`, Google Dynamic SERP, `<noscript>` prompts).
|
|
27
|
+
- When `auto_render_spa: true` (or `config.auto_render_spa = true`) is enabled, `HttpMimic.get` automatically transitions from Tier 1 (HTTP) to Tier 2 (Obscura) to render the full dynamic DOM.
|
|
28
|
+
- Configurable options: `:wait_until` (`networkidle0`, `domcontentloaded`, `load`), `:timeout`, `:proxy`, `:eval`, `:dump`, `:selector`, `:user_agent`, and `:stealth`.
|
|
29
|
+
|
|
30
|
+
## [0.4.1] - 2026-08-30
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- **Google Search Guard & BotGuard VM Detection**:
|
|
34
|
+
- Added `HttpMimic::Waf::GoogleSolver` and `HttpMimic::Waf::Detector.google?` for detecting Google Search Guard (`knitsail`, `enablejs`, `SG_SS`) challenges.
|
|
35
|
+
- Added automatic Google Search referral context (`Referer: https://www.google.com/`, `sec-fetch-site: same-origin`) in `CommandBuilder#apply_navigation_headers`.
|
|
36
|
+
- **Authentic Hardware & WebGL Pipeline Emulation in QuickJS Context**:
|
|
37
|
+
- Full WebGL shader compilation and rasterization pipeline (`createShader`, `compileShader`, `getShaderPrecisionFormat`, `createProgram`, `linkProgram`, `readPixels` simulated gradient buffer).
|
|
38
|
+
- Dynamic font measurement (`REAL_FONT_METRICS`) with exact macOS Chrome `offsetWidth` / `offsetHeight` dimensions across 17 font families and 7 font sizes.
|
|
39
|
+
- Authentic Chromium `OfflineAudioContext` audio buffer rendering curve.
|
|
40
|
+
- Physics-based Cubic Bézier Spline mouse movement simulation with velocity decay, natural micro-jitter, and complete click event chains.
|
|
41
|
+
- Polyfills for `trustedTypes`, `sessionStorage`, `localStorage`, and `document.currentScript`.
|
|
42
|
+
- **Automatic Response Decompression**:
|
|
43
|
+
- Added `--compressed` flag to CommandBuilder to ensure automatic transparent decompression of `gzip`, `deflate`, `br` (Brotli), and `zstd` payloads.
|
|
44
|
+
|
|
8
45
|
## [0.4.0] - 2026-08-30
|
|
9
46
|
|
|
10
47
|
### Added
|
data/README.md
CHANGED
|
@@ -287,6 +287,47 @@ end
|
|
|
287
287
|
| `:bearer_token` | String | Appends `Authorization: Bearer <token>` header |
|
|
288
288
|
| `:insecure` | Boolean | Disable SSL certificate verification (`-k`) |
|
|
289
289
|
| `:curl_options` | Array / String | Additional raw curl arguments (e.g., `['--http2', '--compressed']`) |
|
|
290
|
+
| `:auto_render_spa` | Boolean | Automatically detect unhydrated SPA shells and render via Obscura |
|
|
291
|
+
| `:render` | Symbol | `:spa` or `:obscura` to explicitly render page via Obscura |
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## ⚡ Obscura SPA Rendering (Tier 2 Engine)
|
|
296
|
+
|
|
297
|
+
`http_mimic` provides native support for [`h4ckf0r0day/obscura`](https://github.com/h4ckf0r0day/obscura)—a lightweight, single-binary (<100MB) headless browser engine written in Rust with an embedded V8 JavaScript runtime and built-in stealth anti-detection capabilities.
|
|
298
|
+
|
|
299
|
+
Obscura is auto-downloaded on-demand into `~/.http_mimic/bin/obscura` when SPA rendering is requested.
|
|
300
|
+
|
|
301
|
+
### 1. Automatic SPA Detection & Rendering (`auto_render_spa`)
|
|
302
|
+
|
|
303
|
+
Similar to `auto_solve_waf`, when `auto_render_spa: true` is enabled, `HttpMimic` first sends a microsecond-fast HTTP request (Tier 1). If the response is detected as an unhydrated SPA shell (empty React `#root`, Vue `#app`, Angular `<app-root>`, Google Dynamic SERP, or `<noscript>` prompt), it automatically transitions to Obscura (Tier 2) to render the full DOM tree:
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
# Auto-detects SPA shell and seamlessly renders with Obscura
|
|
307
|
+
response = HttpMimic.get('https://example.com/spa', auto_render_spa: true)
|
|
308
|
+
|
|
309
|
+
puts response.code # => 200
|
|
310
|
+
puts response.title # => Fully hydrated DOM title
|
|
311
|
+
puts response.body # => Fully rendered HTML with client-side injected DOM nodes
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### 2. Direct SPA Rendering
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
# Direct SPA render
|
|
318
|
+
response = HttpMimic.render('https://example.com/spa')
|
|
319
|
+
|
|
320
|
+
# or via alias:
|
|
321
|
+
response = HttpMimic.spa('https://example.com/spa')
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Supported Options**:
|
|
325
|
+
- `:wait_until` - `'load'`, `'domcontentloaded'`, `'networkidle0'` (default: `'networkidle0'`)
|
|
326
|
+
- `:timeout` - execution deadline in seconds (default: 30)
|
|
327
|
+
- `:proxy` - proxy address (`socks5://...` or `http://...`)
|
|
328
|
+
- `:eval` - JavaScript expression to evaluate in page context
|
|
329
|
+
- `:dump` - output format: `'html'` (default), `'text'`, `'links'`, `'markdown'`, `'cookies'`
|
|
330
|
+
- `:stealth` - whether stealth anti-detection is enabled (default: `true`)
|
|
290
331
|
|
|
291
332
|
---
|
|
292
333
|
|
|
@@ -94,9 +94,10 @@ module HttpMimic
|
|
|
94
94
|
args = []
|
|
95
95
|
stdin_data = nil
|
|
96
96
|
|
|
97
|
-
# Base flags: silent mode, include HTTP headers
|
|
97
|
+
# Base flags: silent mode, include HTTP headers, auto-decompress responses
|
|
98
98
|
args << '-s'
|
|
99
99
|
args << '-i'
|
|
100
|
+
args << '--compressed'
|
|
100
101
|
|
|
101
102
|
# HTTP Method
|
|
102
103
|
if @method == 'HEAD'
|
|
@@ -387,6 +388,17 @@ module HttpMimic
|
|
|
387
388
|
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
389
|
headers_to_send['accept-language'] ||= 'en-US,en;q=0.9'
|
|
389
390
|
end
|
|
391
|
+
|
|
392
|
+
# Smart Search referral context (e.g. searching from Google homepage)
|
|
393
|
+
begin
|
|
394
|
+
parsed_uri = URI.parse(url)
|
|
395
|
+
if parsed_uri && parsed_uri.host =~ /google\./i && parsed_uri.path =~ /\/search/i
|
|
396
|
+
headers_to_send['referer'] ||= "#{parsed_uri.scheme || 'https'}://#{parsed_uri.host}/"
|
|
397
|
+
headers_to_send['sec-fetch-site'] ||= 'same-origin'
|
|
398
|
+
end
|
|
399
|
+
rescue StandardError
|
|
400
|
+
# ignore
|
|
401
|
+
end
|
|
390
402
|
end
|
|
391
403
|
|
|
392
404
|
def find_in_download_dir(candidates)
|
|
@@ -30,6 +30,12 @@ module HttpMimic
|
|
|
30
30
|
attr_accessor :qjs_version
|
|
31
31
|
attr_accessor :qjs_github_repo
|
|
32
32
|
|
|
33
|
+
# Obscura headless SPA engine settings
|
|
34
|
+
attr_accessor :obscura_version
|
|
35
|
+
attr_accessor :obscura_github_repo
|
|
36
|
+
attr_accessor :obscura_path
|
|
37
|
+
attr_accessor :auto_render_spa
|
|
38
|
+
|
|
33
39
|
# WAF & JS challenge solving settings
|
|
34
40
|
attr_accessor :auto_solve_waf
|
|
35
41
|
|
|
@@ -68,6 +74,15 @@ module HttpMimic
|
|
|
68
74
|
@qjs_version = 'v0.16.2'
|
|
69
75
|
@qjs_github_repo = 'quickjs-ng/quickjs'
|
|
70
76
|
|
|
77
|
+
# Obscura defaults (Lightweight Rust+V8 headless SPA engine)
|
|
78
|
+
# Temporarily points to 'anxgang/obscura' (v0.2.3) for Web Worker importScripts support.
|
|
79
|
+
# Will switch back to 'h4ckf0r0day/obscura' once upstream Pull Request is merged.
|
|
80
|
+
# @obscura_github_repo = 'h4ckf0r0day/obscura'
|
|
81
|
+
@obscura_version = 'v0.2.3'
|
|
82
|
+
@obscura_github_repo = 'anxgang/obscura'
|
|
83
|
+
@obscura_path = nil
|
|
84
|
+
@auto_render_spa = false
|
|
85
|
+
|
|
71
86
|
# WAF challenge solving defaults
|
|
72
87
|
@auto_solve_waf = true
|
|
73
88
|
|
|
@@ -16,6 +16,9 @@ module HttpMimic
|
|
|
16
16
|
DEFAULT_QJS_REPO = 'quickjs-ng/quickjs'
|
|
17
17
|
DEFAULT_QJS_VERSION = 'v0.16.2'
|
|
18
18
|
|
|
19
|
+
DEFAULT_OBSCURA_REPO = 'h4ckf0r0day/obscura'
|
|
20
|
+
DEFAULT_OBSCURA_VERSION = 'v0.2.1'
|
|
21
|
+
|
|
19
22
|
class DownloadError < HttpMimic::Error; end
|
|
20
23
|
class UnsupportedPlatformError < HttpMimic::Error; end
|
|
21
24
|
|
|
@@ -165,6 +168,100 @@ module HttpMimic
|
|
|
165
168
|
File.join(dir, '.qjs_version')
|
|
166
169
|
end
|
|
167
170
|
|
|
171
|
+
def download_obscura!(version: nil, install_dir: nil, repo: nil, force: false)
|
|
172
|
+
target_version = normalize_version(version || HttpMimic.configuration.obscura_version || DEFAULT_OBSCURA_VERSION)
|
|
173
|
+
target_dir = File.expand_path(install_dir || HttpMimic.configuration.install_dir)
|
|
174
|
+
target_repo = repo || HttpMimic.configuration.obscura_github_repo || DEFAULT_OBSCURA_REPO
|
|
175
|
+
|
|
176
|
+
FileUtils.mkdir_p(target_dir)
|
|
177
|
+
|
|
178
|
+
dest_binary = File.join(target_dir, binary_name_for_platform('obscura'))
|
|
179
|
+
|
|
180
|
+
if !force && obscura_installed?(version: target_version, install_dir: target_dir)
|
|
181
|
+
log_info("obscura #{target_version} is already installed in #{target_dir}")
|
|
182
|
+
return dest_binary
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
asset_name = obscura_platform_asset
|
|
186
|
+
download_url = "https://github.com/#{target_repo}/releases/download/#{target_version}/#{asset_name}"
|
|
187
|
+
|
|
188
|
+
log_info("Downloading Obscura (#{target_version}) [#{asset_name}]...")
|
|
189
|
+
archive_data = fetch_binary(download_url)
|
|
190
|
+
|
|
191
|
+
log_info("Extracting Obscura archive to #{target_dir}...")
|
|
192
|
+
extract_tar_gz(archive_data, target_dir)
|
|
193
|
+
|
|
194
|
+
# Ensure executable permissions on extracted binaries
|
|
195
|
+
%w[obscura obscura-worker].each do |name|
|
|
196
|
+
p = File.join(target_dir, binary_name_for_platform(name))
|
|
197
|
+
File.chmod(0755, p) if File.exist?(p)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
File.write(obscura_version_file_path(target_dir), target_version)
|
|
201
|
+
|
|
202
|
+
log_info("Obscura #{target_version} installation complete! (#{dest_binary})")
|
|
203
|
+
dest_binary
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def obscura_installed?(version: nil, install_dir: nil)
|
|
207
|
+
target_dir = File.expand_path(install_dir || HttpMimic.configuration.install_dir)
|
|
208
|
+
bin = File.join(target_dir, binary_name_for_platform('obscura'))
|
|
209
|
+
return false unless File.file?(bin) && File.executable?(bin)
|
|
210
|
+
|
|
211
|
+
if version
|
|
212
|
+
target_version = normalize_version(version)
|
|
213
|
+
v_file = obscura_version_file_path(target_dir)
|
|
214
|
+
return false unless File.file?(v_file)
|
|
215
|
+
return File.read(v_file).strip == target_version
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def obscura_path(install_dir: nil)
|
|
222
|
+
return HttpMimic.configuration.obscura_path if HttpMimic.configuration.obscura_path && File.executable?(HttpMimic.configuration.obscura_path)
|
|
223
|
+
|
|
224
|
+
target_dir = File.expand_path(install_dir || HttpMimic.configuration.install_dir)
|
|
225
|
+
candidate = File.join(target_dir, binary_name_for_platform('obscura'))
|
|
226
|
+
return candidate if File.file?(candidate) && File.executable?(candidate)
|
|
227
|
+
|
|
228
|
+
# Fallback to system PATH
|
|
229
|
+
sys_bin = `which obscura 2>/dev/null`.strip
|
|
230
|
+
return sys_bin if !sys_bin.empty? && File.executable?(sys_bin)
|
|
231
|
+
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def obscura_platform_asset
|
|
236
|
+
os = host_os
|
|
237
|
+
cpu = host_cpu
|
|
238
|
+
|
|
239
|
+
case os
|
|
240
|
+
when :macos
|
|
241
|
+
case cpu
|
|
242
|
+
when :arm64 then 'obscura-aarch64-macos-stealth.tar.gz'
|
|
243
|
+
when :x86_64 then 'obscura-x86_64-macos-stealth.tar.gz'
|
|
244
|
+
else
|
|
245
|
+
raise UnsupportedPlatformError, "Unsupported macOS CPU architecture for Obscura: #{cpu}"
|
|
246
|
+
end
|
|
247
|
+
when :linux
|
|
248
|
+
case cpu
|
|
249
|
+
when :x86_64 then 'obscura-x86_64-linux-stealth.tar.gz'
|
|
250
|
+
when :aarch64, :arm64 then 'obscura-aarch64-linux-stealth.tar.gz'
|
|
251
|
+
else
|
|
252
|
+
raise UnsupportedPlatformError, "Unsupported Linux CPU architecture for Obscura: #{cpu}"
|
|
253
|
+
end
|
|
254
|
+
when :windows
|
|
255
|
+
'obscura-x86_64-windows-stealth.zip'
|
|
256
|
+
else
|
|
257
|
+
raise UnsupportedPlatformError, "Unsupported operating system for Obscura: #{RbConfig::CONFIG['host_os']}"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def obscura_version_file_path(dir)
|
|
262
|
+
File.join(dir, '.obscura_version')
|
|
263
|
+
end
|
|
264
|
+
|
|
168
265
|
def binary_path(name, install_dir: nil)
|
|
169
266
|
target_dir = File.expand_path(install_dir || HttpMimic.configuration.install_dir)
|
|
170
267
|
candidate = File.join(target_dir, binary_name_for_platform(name))
|
|
@@ -46,6 +46,11 @@ module HttpMimic
|
|
|
46
46
|
default_options[:persist_cookies] = enabled
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def auto_render_spa(enabled = nil)
|
|
50
|
+
return default_options[:auto_render_spa] if enabled.nil?
|
|
51
|
+
default_options[:auto_render_spa] = enabled
|
|
52
|
+
end
|
|
53
|
+
|
|
49
54
|
def mode(m = nil)
|
|
50
55
|
return default_options[:mode] if m.nil?
|
|
51
56
|
default_options[:mode] = m
|
|
@@ -93,6 +98,12 @@ module HttpMimic
|
|
|
93
98
|
request(:options, url, options)
|
|
94
99
|
end
|
|
95
100
|
|
|
101
|
+
def render(url, options = {})
|
|
102
|
+
merged = default_options.merge(options)
|
|
103
|
+
Obscura.render(url, merged)
|
|
104
|
+
end
|
|
105
|
+
alias spa render
|
|
106
|
+
|
|
96
107
|
def request(method, url, options = {})
|
|
97
108
|
merged = default_options.merge(options)
|
|
98
109
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module HttpMimic
|
|
8
|
+
module Obscura
|
|
9
|
+
class ExecutionError < HttpMimic::Error; end
|
|
10
|
+
class BinaryNotFoundError < HttpMimic::Error; end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Renders a web page using the Obscura headless SPA engine and returns an HttpMimic::Response
|
|
14
|
+
#
|
|
15
|
+
# @param url [String, URI] The target URL to render
|
|
16
|
+
# @param options [Hash] Rendering options
|
|
17
|
+
# @option options [String] :wait_until ('networkidle0') Wait condition (e.g. 'load', 'domcontentloaded', 'networkidle0')
|
|
18
|
+
# @option options [Integer] :timeout Per-command timeout in seconds
|
|
19
|
+
# @option options [String] :proxy Proxy URL (e.g. 'socks5://127.0.0.1:1080' or 'http://...')
|
|
20
|
+
# @option options [String] :eval JavaScript expression to evaluate on the page
|
|
21
|
+
# @option options [String] :dump Output format ('html', 'text', 'links')
|
|
22
|
+
# @return [HttpMimic::Response]
|
|
23
|
+
def render(url, options = {})
|
|
24
|
+
bin = resolve_binary
|
|
25
|
+
raise BinaryNotFoundError, "Obscura binary not found. Run HttpMimic.download_obscura! or configure obscura_path." unless bin
|
|
26
|
+
|
|
27
|
+
args = [bin, 'fetch', url.to_s]
|
|
28
|
+
|
|
29
|
+
# Dump format: default to html (can be 'html', 'text', 'links', 'markdown', 'original', 'cookies')
|
|
30
|
+
dump_format = options[:dump] || 'html'
|
|
31
|
+
args << '--dump' << dump_format.to_s
|
|
32
|
+
|
|
33
|
+
# Stealth mode (enabled by default)
|
|
34
|
+
args << '--stealth' if options.fetch(:stealth, true)
|
|
35
|
+
|
|
36
|
+
# Wait condition
|
|
37
|
+
if options[:wait_until]
|
|
38
|
+
args << '--wait-until' << options[:wait_until].to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Wait delay
|
|
42
|
+
if options[:wait]
|
|
43
|
+
args << '--wait' << options[:wait].to_i.to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Timeout in seconds (integer)
|
|
47
|
+
timeout = options[:timeout] || HttpMimic.configuration.default_timeout
|
|
48
|
+
args << '--timeout' << timeout.to_i.to_s if timeout
|
|
49
|
+
|
|
50
|
+
# User Agent
|
|
51
|
+
if options[:user_agent]
|
|
52
|
+
args << '--user-agent' << options[:user_agent].to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Proxy
|
|
56
|
+
if options[:proxy]
|
|
57
|
+
args << '--proxy' << options[:proxy].to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Cookies: pass cookies string or hash (Two-Phase Pipeline support)
|
|
61
|
+
if options[:cookies] || options[:cookie]
|
|
62
|
+
cookie_val = options[:cookies] || options[:cookie]
|
|
63
|
+
cookie_str = if cookie_val.is_a?(Hash)
|
|
64
|
+
cookie_val.map { |k, v| "#{k}=#{v}" }.join('; ')
|
|
65
|
+
elsif cookie_val.is_a?(Array)
|
|
66
|
+
cookie_val.join('; ')
|
|
67
|
+
else
|
|
68
|
+
cookie_val.to_s
|
|
69
|
+
end
|
|
70
|
+
args << '--cookie' << cookie_str unless cookie_str.empty?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Optional JS evaluation
|
|
74
|
+
args << '--eval' << options[:eval].to_s if options[:eval]
|
|
75
|
+
|
|
76
|
+
# Selector
|
|
77
|
+
args << '--selector' << options[:selector].to_s if options[:selector]
|
|
78
|
+
|
|
79
|
+
log_debug("Executing Obscura SPA render: #{args.join(' ')}")
|
|
80
|
+
|
|
81
|
+
stdout, stderr, status = Open3.capture3(*args)
|
|
82
|
+
|
|
83
|
+
code = (status && status.success?) ? 200 : (status ? status.exitstatus : 500)
|
|
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"
|
|
86
|
+
headers = Headers.new({ 'content-type' => 'text/html; charset=utf-8', 'x-rendered-by' => 'obscura' })
|
|
87
|
+
|
|
88
|
+
Response.new(
|
|
89
|
+
code: code,
|
|
90
|
+
http_version: 'HTTP/2',
|
|
91
|
+
status_message: status&.success? ? 'OK (Obscura SPA Rendered)' : 'Error',
|
|
92
|
+
headers: headers,
|
|
93
|
+
cookies: Cookies.new,
|
|
94
|
+
body: stdout,
|
|
95
|
+
parsed_response: nil,
|
|
96
|
+
raw_headers: raw_headers,
|
|
97
|
+
history: [],
|
|
98
|
+
request_url: url.to_s,
|
|
99
|
+
stderr: stderr,
|
|
100
|
+
exit_code: status ? status.exitstatus : 0,
|
|
101
|
+
command: args.join(' ')
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def resolve_binary
|
|
106
|
+
# 1. Configured custom path
|
|
107
|
+
custom_path = HttpMimic.configuration.obscura_path
|
|
108
|
+
if custom_path && (File.file?(custom_path) || File.executable?(custom_path))
|
|
109
|
+
return custom_path
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# 2. Check install directory or system PATH
|
|
113
|
+
installed_path = Downloader.obscura_path
|
|
114
|
+
return installed_path if installed_path
|
|
115
|
+
|
|
116
|
+
# 3. Auto-download if enabled
|
|
117
|
+
if HttpMimic.configuration.auto_download
|
|
118
|
+
begin
|
|
119
|
+
return Downloader.download_obscura!
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
HttpMimic.configuration.logger&.warn("[HttpMimic::Obscura] Auto-download failed: #{e.message}")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def log_debug(msg)
|
|
131
|
+
if HttpMimic.configuration.logger
|
|
132
|
+
HttpMimic.configuration.logger.debug("[HttpMimic::Obscura] #{msg}")
|
|
133
|
+
elsif HttpMimic.configuration.debug
|
|
134
|
+
puts "[HttpMimic::Obscura] #{msg}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/http_mimic/request.rb
CHANGED
|
@@ -15,6 +15,12 @@ module HttpMimic
|
|
|
15
15
|
|
|
16
16
|
def perform
|
|
17
17
|
mode = (options[:mode] || config.mode || :auto).to_sym
|
|
18
|
+
|
|
19
|
+
# Delegate directly to Obscura headless SPA renderer if requested
|
|
20
|
+
if options[:render] == :spa || options[:render] == :obscura || mode == :spa || mode == :obscura
|
|
21
|
+
return Obscura.render(url, options)
|
|
22
|
+
end
|
|
23
|
+
|
|
18
24
|
auto_fallback = options.fetch(:auto_fallback, config.auto_fallback)
|
|
19
25
|
retry_statuses = options[:retry_statuses] || config.retry_statuses || [403, 429, 503]
|
|
20
26
|
|
|
@@ -113,6 +119,29 @@ module HttpMimic
|
|
|
113
119
|
log_debug("[HttpMimic] Attempt #{index + 1} with #{profile} resulted in status #{response.code}. Triggering smart fallback to next profile...")
|
|
114
120
|
end
|
|
115
121
|
|
|
122
|
+
# Automatic SPA Detection & Obscura rendering fallback
|
|
123
|
+
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.")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
116
145
|
# Persist cookies back to store if enabled
|
|
117
146
|
if should_persist_cookie && host && response && response.cookies && !response.cookies.empty?
|
|
118
147
|
CookieStore.save(host, response.cookies)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HttpMimic
|
|
4
|
+
module SpaDetector
|
|
5
|
+
# Framework mount points that indicate an unhydrated SPA client shell
|
|
6
|
+
EMPTY_MOUNT_REGEX = %r{
|
|
7
|
+
<div[^>]+id=["'](?:root|app|__next|__nuxt|svelte)["'][^>]*>\s*</div>|
|
|
8
|
+
<app-root[^>]*>\s*</app-root>|
|
|
9
|
+
<div[^>]+id=["']app["'][^>]*>\s*<!--\s*(?:app-content)?\s*-->\s*</div>
|
|
10
|
+
}ix
|
|
11
|
+
|
|
12
|
+
# Noscript patterns requiring JavaScript execution
|
|
13
|
+
NOSCRIPT_JS_REQUIRED_REGEX = %r{
|
|
14
|
+
<noscript[^>]*>[\s\S]*?(?:enable\s+javascript|javascript\s+is\s+required|need\s+to\s+enable\s+javascript|requires\s+javascript)[\s\S]*?</noscript>
|
|
15
|
+
}ix
|
|
16
|
+
|
|
17
|
+
# Google Search Guard dynamic SPA shell indicators
|
|
18
|
+
GOOGLE_SHELL_REGEX = %r{
|
|
19
|
+
(?:/httpservice/retry/enablejs|SG_REL|emsg=SG_REL|knitsail)
|
|
20
|
+
}ix
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Returns true if the HTTP response represents an unhydrated Single Page Application (SPA) shell
|
|
24
|
+
#
|
|
25
|
+
# @param response [HttpMimic::Response]
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def spa?(response)
|
|
28
|
+
return false unless response && response.success?
|
|
29
|
+
return false if response.respond_to?(:binary?) && response.binary?
|
|
30
|
+
|
|
31
|
+
content_type = response.headers['content-type'].to_s.downcase
|
|
32
|
+
return false unless content_type.empty? || content_type.include?('text/html') || content_type.include?('application/xhtml')
|
|
33
|
+
|
|
34
|
+
body = response.body.to_s
|
|
35
|
+
return false if body.strip.empty?
|
|
36
|
+
|
|
37
|
+
# 1. Google Dynamic SERP / Search Guard JS Shell
|
|
38
|
+
if google_serp_shell?(body)
|
|
39
|
+
return true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 2. Modern SPA framework empty mount points (React, Vue, Angular, Next, Nuxt, Svelte)
|
|
43
|
+
if empty_mount_point?(body)
|
|
44
|
+
return true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 3. Noscript requirement message
|
|
48
|
+
if noscript_js_required?(body)
|
|
49
|
+
return true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 4. Small body with multiple scripts and virtually no visible text
|
|
53
|
+
if low_text_js_heavy_shell?(body)
|
|
54
|
+
return true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def empty_mount_point?(body)
|
|
61
|
+
body.match?(EMPTY_MOUNT_REGEX)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def noscript_js_required?(body)
|
|
65
|
+
body.match?(NOSCRIPT_JS_REQUIRED_REGEX)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def google_serp_shell?(body)
|
|
69
|
+
return false unless body.match?(GOOGLE_SHELL_REGEX)
|
|
70
|
+
|
|
71
|
+
# If it has the Google shell indicators AND lacks rendered organic result elements
|
|
72
|
+
!body.include?('<h3 class=') && !body.include?('id="search"') && !body.include?('id="rso"')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def low_text_js_heavy_shell?(body)
|
|
76
|
+
return false if body.length > 25_000 # Larger HTML files typically have SSR content
|
|
77
|
+
|
|
78
|
+
# Check if there are script tags
|
|
79
|
+
script_count = body.scan(/<script/i).size
|
|
80
|
+
return false if script_count.zero?
|
|
81
|
+
|
|
82
|
+
# Strip all HTML tags, scripts, and styles to get raw visible text length
|
|
83
|
+
visible_text = body
|
|
84
|
+
.gsub(/<script[\s\S]*?<\/script>/i, '')
|
|
85
|
+
.gsub(/<style[\s\S]*?<\/style>/i, '')
|
|
86
|
+
.gsub(/<[^>]+>/, ' ')
|
|
87
|
+
.gsub(/\s+/, ' ')
|
|
88
|
+
.strip
|
|
89
|
+
|
|
90
|
+
# If visible text is virtually nonexistent but page has multiple scripts
|
|
91
|
+
visible_text.length < 80 && script_count >= 2
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/http_mimic/version.rb
CHANGED
|
@@ -9,6 +9,8 @@ module HttpMimic
|
|
|
9
9
|
|
|
10
10
|
if akamai?(response)
|
|
11
11
|
:akamai
|
|
12
|
+
elsif google?(response)
|
|
13
|
+
:google
|
|
12
14
|
elsif cloudflare?(response)
|
|
13
15
|
:cloudflare
|
|
14
16
|
elsif datadome?(response)
|
|
@@ -24,6 +26,16 @@ module HttpMimic
|
|
|
24
26
|
return true if body.include?('sec-if-cpt-container') || body.include?('sec-bc-button-parent')
|
|
25
27
|
return true if body.include?('challenges.cloudflare.com') || body.include?('cf-turnstile')
|
|
26
28
|
return true if body.include?('datadome.captcha') || body.include?('geo.captcha-delivery.com')
|
|
29
|
+
return true if body.include?('/httpservice/retry/enablejs') || (body.include?('knitsail') && body.include?('SG_SS'))
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def google?(response)
|
|
34
|
+
return false unless response
|
|
35
|
+
body = response.body.to_s
|
|
36
|
+
return true if body.include?('/httpservice/retry/enablejs')
|
|
37
|
+
return true if body.include?('knitsail') && body.include?('SG_SS')
|
|
38
|
+
return true if response.headers['server']&.downcase&.include?('gws') && body.include?('enablejs')
|
|
27
39
|
false
|
|
28
40
|
end
|
|
29
41
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module HttpMimic
|
|
7
|
+
module Waf
|
|
8
|
+
class GoogleSolver
|
|
9
|
+
class << self
|
|
10
|
+
def solve(target_url, response, options = {})
|
|
11
|
+
return nil unless response
|
|
12
|
+
|
|
13
|
+
html = response.body.to_s
|
|
14
|
+
return nil if html.empty?
|
|
15
|
+
|
|
16
|
+
uri = URI.parse(target_url) rescue nil
|
|
17
|
+
base_url = uri ? "#{uri.scheme}://#{uri.host}" : "https://www.google.com"
|
|
18
|
+
|
|
19
|
+
impersonate = options[:impersonate] || HttpMimic.configuration.default_impersonate || 'chrome131'
|
|
20
|
+
current_cookies = (response.cookies || Cookies.new).dup
|
|
21
|
+
|
|
22
|
+
# Strategy 1: Session warmup from Google root if cookies are sparse
|
|
23
|
+
if current_cookies.empty? || !current_cookies.key?('NID')
|
|
24
|
+
warmup_resp = HttpMimic.get(
|
|
25
|
+
"#{base_url}/",
|
|
26
|
+
impersonate: impersonate,
|
|
27
|
+
cookies: current_cookies,
|
|
28
|
+
auto_fallback: false,
|
|
29
|
+
solve_waf: false,
|
|
30
|
+
persist_cookies: false
|
|
31
|
+
)
|
|
32
|
+
warmup_resp.cookies.each { |k, v| current_cookies[k] = v } if warmup_resp&.cookies
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Strategy 2: Attempt QuickJS solve if script payload exists
|
|
36
|
+
scripts = html.scan(/<script[^>]*>([\s\S]*?)<\/script>/i).map(&:first)
|
|
37
|
+
if scripts.any? { |s| s.include?('knitsail') || s.include?('SG_SS') || s.include?('closureDynamicButton') }
|
|
38
|
+
sg_ss_cookie = solve_with_quickjs(target_url, scripts, current_cookies)
|
|
39
|
+
current_cookies['SG_SS'] = sg_ss_cookie if sg_ss_cookie && !sg_ss_cookie.start_with?('E:')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Strategy 3: Retry search query with same-origin referral headers and updated cookies
|
|
43
|
+
retry_headers = (options[:headers] || {}).dup
|
|
44
|
+
retry_headers['Referer'] ||= "#{base_url}/"
|
|
45
|
+
retry_headers['sec-fetch-site'] ||= 'same-origin'
|
|
46
|
+
|
|
47
|
+
retry_opts = options.merge(
|
|
48
|
+
cookies: current_cookies,
|
|
49
|
+
headers: retry_headers,
|
|
50
|
+
auto_fallback: false,
|
|
51
|
+
solve_waf: false
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
HttpMimic.get(target_url, **retry_opts)
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
HttpMimic.logger.debug("[HttpMimic::Waf::GoogleSolver] Failed to solve Google challenge: #{e.message}") if HttpMimic.logger
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def solve_with_quickjs(target_url, scripts, cookies)
|
|
63
|
+
context_js_path = File.expand_path('browser_context.js', __dir__)
|
|
64
|
+
context_js = File.read(context_js_path)
|
|
65
|
+
cookie_str = cookies.respond_to?(:to_cookie_string) ? cookies.to_cookie_string : cookies.to_s
|
|
66
|
+
|
|
67
|
+
driver_script = <<~JS
|
|
68
|
+
globalThis.__TARGET_URL__ = #{target_url.to_json};
|
|
69
|
+
globalThis.__DOCUMENT_TITLE__ = "Google Search";
|
|
70
|
+
globalThis.__INITIAL_COOKIES__ = #{cookie_str.to_json};
|
|
71
|
+
globalThis.__REFERRER__ = "https://www.google.com/";
|
|
72
|
+
|
|
73
|
+
#{context_js}
|
|
74
|
+
|
|
75
|
+
// Google specific environment enhancements
|
|
76
|
+
globalThis.sessionStorage = {
|
|
77
|
+
_data: {},
|
|
78
|
+
getItem(k) { return this._data[k] || null; },
|
|
79
|
+
setItem(k, v) { this._data[k] = String(v); },
|
|
80
|
+
removeItem(k) { delete this._data[k]; },
|
|
81
|
+
clear() { this._data = {}; }
|
|
82
|
+
};
|
|
83
|
+
globalThis.localStorage = globalThis.sessionStorage;
|
|
84
|
+
globalThis._F_css = function() {};
|
|
85
|
+
globalThis.google = { c: { c: { a: true } } };
|
|
86
|
+
|
|
87
|
+
#{scripts.join("\n;\n")}
|
|
88
|
+
|
|
89
|
+
// Extract p token and invoke knitsail if present
|
|
90
|
+
var allScripts = #{scripts.join("\n").to_json};
|
|
91
|
+
var pMatch = allScripts.match(/var p=\\x27([^\\x27]+)\\x27/);
|
|
92
|
+
var pVal = pMatch ? pMatch[1] : null;
|
|
93
|
+
|
|
94
|
+
var solved_token = null;
|
|
95
|
+
if (globalThis.knitsail && typeof globalThis.knitsail.a === "function" && pVal) {
|
|
96
|
+
try {
|
|
97
|
+
globalThis.knitsail.a(pVal, function(resultCallback) {
|
|
98
|
+
if (typeof resultCallback === "function") {
|
|
99
|
+
resultCallback(function(token) {
|
|
100
|
+
solved_token = token;
|
|
101
|
+
}, [{}]);
|
|
102
|
+
}
|
|
103
|
+
}, false, undefined, undefined, undefined, undefined, true);
|
|
104
|
+
} catch(e) {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (typeof globalThis.__drainEventLoop === "function") {
|
|
108
|
+
globalThis.__drainEventLoop(50);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
token: solved_token
|
|
113
|
+
});
|
|
114
|
+
JS
|
|
115
|
+
|
|
116
|
+
result = JSRuntime.eval_json(driver_script)
|
|
117
|
+
result ? result['token'] : nil
|
|
118
|
+
rescue StandardError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/http_mimic/waf.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'http_mimic/waf/detector'
|
|
4
4
|
require 'http_mimic/waf/akamai_solver'
|
|
5
|
+
require 'http_mimic/waf/google_solver'
|
|
5
6
|
|
|
6
7
|
module HttpMimic
|
|
7
8
|
module Waf
|
|
@@ -13,6 +14,8 @@ module HttpMimic
|
|
|
13
14
|
case waf_type
|
|
14
15
|
when :akamai
|
|
15
16
|
AkamaiSolver.solve(url, response, options)
|
|
17
|
+
when :google
|
|
18
|
+
GoogleSolver.solve(url, response, options)
|
|
16
19
|
else
|
|
17
20
|
nil
|
|
18
21
|
end
|
data/lib/http_mimic.rb
CHANGED
|
@@ -19,6 +19,8 @@ require 'http_mimic/module_methods'
|
|
|
19
19
|
require 'http_mimic/js_runtime'
|
|
20
20
|
require 'http_mimic/waf'
|
|
21
21
|
require 'http_mimic/cookie_store'
|
|
22
|
+
require 'http_mimic/obscura'
|
|
23
|
+
require 'http_mimic/spa_detector'
|
|
22
24
|
|
|
23
25
|
module HttpMimic
|
|
24
26
|
extend ModuleMethods
|
|
@@ -94,6 +96,30 @@ module HttpMimic
|
|
|
94
96
|
CookieStore.clear(host)
|
|
95
97
|
end
|
|
96
98
|
|
|
99
|
+
# Obscura headless SPA engine helpers
|
|
100
|
+
def download_obscura!(version: nil, force: false)
|
|
101
|
+
Downloader.download_obscura!(version: version, force: force)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def obscura_installed?(version: nil)
|
|
105
|
+
Downloader.obscura_installed?(version: version)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def obscura_path
|
|
109
|
+
Downloader.obscura_path
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Render SPA page using Obscura headless engine
|
|
113
|
+
def render(url, options = {})
|
|
114
|
+
Obscura.render(url, options)
|
|
115
|
+
end
|
|
116
|
+
alias spa render
|
|
117
|
+
|
|
118
|
+
# Check if a response represents an unhydrated SPA shell
|
|
119
|
+
def spa?(response)
|
|
120
|
+
SpaDetector.spa?(response)
|
|
121
|
+
end
|
|
122
|
+
|
|
97
123
|
def included(base)
|
|
98
124
|
base.extend(ModuleMethods)
|
|
99
125
|
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.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- anxgang
|
|
@@ -72,13 +72,16 @@ files:
|
|
|
72
72
|
- lib/http_mimic/headers.rb
|
|
73
73
|
- lib/http_mimic/js_runtime.rb
|
|
74
74
|
- lib/http_mimic/module_methods.rb
|
|
75
|
+
- lib/http_mimic/obscura.rb
|
|
75
76
|
- lib/http_mimic/request.rb
|
|
76
77
|
- lib/http_mimic/response.rb
|
|
77
78
|
- lib/http_mimic/response_parser.rb
|
|
79
|
+
- lib/http_mimic/spa_detector.rb
|
|
78
80
|
- lib/http_mimic/version.rb
|
|
79
81
|
- lib/http_mimic/waf.rb
|
|
80
82
|
- lib/http_mimic/waf/akamai_solver.rb
|
|
81
83
|
- lib/http_mimic/waf/detector.rb
|
|
84
|
+
- lib/http_mimic/waf/google_solver.rb
|
|
82
85
|
homepage: https://github.com/anxgang/http_mimic
|
|
83
86
|
licenses:
|
|
84
87
|
- MIT
|