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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/CHANGELOG.md +54 -3
  4. data/README.md +33 -6
  5. data/Rakefile +48 -16
  6. data/docker_root/Gemfile +4 -0
  7. data/docker_root/Gemfile.lock +28 -0
  8. data/docker_root/TestInDocker.gif +0 -0
  9. data/docker_root/app.rb +92 -0
  10. data/dockerfiles/Dockerfile +21 -7
  11. data/dockerfiles/README.md +49 -0
  12. data/docs/README.md +74 -0
  13. data/docs/browser.md +149 -92
  14. data/docs/element.md +289 -0
  15. data/lib/bot_browser/downloader.rb +52 -0
  16. data/lib/bot_browser/installer.rb +81 -0
  17. data/lib/bot_browser.rb +39 -0
  18. data/lib/chromate/actions/dom.rb +28 -9
  19. data/lib/chromate/actions/navigate.rb +4 -5
  20. data/lib/chromate/actions/screenshot.rb +30 -11
  21. data/lib/chromate/actions/stealth.rb +47 -0
  22. data/lib/chromate/browser.rb +64 -12
  23. data/lib/chromate/c_logger.rb +7 -0
  24. data/lib/chromate/client.rb +40 -18
  25. data/lib/chromate/configuration.rb +31 -14
  26. data/lib/chromate/element.rb +65 -15
  27. data/lib/chromate/elements/select.rb +59 -7
  28. data/lib/chromate/hardwares/keyboard_controller.rb +34 -0
  29. data/lib/chromate/hardwares/keyboards/virtual_controller.rb +65 -0
  30. data/lib/chromate/hardwares/mouse_controller.rb +47 -11
  31. data/lib/chromate/hardwares/mouses/linux_controller.rb +124 -21
  32. data/lib/chromate/hardwares/mouses/mac_os_controller.rb +6 -6
  33. data/lib/chromate/hardwares/mouses/virtual_controller.rb +95 -7
  34. data/lib/chromate/hardwares/mouses/x11.rb +36 -0
  35. data/lib/chromate/hardwares.rb +16 -0
  36. data/lib/chromate/helpers.rb +22 -15
  37. data/lib/chromate/user_agent.rb +39 -15
  38. data/lib/chromate/version.rb +1 -1
  39. data/lib/chromate.rb +2 -0
  40. data/logo.png +0 -0
  41. data/results/bot.png +0 -0
  42. data/results/brotector.png +0 -0
  43. data/results/cloudflare.png +0 -0
  44. data/results/headers.png +0 -0
  45. data/results/pixelscan.png +0 -0
  46. 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
- '--disable-popup-blocking',
19
- '--ignore-certificate-errors',
20
- '--disable-gpu',
21
- '--disable-dev-shm-usage',
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, :args, :headless_args, :xfvb_args, :exclude_switches, :proxy,
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,
@@ -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, :mouse
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 = root_id || document['root']['nodeId']
30
- @mouse = Hardwares.mouse(client: client, element: self)
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
- @text = result['result']['value']
51
+ result['result']['value']
43
52
  end
44
53
 
45
54
  # @return [String]
46
55
  def html
47
- return @html if @html
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
- @attributes = Hash[*result['attributes']]
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
- client.send_message('Runtime.callFunctionOn', functionDeclaration: "function(value) { this.value = value; this.dispatchEvent(new Event('input')); }",
114
- objectId: @object_id, arguments: [{ value: text }])
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] selector
8
+ # @param [String] value
9
+ # @return [self]
9
10
  def select_option(value)
10
- click
11
- opt = find_elements('option').find do |option|
12
- option.attributes['value'] == value
13
- end
14
- opt.set_attribute('selected', 'true')
15
- click
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
- attr_accessor :element, :client, :mouse_position
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
- def bezier_curve(steps: 50) # rubocop:disable Metrics/AbcSize
54
- control_x = (target_x / 2)
55
- control_y = (target_y / 2)
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 |t|
58
- t /= steps.to_f
59
- # Compute the position on the quadratic Bezier curve
60
- new_x = (((1 - t)**2) * position_x) + (2 * (1 - t) * t * control_x) + ((t**2) * target_x)
61
- new_y = (((1 - t)**2) * position_y) + (2 * (1 - t) * t * control_y) + ((t**2) * target_y)
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