http_mimic 0.3.1 → 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 +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +74 -16
- data/lib/http_mimic/command_builder.rb +160 -36
- data/lib/http_mimic/configuration.rb +50 -19
- data/lib/http_mimic/cookie_store.rb +118 -0
- data/lib/http_mimic/cookies.rb +38 -0
- data/lib/http_mimic/downloader.rb +102 -0
- data/lib/http_mimic/exceptions.rb +15 -0
- data/lib/http_mimic/js_runtime.rb +78 -0
- data/lib/http_mimic/module_methods.rb +5 -0
- data/lib/http_mimic/request.rb +62 -5
- data/lib/http_mimic/response.rb +84 -0
- data/lib/http_mimic/response_parser.rb +45 -21
- data/lib/http_mimic/version.rb +1 -1
- data/lib/http_mimic/waf/akamai_solver.rb +123 -0
- data/lib/http_mimic/waf/detector.rb +61 -0
- data/lib/http_mimic/waf.rb +22 -0
- data/lib/http_mimic.rb +51 -0
- metadata +6 -1
|
@@ -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.
|
|
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
|