chromate-rb 0.0.1.pre → 0.0.3.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +72 -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 +87 -0
- data/dockerfiles/Dockerfile +21 -7
- data/dockerfiles/README.md +49 -0
- data/docs/BOT_BROWSER.md +74 -0
- data/docs/README.md +74 -0
- data/docs/browser.md +124 -102
- data/docs/client.md +126 -0
- data/docs/element.md +365 -0
- data/docs/elements/checkbox.md +69 -0
- data/docs/elements/radio.md +57 -0
- data/lib/bot_browser/downloader.rb +64 -0
- data/lib/bot_browser/installer.rb +99 -0
- data/lib/bot_browser.rb +43 -0
- data/lib/chromate/actions/dom.rb +35 -27
- data/lib/chromate/actions/navigate.rb +7 -5
- data/lib/chromate/actions/screenshot.rb +71 -14
- data/lib/chromate/actions/stealth.rb +62 -0
- data/lib/chromate/binary.rb +83 -0
- data/lib/chromate/browser.rb +120 -24
- data/lib/chromate/c_logger.rb +8 -0
- data/lib/chromate/client.rb +65 -26
- data/lib/chromate/configuration.rb +31 -14
- data/lib/chromate/element.rb +119 -16
- data/lib/chromate/elements/checkbox.rb +40 -0
- data/lib/chromate/elements/option.rb +43 -0
- data/lib/chromate/elements/radio.rb +37 -0
- data/lib/chromate/elements/select.rb +50 -6
- data/lib/chromate/elements/tags.rb +29 -0
- data/lib/chromate/exceptions.rb +2 -0
- data/lib/chromate/files/agents.json +11 -0
- data/lib/chromate/files/stealth.js +199 -0
- data/lib/chromate/hardwares/keyboard_controller.rb +45 -0
- data/lib/chromate/hardwares/keyboards/virtual_controller.rb +65 -0
- data/lib/chromate/hardwares/mouse_controller.rb +55 -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 +19 -3
- data/lib/chromate/helpers.rb +22 -15
- data/lib/chromate/user_agent.rb +41 -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 +45 -2
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chromate/element'
|
4
|
+
|
5
|
+
module Chromate
|
6
|
+
module Elements
|
7
|
+
module Tags
|
8
|
+
def select?
|
9
|
+
tag_name == 'select'
|
10
|
+
end
|
11
|
+
|
12
|
+
def option?
|
13
|
+
tag_name == 'option'
|
14
|
+
end
|
15
|
+
|
16
|
+
def radio?
|
17
|
+
tag_name == 'input' && attributes['type'] == 'radio'
|
18
|
+
end
|
19
|
+
|
20
|
+
def checkbox?
|
21
|
+
tag_name == 'input' && attributes['type'] == 'checkbox'
|
22
|
+
end
|
23
|
+
|
24
|
+
def base?
|
25
|
+
!select? && !option? && !radio? && !checkbox?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/chromate/exceptions.rb
CHANGED
@@ -0,0 +1,11 @@
|
|
1
|
+
{
|
2
|
+
"windows": [
|
3
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
|
4
|
+
],
|
5
|
+
"mac": [
|
6
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
|
7
|
+
],
|
8
|
+
"linux": [
|
9
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
|
10
|
+
]
|
11
|
+
}
|
@@ -0,0 +1,199 @@
|
|
1
|
+
/**
|
2
|
+
* Script de Stealth pour masquer l'automatisation
|
3
|
+
*
|
4
|
+
* Ce script est destiné à être injecté via la commande
|
5
|
+
* Page.addScriptToEvaluateOnNewDocument afin qu'il s'exécute
|
6
|
+
* avant le chargement du contenu de la page.
|
7
|
+
*
|
8
|
+
* Il redéfinit plusieurs propriétés du navigateur pour
|
9
|
+
* réduire les indices susceptibles d'indiquer qu'une automatisation est en cours.
|
10
|
+
*/
|
11
|
+
(() => {
|
12
|
+
'use strict';
|
13
|
+
|
14
|
+
// 1) Masquer navigator.webdriver
|
15
|
+
(function removeWebDriverProperty() {
|
16
|
+
// 1) Récupérer le prototype de Navigator
|
17
|
+
const proto = Object.getPrototypeOf(navigator);
|
18
|
+
|
19
|
+
// Vérifier si la propriété webdriver existe sur le prototype
|
20
|
+
if ('webdriver' in proto) {
|
21
|
+
try {
|
22
|
+
// 2) Tenter de supprimer la propriété si elle est configurable
|
23
|
+
const webdriverDescriptor = Object.getOwnPropertyDescriptor(proto, 'webdriver');
|
24
|
+
if (webdriverDescriptor && webdriverDescriptor.configurable) {
|
25
|
+
delete proto.webdriver;
|
26
|
+
} else {
|
27
|
+
// 3) Sinon, on essaye de la redéfinir pour qu'elle retourne undefined et ne soit pas énumérable
|
28
|
+
Object.defineProperty(proto, 'webdriver', {
|
29
|
+
get: () => undefined,
|
30
|
+
configurable: false, // on la rend non-configurable pour éviter d'autres re-déclarations
|
31
|
+
enumerable: false
|
32
|
+
});
|
33
|
+
}
|
34
|
+
} catch (err) {
|
35
|
+
// 4) En cas d'échec (non-configurable), on peut tenter un hack sur l'opérateur 'in'
|
36
|
+
// => ATTENTION : ceci peut avoir des effets de bord dans d'autres scripts
|
37
|
+
patchInOperatorForNavigator('webdriver');
|
38
|
+
}
|
39
|
+
} else {
|
40
|
+
// Si la propriété n'existe pas sur le prototype, vérifier si elle existe directement sur navigator
|
41
|
+
if ('webdriver' in navigator) {
|
42
|
+
try {
|
43
|
+
delete navigator.webdriver;
|
44
|
+
} catch (err) {
|
45
|
+
patchInOperatorForNavigator('webdriver');
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
* Hack optionnel pour empêcher `'webdriver' in navigator` de renvoyer true.
|
52
|
+
* On redéfinit la méthode hasOwnProperty / l'opérateur in pour l'objet navigator.
|
53
|
+
* Cette approche peut avoir des effets de bord, donc à utiliser en dernier recours.
|
54
|
+
*/
|
55
|
+
function patchInOperatorForNavigator(propName) {
|
56
|
+
const originalHasOwn = Object.prototype.hasOwnProperty;
|
57
|
+
Object.prototype.hasOwnProperty = function (property) {
|
58
|
+
// Si c'est navigator et qu'on teste la propriété 'webdriver', on la cache
|
59
|
+
if (this === navigator && property === propName) {
|
60
|
+
return false;
|
61
|
+
}
|
62
|
+
return originalHasOwn.call(this, property);
|
63
|
+
};
|
64
|
+
|
65
|
+
// Variante plus radicale : proxyfier l'objet navigator pour intercepter l'opérateur 'in'.
|
66
|
+
// (Non présenté ici, car encore plus invasif.)
|
67
|
+
}
|
68
|
+
})();
|
69
|
+
|
70
|
+
// 2) Surcharger navigator.permissions.query (pour ne pas renvoyer "default")
|
71
|
+
if (navigator.permissions && navigator.permissions.query) {
|
72
|
+
const originalQuery = navigator.permissions.query;
|
73
|
+
navigator.permissions.query = (params) => {
|
74
|
+
// Ex. : si on interroge les notifications, on renvoie "granted"
|
75
|
+
if (params.name === 'notifications') {
|
76
|
+
return Promise.resolve({ state: 'granted' });
|
77
|
+
}
|
78
|
+
// Pour les autres, on tente la requête d'origine, sinon "granted"
|
79
|
+
return originalQuery(params).catch(() => ({ state: 'granted' }));
|
80
|
+
};
|
81
|
+
}
|
82
|
+
|
83
|
+
// 3) Créer un PluginArray réaliste
|
84
|
+
// - On réutilise le prototype existant pour ne pas éveiller de soupçons
|
85
|
+
const pluginArrayProto = Object.getPrototypeOf(navigator.plugins);
|
86
|
+
function FakePlugin(name, description, filename) {
|
87
|
+
this.name = name;
|
88
|
+
this.description = description;
|
89
|
+
this.filename = filename;
|
90
|
+
}
|
91
|
+
// On pointe vers Plugin.prototype pour se comporter comme un plugin "réel"
|
92
|
+
FakePlugin.prototype = Plugin.prototype;
|
93
|
+
|
94
|
+
const fakePlugins = [
|
95
|
+
new FakePlugin('Chrome PDF Plugin', 'Portable Document Format', 'internal-pdf-viewer'),
|
96
|
+
new FakePlugin('Chrome PDF Viewer', '', 'mhjfbmdgcfjbbpaeojofohoefgiehjai')
|
97
|
+
];
|
98
|
+
|
99
|
+
Object.defineProperty(navigator, 'plugins', {
|
100
|
+
get() {
|
101
|
+
const pluginArray = Object.create(pluginArrayProto);
|
102
|
+
// On copie les faux plugins dans l’objet
|
103
|
+
for (let i = 0; i < fakePlugins.length; i++) {
|
104
|
+
pluginArray[i] = fakePlugins[i];
|
105
|
+
}
|
106
|
+
pluginArray.length = fakePlugins.length;
|
107
|
+
return pluginArray;
|
108
|
+
}
|
109
|
+
});
|
110
|
+
|
111
|
+
// 4) Aligner navigator.languages avec vos entêtes Accept-Language
|
112
|
+
Object.defineProperty(navigator, 'languages', {
|
113
|
+
get: () => ['en-US', 'en'],
|
114
|
+
configurable: true
|
115
|
+
});
|
116
|
+
|
117
|
+
// 5) Override de getContext pour forcer un contexte WebGL factice
|
118
|
+
const originalGetContext = HTMLCanvasElement.prototype.getContext;
|
119
|
+
HTMLCanvasElement.prototype.getContext = function (type, ...args) {
|
120
|
+
if (['webgl', 'experimental-webgl', 'webgl2'].includes(type)) {
|
121
|
+
// Tenter d'obtenir le contexte natif (au cas où)
|
122
|
+
let ctx = originalGetContext.apply(this, [type, ...args]);
|
123
|
+
if (ctx) {
|
124
|
+
// Si un contexte natif est obtenu, retourner ce contexte
|
125
|
+
return ctx;
|
126
|
+
}
|
127
|
+
console.log("No native WebGL context found, returning fake context");
|
128
|
+
// Forcer la création d'un contexte factice
|
129
|
+
const proto = (window.WebGLRenderingContext && window.WebGLRenderingContext.prototype) || {};
|
130
|
+
const fakeContext = Object.create(proto);
|
131
|
+
|
132
|
+
fakeContext.getParameter = function (param) {
|
133
|
+
if (param === 37445) return 'Intel Inc.';
|
134
|
+
if (param === 37446) return 'Intel Iris OpenGL Engine';
|
135
|
+
return null;
|
136
|
+
};
|
137
|
+
fakeContext.getSupportedExtensions = function () {
|
138
|
+
return ['WEBGL_debug_renderer_info'];
|
139
|
+
};
|
140
|
+
fakeContext.getExtension = function (name) {
|
141
|
+
if (name === 'WEBGL_debug_renderer_info') {
|
142
|
+
return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
143
|
+
}
|
144
|
+
return null;
|
145
|
+
};
|
146
|
+
|
147
|
+
// Ajout de stubs pour d'autres méthodes WebGL essentielles
|
148
|
+
fakeContext.clear = function () { };
|
149
|
+
fakeContext.clearColor = function () { };
|
150
|
+
fakeContext.viewport = function () { };
|
151
|
+
fakeContext.createShader = function () { return {}; };
|
152
|
+
fakeContext.shaderSource = function () { };
|
153
|
+
fakeContext.compileShader = function () { };
|
154
|
+
fakeContext.createProgram = function () { return {}; };
|
155
|
+
fakeContext.attachShader = function () { };
|
156
|
+
fakeContext.linkProgram = function () { };
|
157
|
+
fakeContext.useProgram = function () { };
|
158
|
+
|
159
|
+
return fakeContext;
|
160
|
+
}
|
161
|
+
return originalGetContext.apply(this, [type, ...args]);
|
162
|
+
};
|
163
|
+
|
164
|
+
// 6) Optionnel : Override de toDataURL pour renvoyer une image fixe
|
165
|
+
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
166
|
+
HTMLCanvasElement.prototype.toDataURL = function (...args) {
|
167
|
+
const gl = this.getContext('webgl') || this.getContext('experimental-webgl') || this.getContext('webgl2');
|
168
|
+
if (gl) {
|
169
|
+
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII=";
|
170
|
+
}
|
171
|
+
return originalToDataURL.apply(this, args);
|
172
|
+
};
|
173
|
+
|
174
|
+
// 7) Correction des dimensions d’images
|
175
|
+
Object.defineProperty(HTMLImageElement.prototype, 'naturalWidth', {
|
176
|
+
get() { return 128; }
|
177
|
+
});
|
178
|
+
Object.defineProperty(HTMLImageElement.prototype, 'naturalHeight', {
|
179
|
+
get() { return 128; }
|
180
|
+
});
|
181
|
+
|
182
|
+
// 8) Randomisation pour varier l'injection
|
183
|
+
(function () {
|
184
|
+
const randomSuffix = Math.random().toString(36).substring(2);
|
185
|
+
document.documentElement.setAttribute('data-stealth', randomSuffix);
|
186
|
+
})();
|
187
|
+
|
188
|
+
// 9) Masquer la modification des fonctions natives
|
189
|
+
(function () {
|
190
|
+
const originalToString = Function.prototype.toString;
|
191
|
+
Function.prototype.toString = function () {
|
192
|
+
if (this === navigator.permissions.query) {
|
193
|
+
return "function query() { [native code] }";
|
194
|
+
}
|
195
|
+
return originalToString.apply(this, arguments);
|
196
|
+
};
|
197
|
+
})();
|
198
|
+
|
199
|
+
})();
|
@@ -0,0 +1,45 @@
|
|
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 [Chromate::Element] element
|
17
|
+
# @return [self]
|
18
|
+
def set_element(element) # rubocop:disable Naming/AccessorMethodName
|
19
|
+
@element = element
|
20
|
+
@type_interval = rand(0.05..0.1)
|
21
|
+
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param [String] key
|
26
|
+
# @return [self]
|
27
|
+
def press_key(_key)
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param [String] text
|
32
|
+
# @return [self]
|
33
|
+
def type(text)
|
34
|
+
text.each_char do |char|
|
35
|
+
sleep(rand(0.01..0.05)) if rand(10).zero?
|
36
|
+
|
37
|
+
press_key(char)
|
38
|
+
sleep(@type_interval)
|
39
|
+
end
|
40
|
+
|
41
|
+
self
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
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,106 @@ 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
|
+
# @param [Chromate::Element] element
|
23
|
+
# @return [self]
|
24
|
+
def set_element(element) # rubocop:disable Naming/AccessorMethodName
|
25
|
+
@element = element
|
26
|
+
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Hash]
|
31
|
+
def mouse_position
|
32
|
+
@@mouse_position ||= { x: 0, y: 0 } # rubocop:disable Style/ClassVars
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [self]
|
19
36
|
def hover
|
20
37
|
raise NotImplementedError
|
21
38
|
end
|
22
39
|
|
40
|
+
# @return [self]
|
23
41
|
def click
|
24
42
|
raise NotImplementedError
|
25
43
|
end
|
26
44
|
|
45
|
+
# @return [self]
|
27
46
|
def double_click
|
28
47
|
raise NotImplementedError
|
29
48
|
end
|
30
49
|
|
50
|
+
# @return [self]
|
31
51
|
def right_click
|
32
52
|
raise NotImplementedError
|
33
53
|
end
|
34
54
|
|
55
|
+
# @params [Chromate::Element] element
|
56
|
+
# @return [self]
|
57
|
+
def drag_and_drop_to(element)
|
58
|
+
raise NotImplementedError
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Integer]
|
35
62
|
def position_x
|
36
63
|
mouse_position[:x]
|
37
64
|
end
|
38
65
|
|
66
|
+
# @return [Integer]
|
39
67
|
def position_y
|
40
68
|
mouse_position[:y]
|
41
69
|
end
|
42
70
|
|
43
71
|
private
|
44
72
|
|
73
|
+
# @return [Integer]
|
45
74
|
def target_x
|
46
75
|
element.x + (element.width / 2)
|
47
76
|
end
|
48
77
|
|
78
|
+
# @return [Integer]
|
49
79
|
def target_y
|
50
80
|
element.y + (element.height / 2)
|
51
81
|
end
|
52
82
|
|
53
|
-
|
54
|
-
|
55
|
-
|
83
|
+
# @param [Integer] steps
|
84
|
+
# @return [Array<Hash>]
|
85
|
+
def bezier_curve(steps:, start_x: position_x, start_y: position_y, t_x: target_x, t_y: target_y) # rubocop:disable Metrics/AbcSize
|
86
|
+
# Points for the Bézier curve
|
87
|
+
control_x1 = start_x + (rand(50..150) * (t_x > start_x ? 1 : -1))
|
88
|
+
control_y1 = start_y + (rand(50..150) * (t_y > start_y ? 1 : -1))
|
89
|
+
control_x2 = t_x + (rand(50..150) * (t_x > start_x ? -1 : 1))
|
90
|
+
control_y2 = t_y + (rand(50..150) * (t_y > start_y ? -1 : 1))
|
56
91
|
|
57
|
-
(0..steps).map do |
|
58
|
-
t
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
{ x: new_x, y: new_y }
|
92
|
+
(0..steps).map do |i|
|
93
|
+
t = i.to_f / steps
|
94
|
+
x = (((1 - t)**3) * start_x) + (3 * ((1 - t)**2) * t * control_x1) + (3 * (1 - t) * (t**2) * control_x2) + ((t**3) * t_x)
|
95
|
+
y = (((1 - t)**3) * start_y) + (3 * ((1 - t)**2) * t * control_y1) + (3 * (1 - t) * (t**2) * control_y2) + ((t**3) * t_y)
|
96
|
+
{ x: x, y: y }
|
63
97
|
end
|
64
98
|
end
|
99
|
+
|
100
|
+
# @param [Integer] target_x
|
101
|
+
# @param [Integer] target_y
|
102
|
+
# @return [Hash]
|
103
|
+
def update_mouse_position(target_x, target_y)
|
104
|
+
@@mouse_position[:x] = target_x
|
105
|
+
@@mouse_position[:y] = target_y
|
106
|
+
|
107
|
+
mouse_position
|
108
|
+
end
|
65
109
|
end
|
66
110
|
end
|
67
111
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'chromate/helpers'
|
4
|
-
|
4
|
+
require_relative 'x11'
|
5
5
|
|
6
6
|
module Chromate
|
7
7
|
module Hardwares
|
@@ -17,23 +17,29 @@ module Chromate
|
|
17
17
|
raise InvalidPlatformError, 'MouseController is only supported on Linux' unless linux?
|
18
18
|
|
19
19
|
super
|
20
|
+
@display = X11.XOpenDisplay(nil)
|
21
|
+
raise 'Impossible d\'ouvrir l\'affichage X11' if @display.null?
|
22
|
+
|
23
|
+
@root_window = X11.XDefaultRootWindow(@display)
|
20
24
|
end
|
21
25
|
|
22
26
|
def hover
|
23
27
|
focus_chrome_window
|
24
|
-
|
25
|
-
|
26
|
-
current_mouse_position
|
28
|
+
smooth_move_to(target_x, target_y)
|
29
|
+
update_mouse_position(target_x, target_y)
|
27
30
|
end
|
28
31
|
|
29
32
|
def click
|
30
33
|
hover
|
31
34
|
simulate_button_event(LEFT_BUTTON, true)
|
35
|
+
sleep(rand(CLICK_DURATION_RANGE))
|
32
36
|
simulate_button_event(LEFT_BUTTON, false)
|
33
37
|
end
|
34
38
|
|
35
39
|
def right_click
|
40
|
+
hover
|
36
41
|
simulate_button_event(RIGHT_BUTTON, true)
|
42
|
+
sleep(rand(CLICK_DURATION_RANGE))
|
37
43
|
simulate_button_event(RIGHT_BUTTON, false)
|
38
44
|
end
|
39
45
|
|
@@ -43,35 +49,132 @@ module Chromate
|
|
43
49
|
click
|
44
50
|
end
|
45
51
|
|
52
|
+
def drag_and_drop_to(element)
|
53
|
+
hover
|
54
|
+
|
55
|
+
target_x = element.x + (element.width / 2)
|
56
|
+
target_y = element.y + (element.height / 2)
|
57
|
+
start_x = position_x
|
58
|
+
start_y = position_y
|
59
|
+
steps = rand(25..50)
|
60
|
+
duration = rand(0.1..0.3)
|
61
|
+
|
62
|
+
# Generate a Bézier curve for natural movement
|
63
|
+
points = bezier_curve(steps: steps, start_x: start_x, start_y: start_y, t_x: target_x, t_y: target_y)
|
64
|
+
|
65
|
+
# Step 1: Press the left mouse button
|
66
|
+
simulate_button_event(LEFT_BUTTON, true)
|
67
|
+
sleep(rand(CLICK_DURATION_RANGE))
|
68
|
+
|
69
|
+
# Step 2: Drag the element
|
70
|
+
points.each do |point|
|
71
|
+
move_mouse_to(point[:x], point[:y])
|
72
|
+
sleep(duration / steps)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Step 3: Release the left mouse button
|
76
|
+
simulate_button_event(LEFT_BUTTON, false)
|
77
|
+
|
78
|
+
# Update the mouse position
|
79
|
+
update_mouse_position(target_x, target_y)
|
80
|
+
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
46
84
|
private
|
47
85
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
86
|
+
def smooth_move_to(dest_x, dest_y)
|
87
|
+
start_x = position_x
|
88
|
+
start_y = position_y
|
89
|
+
|
90
|
+
steps = rand(25..50)
|
91
|
+
duration = rand(0.1..0.3)
|
51
92
|
|
52
|
-
|
53
|
-
|
93
|
+
# Build a Bézier curve for natural movement
|
94
|
+
points = bezier_curve(steps: steps, start_x: start_x, start_y: start_y, t_x: dest_x, t_y: dest_y)
|
95
|
+
|
96
|
+
# Move the mouse along the Bézier curve
|
97
|
+
points.each do |point|
|
98
|
+
move_mouse_to(point[:x], point[:y])
|
99
|
+
sleep(duration / steps)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def move_mouse_to(x_target, y_target)
|
104
|
+
X11.XWarpPointer(@display, 0, @root_window, 0, 0, 0, 0, x_target.to_i, y_target.to_i)
|
105
|
+
X11.XFlush(@display)
|
106
|
+
end
|
107
|
+
|
108
|
+
def focus_chrome_window
|
109
|
+
chrome_window = find_window_by_name(@root_window, 'Chrome')
|
110
|
+
if chrome_window.zero?
|
111
|
+
Chromate::CLogger.log('No Chrome window found')
|
54
112
|
else
|
55
|
-
|
56
|
-
|
113
|
+
X11.XRaiseWindow(@display, chrome_window)
|
114
|
+
X11.XSetInputFocus(@display, chrome_window, X11::REVERT_TO_PARENT, 0)
|
115
|
+
X11.XFlush(@display)
|
57
116
|
end
|
58
117
|
end
|
59
118
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
119
|
+
def find_window_by_name(window, name) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
120
|
+
root_return = FFI::MemoryPointer.new(:ulong)
|
121
|
+
parent_return = FFI::MemoryPointer.new(:ulong)
|
122
|
+
children_return = FFI::MemoryPointer.new(:pointer)
|
123
|
+
nchildren_return = FFI::MemoryPointer.new(:uint)
|
124
|
+
|
125
|
+
status = X11.XQueryTree(@display, window, root_return, parent_return, children_return, nchildren_return)
|
126
|
+
return 0 if status.zero?
|
127
|
+
|
128
|
+
nchildren = nchildren_return.read_uint
|
129
|
+
children_ptr = children_return.read_pointer
|
130
|
+
|
131
|
+
return 0 if nchildren.zero? || children_ptr.null?
|
132
|
+
|
133
|
+
children = children_ptr.get_array_of_ulong(0, nchildren)
|
134
|
+
found_window = 0
|
135
|
+
|
136
|
+
children.each do |child|
|
137
|
+
window_name_ptr = FFI::MemoryPointer.new(:pointer)
|
138
|
+
status = X11.XFetchName(@display, child, window_name_ptr)
|
139
|
+
if status != 0 && !window_name_ptr.read_pointer.null?
|
140
|
+
window_name = window_name_ptr.read_pointer.read_string
|
141
|
+
if window_name.include?(name)
|
142
|
+
X11.XFree(window_name_ptr.read_pointer)
|
143
|
+
found_window = child
|
144
|
+
break
|
145
|
+
end
|
146
|
+
X11.XFree(window_name_ptr.read_pointer)
|
147
|
+
end
|
148
|
+
# Recursive search for the window
|
149
|
+
found_window = find_window_by_name(child, name)
|
150
|
+
break if found_window != 0
|
67
151
|
end
|
68
152
|
|
69
|
-
|
153
|
+
X11.XFree(children_ptr)
|
154
|
+
found_window
|
155
|
+
end
|
156
|
+
|
157
|
+
def current_mouse_position
|
158
|
+
root_return = FFI::MemoryPointer.new(:ulong)
|
159
|
+
child_return = FFI::MemoryPointer.new(:ulong)
|
160
|
+
root_x = FFI::MemoryPointer.new(:int)
|
161
|
+
root_y = FFI::MemoryPointer.new(:int)
|
162
|
+
win_x = FFI::MemoryPointer.new(:int)
|
163
|
+
win_y = FFI::MemoryPointer.new(:int)
|
164
|
+
mask_return = FFI::MemoryPointer.new(:uint)
|
165
|
+
|
166
|
+
X11.XQueryPointer(@display, @root_window, root_return, child_return, root_x, root_y, win_x, win_y, mask_return)
|
167
|
+
|
168
|
+
{ x: root_x.read_int, y: root_y.read_int }
|
70
169
|
end
|
71
170
|
|
72
171
|
def simulate_button_event(button, press)
|
73
|
-
|
74
|
-
|
172
|
+
Xtst.XTestFakeButtonEvent(@display, button, press ? 1 : 0, 0)
|
173
|
+
X11.XFlush(@display)
|
174
|
+
end
|
175
|
+
|
176
|
+
def finalize
|
177
|
+
X11.XCloseDisplay(@display) if @display && !@display.null?
|
75
178
|
end
|
76
179
|
end
|
77
180
|
end
|