adspower-client 1.0.15 → 1.0.17
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/adspower-client.gemspec +4 -2
- data/lib/adspower-client.rb +206 -48
- metadata +42 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0fa9af194c360323a9ddb4533f0be4df21414e2a3645fdebb03a9a13eb315183
|
4
|
+
data.tar.gz: 474d47d140310200db7919c293e260761e90f33bbeef72ad923b27cfdbb180e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3e39840a574b00918fc9365be4ee9ffde97c8d9bdbcc4017184a2b4589dd99de31c6c6cc0e7a4eb14f6506b0e8e686ffe96f256d69dd7688dd379de9bab61b0d
|
7
|
+
data.tar.gz: 93957f83998d804fe74a423337fb13afa8bff70ed1eddb74a4da0f7cd9722b857eed3870797670ce71c8387e3f7cab745752c48df7f6c5b9520e9f3d8b21c152
|
data/adspower-client.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'adspower-client'
|
3
|
-
s.version = '1.0.
|
4
|
-
s.date = '2025-07-
|
3
|
+
s.version = '1.0.17'
|
4
|
+
s.date = '2025-07-27'
|
5
5
|
s.summary = "Ruby library for operating AdsPower API."
|
6
6
|
s.description = "Ruby library for operating AdsPower API."
|
7
7
|
s.authors = ["Leandro Daniel Sardi"]
|
@@ -19,6 +19,8 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.add_runtime_dependency 'selenium-webdriver', '~> 4.10.0', '>= 4.10.0'
|
20
20
|
s.add_runtime_dependency 'watir', '~> 7.3.0', '>= 7.3.0'
|
21
21
|
#s.add_runtime_dependency 'sequel', '~> 5.75.0', '>= 5.75.0'
|
22
|
+
s.add_runtime_dependency 'fileutils', '~> 1.6.0', '>= 1.6.0'
|
22
23
|
s.add_runtime_dependency 'colorize', '~> 0.8.1', '>= 0.8.1'
|
23
24
|
s.add_runtime_dependency 'simple_cloud_logging', '~> 1.2.2', '>= 1.2.2'
|
25
|
+
s.add_runtime_dependency 'countries', '~> 7.1.1', '>= 7.1.1'
|
24
26
|
end
|
data/lib/adspower-client.rb
CHANGED
@@ -5,10 +5,19 @@ require 'blackstack-core'
|
|
5
5
|
require 'selenium-webdriver'
|
6
6
|
require 'watir'
|
7
7
|
require 'fileutils'
|
8
|
+
require 'countries'
|
8
9
|
|
9
10
|
class AdsPowerClient
|
10
11
|
CLOUD_API_BASE = 'https://api.adspower.com/v1'
|
11
12
|
|
13
|
+
# Constante generada en tiempo de ejecución:
|
14
|
+
COUNTRY_LANG = ISO3166::Country.all.each_with_object({}) do |country, h|
|
15
|
+
# El primer idioma oficial (ISO 639-1) que encuentre:
|
16
|
+
language_code = country.languages&.first || 'en'
|
17
|
+
# Construimos la etiqueta BCP47 Language-Region:
|
18
|
+
h[country.alpha2] = "#{language_code}-#{country.alpha2}"
|
19
|
+
end.freeze
|
20
|
+
|
12
21
|
# reference: https://localapi-doc-en.adspower.com/
|
13
22
|
# reference: https://localapi-doc-en.adspower.com/docs/Rdw7Iu
|
14
23
|
attr_accessor :key, :port, :server_log, :adspower_listener, :adspower_default_browser_version, :cloud_token
|
@@ -163,49 +172,187 @@ class AdsPowerClient
|
|
163
172
|
end
|
164
173
|
end # def create
|
165
174
|
|
166
|
-
|
175
|
+
|
176
|
+
# Lookup GeoIP gratuito (freegeoip.app) y parseo básico
|
177
|
+
def geolocate(ip)
|
178
|
+
uri = URI("https://freegeoip.app/json/#{ip}")
|
179
|
+
res = Net::HTTP.get(uri)
|
180
|
+
h = JSON.parse(res)
|
181
|
+
{
|
182
|
+
country_code: h["country_code"],
|
183
|
+
time_zone: h["time_zone"],
|
184
|
+
latitude: h["latitude"],
|
185
|
+
longitude: h["longitude"]
|
186
|
+
}
|
187
|
+
rescue
|
188
|
+
# Fallback genérico
|
189
|
+
{ country_code: "US", time_zone: "America/New_York", latitude: 38.9, longitude: -77.0 }
|
190
|
+
end
|
191
|
+
|
192
|
+
# Create a new desktop profile with:
|
193
|
+
# • name, proxy, fingerprint, etc (unchanged)
|
194
|
+
# • platform (e.g. "linkedin.com")
|
195
|
+
# • tabs (Array of URLs to open)
|
196
|
+
# • username / password / fakey for that platform
|
167
197
|
#
|
168
198
|
# @param name [String] the profile’s display name
|
169
199
|
# @param proxy_config [Hash] keys: :ip, :port, :user, :password, :proxy_soft (default 'other'), :proxy_type (default 'http')
|
170
200
|
# @param group_id [String] which AdsPower group to assign (default '0')
|
171
|
-
# @param browser_version [String] Chrome version to use (must match Chromedriver),
|
201
|
+
# @param browser_version [String] optional Chrome version to use (must match Chromedriver). Only applies if `fingerprint` is nil, as custom fingerprints override kernel settings.
|
202
|
+
# @param os [String] target OS for Chrome binary (one of 'linux64', 'mac-x64', 'mac-arm64', 'win32', 'win64'; default 'linux64'); used to filter the known-good versions JSON so we pick a build that actually ships for that platform
|
203
|
+
# @param fingerprint [Hash, nil] optional fingerprint configuration. If not provided, a stealth-ready default is applied with DNS-over-HTTPS, spoofed WebGL/Canvas/audio, consistent User-Agent and locale, and hardening flags to minimize detection risks from tools like BrowserScan, Cloudflare, and Arkose Labs.
|
204
|
+
# @param platform [String] (optional) target site domain, e.g. 'linkedin.com'
|
205
|
+
# @param tabs [Array<String>] (optional) array of URLs to open on launch
|
206
|
+
# @param username [String] (optional) platform login username
|
207
|
+
# @param password [String] (optional) platform login password
|
208
|
+
# @param fakey [String,nil] optional 2FA key
|
172
209
|
# @return String the new profile’s ID
|
173
|
-
def create2(
|
210
|
+
def create2(
|
211
|
+
name:,
|
212
|
+
proxy_config:,
|
213
|
+
group_id: '0',
|
214
|
+
browser_version: nil,
|
215
|
+
os: 'linux64',# new: one of linux64, mac-x64, mac-arm64, win32, win64
|
216
|
+
fingerprint: nil,
|
217
|
+
platform: '', # default: no platform
|
218
|
+
tabs: [], # default: no tabs to open
|
219
|
+
username: '', # default: no login
|
220
|
+
password: '', # default: no password
|
221
|
+
fakey: '' # leave blank if no 2FA
|
222
|
+
)
|
174
223
|
browser_version ||= adspower_default_browser_version
|
224
|
+
|
225
|
+
# 0) Resolve full Chrome version ─────────────────────────────
|
226
|
+
# Fetch the list of known-good Chrome versions and pick the highest
|
227
|
+
uri = URI('https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json')
|
228
|
+
resp = Net::HTTP.get_response(uri)
|
229
|
+
unless resp.is_a?(Net::HTTPSuccess)
|
230
|
+
raise "Error fetching Chrome versions: HTTP #{resp.code}"
|
231
|
+
end
|
232
|
+
listing = JSON.parse(resp.body)
|
233
|
+
entries = listing['versions'] || []
|
234
|
+
# keep only those entries whose version matches prefix *and* has a download for our OS
|
235
|
+
matches = entries.
|
236
|
+
select { |e|
|
237
|
+
e['version'].start_with?("#{browser_version}.") &&
|
238
|
+
e.dig('downloads','chrome').any? { |d| d['platform'] == os }
|
239
|
+
}.
|
240
|
+
map { |e| e['version'] }
|
241
|
+
if matches.empty?
|
242
|
+
raise "Chrome version '#{browser_version}' not found in known-good versions list"
|
243
|
+
end
|
244
|
+
# pick the highest patch/build by semantic compare
|
245
|
+
full_version = matches
|
246
|
+
.map { |ver| ver.split('.').map(&:to_i) }
|
247
|
+
.max
|
248
|
+
.join('.')
|
249
|
+
|
250
|
+
# 1) Hacemos GeoIP sobre la IP del proxy
|
251
|
+
geo = geolocate(proxy_config[:ip])
|
252
|
+
lang = COUNTRY_LANG[geo[:country_code]] || "en-US"
|
253
|
+
screen_res = "1920_1080"
|
175
254
|
|
176
255
|
with_lock do
|
177
256
|
url = "#{adspower_listener}:#{port}/api/v2/browser-profile/create"
|
178
257
|
body = {
|
258
|
+
# ─── GENERAL & PROXY ─────────────────────────────
|
179
259
|
'name' => name,
|
180
260
|
'group_id' => group_id,
|
181
261
|
'user_proxy_config' => {
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
262
|
+
'proxy_soft' => proxy_config[:proxy_soft] || 'other',
|
263
|
+
'proxy_type' => proxy_config[:proxy_type] || 'socks5',
|
264
|
+
'proxy_host' => proxy_config[:ip],
|
265
|
+
'proxy_port' => proxy_config[:port].to_s,
|
266
|
+
'proxy_user' => proxy_config[:user],
|
267
|
+
'proxy_password' => proxy_config[:password],
|
268
|
+
|
269
|
+
# ─── FORCE ALL DNS THROUGH PROXY ─────────────────
|
270
|
+
# Avoid DNS-Leak
|
271
|
+
"proxy_dns": 1, # 1 = yes, 0 = no
|
272
|
+
"dns_servers": ["8.8.8.8","8.8.4.4"] # optional: your choice of DNS
|
188
273
|
},
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
274
|
+
|
275
|
+
# ─── PLATFORM ─────────────────────────────────────
|
276
|
+
'platform' => platform, # must be one of AdsPower’s supported “sites”
|
277
|
+
'tabs' => tabs, # array of URLs to open
|
278
|
+
'username' => username,
|
279
|
+
'password' => password,
|
280
|
+
'fakey' => fakey, # 2FA, if any
|
281
|
+
|
282
|
+
# ─── FINGERPRINT ──────────────────────────────────
|
283
|
+
"fingerprint_config" => fingerprint || {
|
284
|
+
|
285
|
+
# ─── 0) DNS Leak Prevention ───────────────────────────
|
286
|
+
# Even with “proxy_dns” forced on, a few ISPs will still
|
287
|
+
# silently intercept every UDP:53 out of your AdsPower VPS
|
288
|
+
# and shove it into their own resolver farm (the classic
|
289
|
+
# “transparent DNS proxy” attack that BrowserScan is warning you about).
|
290
|
+
#
|
291
|
+
# Because you refuse to hot-patch your Chrome via extra args or CDP,
|
292
|
+
# the only way to survive an ISP-level hijack is to push all name lookups
|
293
|
+
# into an encrypted channel that the ISP simply can’t touch: DNS-over-HTTPS (DoH).
|
294
|
+
#
|
295
|
+
# Here’s the minimal change you need to bake into your AdsPower profile at
|
296
|
+
# creation time so that every DNS query happens inside Chrome’s DoH stack:
|
297
|
+
#
|
298
|
+
"extra_launch_flags" => [
|
299
|
+
# === DNS over HTTPS only ===
|
300
|
+
"--enable-features=DnsOverHttps",
|
301
|
+
"--dns-over-https-mode=secure",
|
302
|
+
"--dns-over-https-templates=https://cloudflare-dns.com/dns-query",
|
303
|
+
"--disable-ipv6",
|
304
|
+
|
305
|
+
# === hide “Chrome is being controlled…” banner ===
|
306
|
+
#
|
307
|
+
# Even though you baked in the DoH flags under extra_launch_flags,
|
308
|
+
# you never told Chrome to hide its “automation” banners or black-hole
|
309
|
+
# all other DNS lookups — and BrowserScan still sees those UDP:53 calls
|
310
|
+
# leaking out.
|
311
|
+
#
|
312
|
+
# What you need is to push three more flags into your profile creation,
|
313
|
+
# and then attach with the exact same flags when Selenium hooks in.
|
314
|
+
#
|
315
|
+
"--disable-blink-features=AutomationControlled",
|
316
|
+
"--disable-infobars",
|
317
|
+
"--disable-features=TranslateUI", # optional but reduces tell-tale infobars
|
318
|
+
"--host-resolver-rules=MAP * 0.0.0.0,EXCLUDE localhost,EXCLUDE cloudflare-dns.com"
|
319
|
+
],
|
320
|
+
|
321
|
+
# ─── 1) Kernel & versión ───────────────────────────
|
322
|
+
"browser_kernel_config" => {
|
323
|
+
"version" => browser_version, # aquí usamos el parámetro
|
324
|
+
"type" => "chrome"
|
325
|
+
},
|
326
|
+
|
327
|
+
# ─── 2) Timezone & locale ──────────────────────────
|
328
|
+
"automatic_timezone" => "1",
|
329
|
+
#"timezone" => geo[:time_zone],
|
330
|
+
"language" => [ lang ],
|
331
|
+
|
332
|
+
# ─── 3) User-Agent coherente ───────────────────────
|
333
|
+
"ua_category" => "desktop",
|
334
|
+
'ua' => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/#{full_version} Safari/537.36",
|
335
|
+
"is_mobile" => false,
|
336
|
+
|
337
|
+
# ─── 4) Pantalla y plataforma ──────────────────────
|
338
|
+
# It turns out that “Based on User-Agent” is purely a UI setting
|
339
|
+
#"screen_resolution" => screen_res, "1920_1080"
|
340
|
+
"platform" => "Linux x86_64",
|
341
|
+
|
342
|
+
# ─── 5) Canvas & WebGL custom ─────────────────────
|
343
|
+
"canvas" => "1",
|
344
|
+
"webgl_image" => "1",
|
345
|
+
"webgl" => "0", # 0=deshabilitado, 2=modo custom, 3=modo random-match
|
346
|
+
"webgl_config" => {
|
347
|
+
"unmasked_vendor" => "Intel Inc.",
|
348
|
+
"unmasked_renderer" => "ANGLE (Intel, Mesa Intel(R) Xe Graphics (TGL GT2), OpenGL 4.6)",
|
349
|
+
"webgpu" => { "webgpu_switch" => "1" }
|
194
350
|
},
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
#
|
200
|
-
'ua' => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "\
|
201
|
-
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/#{browser_version}.0.0.0 Safari/537.36",
|
202
|
-
'ua_category' => 'desktop',
|
203
|
-
#'screen_resolution' => '1920*1080',
|
204
|
-
'is_mobile' => false,
|
205
|
-
# standard desktop fingerprints
|
206
|
-
'webrtc' => 'disabled', # hide real IP via WebRTC
|
207
|
-
'flash' => 'allow',
|
208
|
-
'fonts' => [], # default fonts
|
351
|
+
|
352
|
+
# ─── 6) Resto de ajustes ───────────────────────────
|
353
|
+
"webrtc" => "disabled", # WebRTC sí admite “disabled”
|
354
|
+
"flash" => "block", # Flash únicamente “allow” o “block”
|
355
|
+
"fonts" => [] # usar fonts por defecto
|
209
356
|
}
|
210
357
|
}
|
211
358
|
|
@@ -295,33 +442,44 @@ class AdsPowerClient
|
|
295
442
|
driver
|
296
443
|
end
|
297
444
|
|
298
|
-
# Attach to the existing browser session with Selenium WebDriver.
|
299
445
|
def driver2(id, headless: false, read_timeout: 180)
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
#
|
305
|
-
|
306
|
-
|
307
|
-
# Attach test execution to the existing browser
|
308
|
-
url = ret['data']['ws']['selenium']
|
446
|
+
return @@drivers[id] if @@drivers[id]
|
447
|
+
|
448
|
+
# 1) start the AdsPower profile / grab its WebSocket URL
|
449
|
+
data = start(id, headless)['data']
|
450
|
+
ws = data['ws']['selenium'] # e.g. "127.0.0.1:XXXXX"
|
451
|
+
|
452
|
+
# 2) attach with DevTools (no more excludeSwitches or caps!)
|
309
453
|
opts = Selenium::WebDriver::Chrome::Options.new
|
310
|
-
opts.
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
454
|
+
opts.debugger_address = ws
|
455
|
+
opts.add_argument('--headless') if headless
|
456
|
+
|
457
|
+
http = Selenium::WebDriver::Remote::Http::Default.new
|
458
|
+
http.read_timeout = read_timeout
|
459
|
+
|
460
|
+
driver = Selenium::WebDriver.for(:chrome, options: opts, http_client: http)
|
461
|
+
|
462
|
+
driver.execute_cdp(
|
463
|
+
'Page.addScriptToEvaluateOnNewDocument',
|
464
|
+
source: <<~JS
|
465
|
+
// 1) remove any leftover cdc_… / webdriver hooks
|
466
|
+
for (const k of Object.getOwnPropertyNames(window)) {
|
467
|
+
if (k.startsWith('cdc_') || k.includes('webdriver')) {
|
468
|
+
try { delete window[k]; } catch(e){}
|
469
|
+
}
|
470
|
+
}
|
315
471
|
|
316
|
-
|
317
|
-
|
472
|
+
// 2) stub out window.chrome so Chrome-based detection thinks this is “normal” Chrome
|
473
|
+
window.chrome = { runtime: {} };
|
474
|
+
JS
|
475
|
+
)
|
318
476
|
|
319
|
-
# Save the driver
|
320
477
|
@@drivers[id] = driver
|
321
|
-
|
322
|
-
# Return the driver
|
323
478
|
driver
|
324
479
|
end
|
480
|
+
|
481
|
+
|
482
|
+
|
325
483
|
|
326
484
|
# DEPRECATED - Use Zyte instead of this method.
|
327
485
|
#
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: adspower-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.17
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Leandro Daniel Sardi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-07-
|
11
|
+
date: 2025-07-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: uri
|
@@ -130,6 +130,26 @@ dependencies:
|
|
130
130
|
- - ">="
|
131
131
|
- !ruby/object:Gem::Version
|
132
132
|
version: 7.3.0
|
133
|
+
- !ruby/object:Gem::Dependency
|
134
|
+
name: fileutils
|
135
|
+
requirement: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - "~>"
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: 1.6.0
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: 1.6.0
|
143
|
+
type: :runtime
|
144
|
+
prerelease: false
|
145
|
+
version_requirements: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - "~>"
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: 1.6.0
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 1.6.0
|
133
153
|
- !ruby/object:Gem::Dependency
|
134
154
|
name: colorize
|
135
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -170,6 +190,26 @@ dependencies:
|
|
170
190
|
- - ">="
|
171
191
|
- !ruby/object:Gem::Version
|
172
192
|
version: 1.2.2
|
193
|
+
- !ruby/object:Gem::Dependency
|
194
|
+
name: countries
|
195
|
+
requirement: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - "~>"
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: 7.1.1
|
200
|
+
- - ">="
|
201
|
+
- !ruby/object:Gem::Version
|
202
|
+
version: 7.1.1
|
203
|
+
type: :runtime
|
204
|
+
prerelease: false
|
205
|
+
version_requirements: !ruby/object:Gem::Requirement
|
206
|
+
requirements:
|
207
|
+
- - "~>"
|
208
|
+
- !ruby/object:Gem::Version
|
209
|
+
version: 7.1.1
|
210
|
+
- - ">="
|
211
|
+
- !ruby/object:Gem::Version
|
212
|
+
version: 7.1.1
|
173
213
|
description: Ruby library for operating AdsPower API.
|
174
214
|
email: leandro@connectionsphere.com
|
175
215
|
executables: []
|