chromate-rb 0.0.1.pre → 0.0.2.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +54 -3
- data/README.md +33 -6
- data/Rakefile +48 -16
- data/docker_root/Gemfile +4 -0
- data/docker_root/Gemfile.lock +28 -0
- data/docker_root/TestInDocker.gif +0 -0
- data/docker_root/app.rb +92 -0
- data/dockerfiles/Dockerfile +21 -7
- data/dockerfiles/README.md +49 -0
- data/docs/README.md +74 -0
- data/docs/browser.md +149 -92
- data/docs/element.md +289 -0
- data/lib/bot_browser/downloader.rb +52 -0
- data/lib/bot_browser/installer.rb +81 -0
- data/lib/bot_browser.rb +39 -0
- data/lib/chromate/actions/dom.rb +28 -9
- data/lib/chromate/actions/navigate.rb +4 -5
- data/lib/chromate/actions/screenshot.rb +30 -11
- data/lib/chromate/actions/stealth.rb +47 -0
- data/lib/chromate/browser.rb +64 -12
- data/lib/chromate/c_logger.rb +7 -0
- data/lib/chromate/client.rb +40 -18
- data/lib/chromate/configuration.rb +31 -14
- data/lib/chromate/element.rb +65 -15
- data/lib/chromate/elements/select.rb +59 -7
- data/lib/chromate/hardwares/keyboard_controller.rb +34 -0
- data/lib/chromate/hardwares/keyboards/virtual_controller.rb +65 -0
- data/lib/chromate/hardwares/mouse_controller.rb +47 -11
- data/lib/chromate/hardwares/mouses/linux_controller.rb +124 -21
- data/lib/chromate/hardwares/mouses/mac_os_controller.rb +6 -6
- data/lib/chromate/hardwares/mouses/virtual_controller.rb +95 -7
- data/lib/chromate/hardwares/mouses/x11.rb +36 -0
- data/lib/chromate/hardwares.rb +16 -0
- data/lib/chromate/helpers.rb +22 -15
- data/lib/chromate/user_agent.rb +39 -15
- data/lib/chromate/version.rb +1 -1
- data/lib/chromate.rb +2 -0
- data/logo.png +0 -0
- data/results/bot.png +0 -0
- data/results/brotector.png +0 -0
- data/results/cloudflare.png +0 -0
- data/results/headers.png +0 -0
- data/results/pixelscan.png +0 -0
- metadata +20 -2
@@ -9,25 +9,27 @@ module Chromate
|
|
9
9
|
include Helpers
|
10
10
|
include Exceptions
|
11
11
|
DEFAULT_ARGS = [
|
12
|
-
'--no-first-run',
|
13
|
-
'--no-default-browser-check',
|
14
|
-
'--disable-blink-features=AutomationControlled',
|
15
|
-
'--disable-extensions',
|
16
|
-
'--disable-infobars',
|
17
|
-
'--no-sandbox',
|
18
|
-
'--
|
19
|
-
'--
|
20
|
-
'--disable-gpu',
|
21
|
-
'--disable-
|
12
|
+
'--no-first-run', # Skip the first run wizard
|
13
|
+
'--no-default-browser-check', # Disable the default browser check
|
14
|
+
'--disable-blink-features=AutomationControlled', # Disable the AutomationControlled feature
|
15
|
+
'--disable-extensions', # Disable extensions
|
16
|
+
'--disable-infobars', # Disable the infobar that asks if you want to install Chrome
|
17
|
+
'--no-sandbox', # Required for chrome devtools to work
|
18
|
+
'--test-type', # Remove the not allowed message for --no-sandbox flag
|
19
|
+
'--disable-dev-shm-usage', # Disable /dev/shm usage
|
20
|
+
'--disable-gpu', # Disable the GPU
|
21
|
+
'--disable-popup-blocking', # Disable popup blocking
|
22
|
+
'--ignore-certificate-errors', # Ignore certificate errors
|
22
23
|
'--window-size=1920,1080', # TODO: Make this automatic
|
23
|
-
'--hide-crash-restore-bubble'
|
24
|
+
'--hide-crash-restore-bubble' # Hide the crash restore bubble
|
24
25
|
].freeze
|
25
26
|
HEADLESS_ARGS = [
|
26
27
|
'--headless=new',
|
27
28
|
'--window-position=2400,2400'
|
28
29
|
].freeze
|
29
30
|
XVFB_ARGS = [
|
30
|
-
'--window-position=0,0'
|
31
|
+
'--window-position=0,0',
|
32
|
+
'--start-fullscreen'
|
31
33
|
].freeze
|
32
34
|
DISABLED_FEATURES = %w[
|
33
35
|
Translate
|
@@ -45,14 +47,15 @@ module Chromate
|
|
45
47
|
enable-automation
|
46
48
|
].freeze
|
47
49
|
|
48
|
-
attr_accessor :user_data_dir, :headless, :xfvb, :native_control, :
|
49
|
-
:disable_features
|
50
|
+
attr_accessor :user_data_dir, :headless, :xfvb, :native_control, :startup_patch,
|
51
|
+
:args, :headless_args, :xfvb_args, :exclude_switches, :proxy, :disable_features
|
50
52
|
|
51
53
|
def initialize
|
52
54
|
@user_data_dir = File.expand_path('~/.config/google-chrome/Default')
|
53
55
|
@headless = true
|
54
56
|
@xfvb = false
|
55
57
|
@native_control = false
|
58
|
+
@startup_patch = true
|
56
59
|
@proxy = nil
|
57
60
|
@args = [] + DEFAULT_ARGS
|
58
61
|
@headless_args = [] + HEADLESS_ARGS
|
@@ -63,18 +66,27 @@ module Chromate
|
|
63
66
|
@args << '--use-angle=metal' if mac?
|
64
67
|
end
|
65
68
|
|
69
|
+
# @return [Chromate::Configuration]
|
66
70
|
def self.config
|
67
71
|
@config ||= Configuration.new
|
68
72
|
end
|
69
73
|
|
74
|
+
# @yield [Chromate::Configuration]
|
70
75
|
def self.configure
|
71
76
|
yield(config)
|
72
77
|
end
|
73
78
|
|
79
|
+
# @return [Chromate::Configuration]
|
74
80
|
def config
|
75
81
|
self.class.config
|
76
82
|
end
|
77
83
|
|
84
|
+
# @return [Boolean]
|
85
|
+
def patch?
|
86
|
+
@startup_patch
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [String]
|
78
90
|
def chrome_path
|
79
91
|
return ENV['CHROME_BIN'] if ENV['CHROME_BIN']
|
80
92
|
|
@@ -89,6 +101,10 @@ module Chromate
|
|
89
101
|
end
|
90
102
|
end
|
91
103
|
|
104
|
+
# @option [Boolean] headless
|
105
|
+
# @option [Boolean] xfvb
|
106
|
+
# @option [Hash] proxy
|
107
|
+
# @option [Array<String>] disable_features
|
92
108
|
def generate_arguments(headless: @headless, xfvb: @xfvb, proxy: @proxy, disable_features: @disable_features, **_args)
|
93
109
|
dynamic_args = []
|
94
110
|
|
@@ -100,6 +116,7 @@ module Chromate
|
|
100
116
|
@args + dynamic_args
|
101
117
|
end
|
102
118
|
|
119
|
+
# @return [Hash]
|
103
120
|
def options
|
104
121
|
{
|
105
122
|
chrome_path: chrome_path,
|
data/lib/chromate/element.rb
CHANGED
@@ -13,7 +13,7 @@ module Chromate
|
|
13
13
|
super("Unable to resolve element with selector: #{selector}")
|
14
14
|
end
|
15
15
|
end
|
16
|
-
attr_reader :selector, :client
|
16
|
+
attr_reader :selector, :client
|
17
17
|
|
18
18
|
# @param [String] selector
|
19
19
|
# @param [Chromate::Client] client
|
@@ -26,36 +26,41 @@ module Chromate
|
|
26
26
|
@object_id = object_id
|
27
27
|
@node_id = node_id
|
28
28
|
@object_id, @node_id = find(selector, root_id) unless @object_id && @node_id
|
29
|
-
@root_id
|
30
|
-
|
29
|
+
@root_id = root_id || document['root']['nodeId']
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Chromate::Hardwares::MouseController]
|
33
|
+
def mouse
|
34
|
+
@mouse ||= Hardwares.mouse(client: client, element: self)
|
31
35
|
end
|
32
36
|
|
37
|
+
# @return [Chromate::Hardwares::KeyboardController]
|
38
|
+
def keyboard
|
39
|
+
@keyboard ||= Hardwares.keyboard(client: client, element: self)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [String]
|
33
43
|
def inspect
|
34
44
|
value = selector.length > 20 ? "#{selector[0..20]}..." : selector
|
35
45
|
"#<Chromate::Element:#{value}>"
|
36
46
|
end
|
37
47
|
|
48
|
+
# @return [String]
|
38
49
|
def text
|
39
|
-
return @text if @text
|
40
|
-
|
41
50
|
result = client.send_message('Runtime.callFunctionOn', functionDeclaration: 'function() { return this.innerText; }', objectId: @object_id)
|
42
|
-
|
51
|
+
result['result']['value']
|
43
52
|
end
|
44
53
|
|
45
54
|
# @return [String]
|
46
55
|
def html
|
47
|
-
|
48
|
-
|
49
|
-
@html = client.send_message('DOM.getOuterHTML', objectId: @object_id)
|
50
|
-
@html = @html['outerHTML']
|
56
|
+
html = client.send_message('DOM.getOuterHTML', objectId: @object_id)
|
57
|
+
html['outerHTML']
|
51
58
|
end
|
52
59
|
|
53
60
|
# @return [Hash]
|
54
61
|
def attributes
|
55
|
-
return @attributes if @attributes
|
56
|
-
|
57
62
|
result = client.send_message('DOM.getAttributes', nodeId: @node_id)
|
58
|
-
|
63
|
+
Hash[*result['attributes']]
|
59
64
|
end
|
60
65
|
|
61
66
|
# @param [String] name
|
@@ -94,6 +99,13 @@ module Chromate
|
|
94
99
|
bounding_box['height']
|
95
100
|
end
|
96
101
|
|
102
|
+
# @return [self]
|
103
|
+
def focus
|
104
|
+
client.send_message('DOM.focus', nodeId: @node_id)
|
105
|
+
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
97
109
|
# @return [self]
|
98
110
|
def click
|
99
111
|
mouse.click
|
@@ -109,9 +121,24 @@ module Chromate
|
|
109
121
|
end
|
110
122
|
|
111
123
|
# @param [String] text
|
124
|
+
# @return [self]
|
112
125
|
def type(text)
|
113
|
-
|
114
|
-
|
126
|
+
focus
|
127
|
+
keyboard.type(text)
|
128
|
+
|
129
|
+
self
|
130
|
+
end
|
131
|
+
|
132
|
+
# @return [self]
|
133
|
+
def press_enter
|
134
|
+
keyboard.press_key('Enter')
|
135
|
+
submit_parent_form
|
136
|
+
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
def drop_to(element)
|
141
|
+
mouse.drag_and_drop_to(element)
|
115
142
|
|
116
143
|
self
|
117
144
|
end
|
@@ -171,6 +198,8 @@ module Chromate
|
|
171
198
|
|
172
199
|
private
|
173
200
|
|
201
|
+
# @param [String] event
|
202
|
+
# @return [void]
|
174
203
|
def dispatch_event(event)
|
175
204
|
client.send_message('DOM.dispatchEvent', nodeId: @node_id, type: event)
|
176
205
|
end
|
@@ -190,5 +219,26 @@ module Chromate
|
|
190
219
|
def document
|
191
220
|
@document ||= client.send_message('DOM.getDocument')
|
192
221
|
end
|
222
|
+
|
223
|
+
def submit_parent_form
|
224
|
+
script = <<~JAVASCRIPT
|
225
|
+
function() {
|
226
|
+
const form = this.closest('form');
|
227
|
+
if (form) {
|
228
|
+
const submitEvent = new Event('submit', {
|
229
|
+
bubbles: true,
|
230
|
+
cancelable: true
|
231
|
+
});
|
232
|
+
if (form.dispatchEvent(submitEvent)) {
|
233
|
+
form.submit();
|
234
|
+
}
|
235
|
+
}
|
236
|
+
}
|
237
|
+
JAVASCRIPT
|
238
|
+
|
239
|
+
client.send_message('Runtime.callFunctionOn',
|
240
|
+
functionDeclaration: script,
|
241
|
+
objectId: @object_id)
|
242
|
+
end
|
193
243
|
end
|
194
244
|
end
|
@@ -5,14 +5,66 @@ require 'chromate/element'
|
|
5
5
|
module Chromate
|
6
6
|
module Elements
|
7
7
|
class Select < Element
|
8
|
-
# @param [String]
|
8
|
+
# @param [String] value
|
9
|
+
# @return [self]
|
9
10
|
def select_option(value)
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
11
|
+
script = javascript
|
12
|
+
|
13
|
+
client.send_message('Runtime.callFunctionOn',
|
14
|
+
functionDeclaration: script,
|
15
|
+
objectId: @object_id,
|
16
|
+
arguments: [{ value: value }])
|
17
|
+
|
18
|
+
self
|
19
|
+
rescue StandardError => e
|
20
|
+
raise ArgumentError, "Option '#{value}' not found in select" if e.message.include?('Option')
|
21
|
+
|
22
|
+
raise e
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String|nil]
|
26
|
+
def selected_value
|
27
|
+
result = client.send_message('Runtime.callFunctionOn',
|
28
|
+
functionDeclaration: 'function() { return this.value; }',
|
29
|
+
objectId: @object_id)
|
30
|
+
result.dig('result', 'value')
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [String|nil]
|
34
|
+
def selected_text
|
35
|
+
result = client.send_message('Runtime.callFunctionOn',
|
36
|
+
functionDeclaration: 'function() {
|
37
|
+
const option = this.options[this.selectedIndex];
|
38
|
+
return option ? option.textContent.trim() : null;
|
39
|
+
}',
|
40
|
+
objectId: @object_id)
|
41
|
+
result.dig('result', 'value')
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [String]
|
45
|
+
def javascript
|
46
|
+
<<~JAVASCRIPT
|
47
|
+
function() {
|
48
|
+
this.focus();
|
49
|
+
this.dispatchEvent(new MouseEvent('mousedown'));
|
50
|
+
|
51
|
+
const options = Array.from(this.options);
|
52
|
+
const option = options.find(opt =>#{" "}
|
53
|
+
opt.value === arguments[0] || opt.textContent.trim() === arguments[0]
|
54
|
+
);
|
55
|
+
|
56
|
+
if (!option) {
|
57
|
+
throw new Error(`Option '${arguments[0]}' not found in select`);
|
58
|
+
}
|
59
|
+
|
60
|
+
this.value = option.value;
|
61
|
+
|
62
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
63
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
64
|
+
|
65
|
+
this.blur();
|
66
|
+
}
|
67
|
+
JAVASCRIPT
|
16
68
|
end
|
17
69
|
end
|
18
70
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chromate
|
4
|
+
module Hardwares
|
5
|
+
class KeyboardController
|
6
|
+
attr_accessor :element, :client
|
7
|
+
|
8
|
+
# @param [Chromate::Element] element
|
9
|
+
# @param [Chromate::Client] client
|
10
|
+
def initialize(element: nil, client: nil)
|
11
|
+
@element = element
|
12
|
+
@client = client
|
13
|
+
@type_interval = rand(0.05..0.1)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param [String] key
|
17
|
+
# @return [self]
|
18
|
+
def press_key(_key)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [String] text
|
23
|
+
# @return [self]
|
24
|
+
def type(text)
|
25
|
+
text.each_char do |char|
|
26
|
+
press_key(char)
|
27
|
+
sleep(@type_interval)
|
28
|
+
end
|
29
|
+
|
30
|
+
self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chromate
|
4
|
+
module Hardwares
|
5
|
+
module Keyboards
|
6
|
+
class VirtualController < Chromate::Hardwares::KeyboardController
|
7
|
+
def press_key(key = 'Enter')
|
8
|
+
params = {
|
9
|
+
key: key,
|
10
|
+
code: key_to_code(key),
|
11
|
+
windowsVirtualKeyCode: key_to_virtual_code(key)
|
12
|
+
}
|
13
|
+
|
14
|
+
params[:text] = key if key.length == 1
|
15
|
+
|
16
|
+
# Dispatch keyDown event
|
17
|
+
client.send_message('Input.dispatchKeyEvent', params.merge(type: 'keyDown'))
|
18
|
+
|
19
|
+
# Dispatch keyUp event
|
20
|
+
client.send_message('Input.dispatchKeyEvent', params.merge(type: 'keyUp'))
|
21
|
+
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# @param [String] key
|
28
|
+
# @return [String]
|
29
|
+
def key_to_code(key)
|
30
|
+
case key
|
31
|
+
when 'Enter' then 'Enter'
|
32
|
+
when 'Tab' then 'Tab'
|
33
|
+
when 'Backspace' then 'Backspace'
|
34
|
+
when 'Delete' then 'Delete'
|
35
|
+
when 'Escape' then 'Escape'
|
36
|
+
when 'ArrowLeft' then 'ArrowLeft'
|
37
|
+
when 'ArrowRight' then 'ArrowRight'
|
38
|
+
when 'ArrowUp' then 'ArrowUp'
|
39
|
+
when 'ArrowDown' then 'ArrowDown'
|
40
|
+
else
|
41
|
+
"Key#{key.upcase}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param [String] key
|
46
|
+
# @return [Integer]
|
47
|
+
def key_to_virtual_code(key)
|
48
|
+
case key
|
49
|
+
when 'Enter' then 0x0D
|
50
|
+
when 'Tab' then 0x09
|
51
|
+
when 'Backspace' then 0x08
|
52
|
+
when 'Delete' then 0x2E
|
53
|
+
when 'Escape' then 0x1B
|
54
|
+
when 'ArrowLeft' then 0x25
|
55
|
+
when 'ArrowRight' then 0x27
|
56
|
+
when 'ArrowUp' then 0x26
|
57
|
+
when 'ArrowDown' then 0x28
|
58
|
+
else
|
59
|
+
key.upcase.ord
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -6,62 +6,98 @@ module Chromate
|
|
6
6
|
CLICK_DURATION_RANGE = (0.01..0.1)
|
7
7
|
DOUBLE_CLICK_DURATION_RANGE = (0.1..0.5)
|
8
8
|
|
9
|
-
|
9
|
+
def self.reset_mouse_position
|
10
|
+
@@mouse_position = { x: 0, y: 0 } # rubocop:disable Style/ClassVars
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :element, :client
|
10
14
|
|
11
15
|
# @param [Chromate::Element] element
|
12
16
|
# @param [Chromate::Client] client
|
13
17
|
def initialize(element: nil, client: nil)
|
14
18
|
@element = element
|
15
19
|
@client = client
|
16
|
-
@mouse_position = { x: 0, y: 0 }
|
17
20
|
end
|
18
21
|
|
22
|
+
# @return [Hash]
|
23
|
+
def mouse_position
|
24
|
+
@@mouse_position ||= { x: 0, y: 0 } # rubocop:disable Style/ClassVars
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [self]
|
19
28
|
def hover
|
20
29
|
raise NotImplementedError
|
21
30
|
end
|
22
31
|
|
32
|
+
# @return [self]
|
23
33
|
def click
|
24
34
|
raise NotImplementedError
|
25
35
|
end
|
26
36
|
|
37
|
+
# @return [self]
|
27
38
|
def double_click
|
28
39
|
raise NotImplementedError
|
29
40
|
end
|
30
41
|
|
42
|
+
# @return [self]
|
31
43
|
def right_click
|
32
44
|
raise NotImplementedError
|
33
45
|
end
|
34
46
|
|
47
|
+
# @params [Chromate::Element] element
|
48
|
+
# @return [self]
|
49
|
+
def drag_and_drop_to(element)
|
50
|
+
raise NotImplementedError
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Integer]
|
35
54
|
def position_x
|
36
55
|
mouse_position[:x]
|
37
56
|
end
|
38
57
|
|
58
|
+
# @return [Integer]
|
39
59
|
def position_y
|
40
60
|
mouse_position[:y]
|
41
61
|
end
|
42
62
|
|
43
63
|
private
|
44
64
|
|
65
|
+
# @return [Integer]
|
45
66
|
def target_x
|
46
67
|
element.x + (element.width / 2)
|
47
68
|
end
|
48
69
|
|
70
|
+
# @return [Integer]
|
49
71
|
def target_y
|
50
72
|
element.y + (element.height / 2)
|
51
73
|
end
|
52
74
|
|
53
|
-
|
54
|
-
|
55
|
-
|
75
|
+
# @param [Integer] steps
|
76
|
+
# @return [Array<Hash>]
|
77
|
+
def bezier_curve(steps:, start_x: position_x, start_y: position_y, t_x: target_x, t_y: target_y) # rubocop:disable Metrics/AbcSize
|
78
|
+
# Points for the Bézier curve
|
79
|
+
control_x1 = start_x + (rand(50..150) * (t_x > start_x ? 1 : -1))
|
80
|
+
control_y1 = start_y + (rand(50..150) * (t_y > start_y ? 1 : -1))
|
81
|
+
control_x2 = t_x + (rand(50..150) * (t_x > start_x ? -1 : 1))
|
82
|
+
control_y2 = t_y + (rand(50..150) * (t_y > start_y ? -1 : 1))
|
56
83
|
|
57
|
-
(0..steps).map do |
|
58
|
-
t
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
{ x: new_x, y: new_y }
|
84
|
+
(0..steps).map do |i|
|
85
|
+
t = i.to_f / steps
|
86
|
+
x = (((1 - t)**3) * start_x) + (3 * ((1 - t)**2) * t * control_x1) + (3 * (1 - t) * (t**2) * control_x2) + ((t**3) * t_x)
|
87
|
+
y = (((1 - t)**3) * start_y) + (3 * ((1 - t)**2) * t * control_y1) + (3 * (1 - t) * (t**2) * control_y2) + ((t**3) * t_y)
|
88
|
+
{ x: x, y: y }
|
63
89
|
end
|
64
90
|
end
|
91
|
+
|
92
|
+
# @param [Integer] target_x
|
93
|
+
# @param [Integer] target_y
|
94
|
+
# @return [Hash]
|
95
|
+
def update_mouse_position(target_x, target_y)
|
96
|
+
@@mouse_position[:x] = target_x
|
97
|
+
@@mouse_position[:y] = target_y
|
98
|
+
|
99
|
+
mouse_position
|
100
|
+
end
|
65
101
|
end
|
66
102
|
end
|
67
103
|
end
|