chromate-rb 0.0.1.pre → 0.0.2.pre

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.
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