http_mimic 0.3.2 → 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 +53 -0
- data/README.md +89 -1
- data/lib/http_mimic/command_builder.rb +109 -12
- data/lib/http_mimic/configuration.rb +47 -4
- data/lib/http_mimic/cookie_store.rb +118 -0
- data/lib/http_mimic/cookies.rb +38 -0
- data/lib/http_mimic/downloader.rb +199 -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 +16 -0
- data/lib/http_mimic/obscura.rb +139 -0
- data/lib/http_mimic/request.rb +75 -1
- data/lib/http_mimic/spa_detector.rb +95 -0
- 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 +73 -0
- data/lib/http_mimic/waf/google_solver.rb +124 -0
- data/lib/http_mimic/waf.rb +25 -0
- data/lib/http_mimic.rb +77 -0
- metadata +9 -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,73 @@
|
|
|
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 google?(response)
|
|
13
|
+
:google
|
|
14
|
+
elsif cloudflare?(response)
|
|
15
|
+
:cloudflare
|
|
16
|
+
elsif datadome?(response)
|
|
17
|
+
:datadome
|
|
18
|
+
else
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def challenge_page?(response)
|
|
24
|
+
return false unless response
|
|
25
|
+
body = response.body.to_s
|
|
26
|
+
return true if body.include?('sec-if-cpt-container') || body.include?('sec-bc-button-parent')
|
|
27
|
+
return true if body.include?('challenges.cloudflare.com') || body.include?('cf-turnstile')
|
|
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')
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def akamai?(response)
|
|
43
|
+
return true if response.headers['set-cookie']&.include?('_abck=')
|
|
44
|
+
return true if response.cookies.key?('_abck') || response.cookies.key?('bm_sz') || response.cookies.key?('ak_bmsc')
|
|
45
|
+
return true if response.headers['x-akamai-transformed'] || response.headers['x-reference-error']
|
|
46
|
+
|
|
47
|
+
body = response.body.to_s
|
|
48
|
+
return true if body.include?('Reference Error:') && body.include?('Akamai')
|
|
49
|
+
return true if body.include?('bmak') || body.include?('sensor_data')
|
|
50
|
+
return true if body.include?('sec-if-cpt-container') || body.include?('sec-bc-button-parent')
|
|
51
|
+
return true if body =~ /<script[^>]*src=["\x27]([^"\x27]*\/[a-zA-Z0-9_-]{10,}\?[a-zA-Z0-9_=-]+)["\x27]/
|
|
52
|
+
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cloudflare?(response)
|
|
57
|
+
return true if response.headers['server']&.downcase&.include?('cloudflare')
|
|
58
|
+
return true if response.headers['cf-ray'] || response.cookies.key?('cf_clearance')
|
|
59
|
+
return true if response.body.to_s.include?('challenges.cloudflare.com')
|
|
60
|
+
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def datadome?(response)
|
|
65
|
+
return true if response.headers['x-datadome'] || response.cookies.key?('datadome')
|
|
66
|
+
return true if response.headers['server']&.downcase&.include?('datadome')
|
|
67
|
+
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'http_mimic/waf/detector'
|
|
4
|
+
require 'http_mimic/waf/akamai_solver'
|
|
5
|
+
require 'http_mimic/waf/google_solver'
|
|
6
|
+
|
|
7
|
+
module HttpMimic
|
|
8
|
+
module Waf
|
|
9
|
+
class << self
|
|
10
|
+
def solve(url, response, options = {})
|
|
11
|
+
waf_type = Detector.detect(response)
|
|
12
|
+
return nil unless waf_type
|
|
13
|
+
|
|
14
|
+
case waf_type
|
|
15
|
+
when :akamai
|
|
16
|
+
AkamaiSolver.solve(url, response, options)
|
|
17
|
+
when :google
|
|
18
|
+
GoogleSolver.solve(url, response, options)
|
|
19
|
+
else
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/http_mimic.rb
CHANGED
|
@@ -16,6 +16,11 @@ 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'
|
|
22
|
+
require 'http_mimic/obscura'
|
|
23
|
+
require 'http_mimic/spa_detector'
|
|
19
24
|
|
|
20
25
|
module HttpMimic
|
|
21
26
|
extend ModuleMethods
|
|
@@ -43,6 +48,78 @@ module HttpMimic
|
|
|
43
48
|
Downloader.installed?(version: version)
|
|
44
49
|
end
|
|
45
50
|
|
|
51
|
+
# Manually download and install QuickJS binary driver
|
|
52
|
+
def download_qjs!(version: nil, force: false)
|
|
53
|
+
Downloader.download_qjs!(version: version, force: force)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if QuickJS driver is installed locally
|
|
57
|
+
def qjs_installed?(version: nil)
|
|
58
|
+
Downloader.qjs_installed?(version: version)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Return local path to QuickJS binary
|
|
62
|
+
def qjs_path
|
|
63
|
+
Downloader.qjs_path
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Evaluate JavaScript code using QuickJS
|
|
67
|
+
def eval_js(code, options = {})
|
|
68
|
+
JSRuntime.eval(code, timeout: options[:timeout], install_dir: options[:install_dir], auto_download: options[:auto_download])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Evaluate JavaScript and auto-parse JSON result
|
|
72
|
+
def eval_js_json(code, options = {})
|
|
73
|
+
JSRuntime.eval_json(code, timeout: options[:timeout], install_dir: options[:install_dir], auto_download: options[:auto_download])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Automatically detect WAF type from response
|
|
77
|
+
def detect_waf(response)
|
|
78
|
+
Waf::Detector.detect(response)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Attempt to solve WAF challenge for a blocked response
|
|
82
|
+
def solve_waf(url, response, options = {})
|
|
83
|
+
Waf.solve(url, response, options)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Persistent Host CookieStore helpers
|
|
87
|
+
def load_cookies(host, max_age: nil)
|
|
88
|
+
CookieStore.load(host, max_age: max_age)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def save_cookies(host, cookies, ttl: nil)
|
|
92
|
+
CookieStore.save(host, cookies, ttl: ttl || CookieStore::DEFAULT_TTL)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def clear_cookies!(host = nil)
|
|
96
|
+
CookieStore.clear(host)
|
|
97
|
+
end
|
|
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
|
+
|
|
46
123
|
def included(base)
|
|
47
124
|
base.extend(ModuleMethods)
|
|
48
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
|
|
@@ -65,15 +65,23 @@ 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
|
|
75
|
+
- lib/http_mimic/obscura.rb
|
|
73
76
|
- lib/http_mimic/request.rb
|
|
74
77
|
- lib/http_mimic/response.rb
|
|
75
78
|
- lib/http_mimic/response_parser.rb
|
|
79
|
+
- lib/http_mimic/spa_detector.rb
|
|
76
80
|
- lib/http_mimic/version.rb
|
|
81
|
+
- lib/http_mimic/waf.rb
|
|
82
|
+
- lib/http_mimic/waf/akamai_solver.rb
|
|
83
|
+
- lib/http_mimic/waf/detector.rb
|
|
84
|
+
- lib/http_mimic/waf/google_solver.rb
|
|
77
85
|
homepage: https://github.com/anxgang/http_mimic
|
|
78
86
|
licenses:
|
|
79
87
|
- MIT
|