adspower-client 1.0.15 → 1.0.16
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 +200 -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: 0b196d617707b812d5e87b904ccd55beb1230522d8d5b2bc81c38ac390e19083
|
4
|
+
data.tar.gz: a5a2da96222347a99953c923a31fa34d73f9c5b1cbff0426cd31b00c095cd9a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea9fc969ed0823856448a27a66ad1bf1b96553d85fa710772abd7035449111f3e430f2bcead285b05c3bad3ad74f716620fb1f065b04e1a2d4a81351e3553767
|
7
|
+
data.tar.gz: 9b46991822d21c1ea0c2ac3f107a083fb0d77d7e776a9cfbb2a9ab79a19050cf74bf5e55078e7e16581a3df240d9265b00f83525a19a2c7950bbab88a1e561a6
|
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.16'
|
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,181 @@ 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 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.
|
203
|
+
# @param platform [String] (optional) target site domain, e.g. 'linkedin.com'
|
204
|
+
# @param tabs [Array<String>] (optional) array of URLs to open on launch
|
205
|
+
# @param username [String] (optional) platform login username
|
206
|
+
# @param password [String] (optional) platform login password
|
207
|
+
# @param fakey [String,nil] optional 2FA key
|
172
208
|
# @return String the new profile’s ID
|
173
|
-
def create2(
|
209
|
+
def create2(
|
210
|
+
name:,
|
211
|
+
proxy_config:,
|
212
|
+
group_id: '0',
|
213
|
+
browser_version: nil,
|
214
|
+
fingerprint: nil,
|
215
|
+
platform: '', # default: no platform
|
216
|
+
tabs: [], # default: no tabs to open
|
217
|
+
username: '', # default: no login
|
218
|
+
password: '', # default: no password
|
219
|
+
fakey: '' # leave blank if no 2FA
|
220
|
+
)
|
174
221
|
browser_version ||= adspower_default_browser_version
|
222
|
+
|
223
|
+
# 0) Resolve full Chrome version ─────────────────────────────
|
224
|
+
# Fetch the list of known-good Chrome versions and pick the highest
|
225
|
+
uri = URI('https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json')
|
226
|
+
resp = Net::HTTP.get_response(uri)
|
227
|
+
unless resp.is_a?(Net::HTTPSuccess)
|
228
|
+
raise "Error fetching Chrome versions: HTTP #{resp.code}"
|
229
|
+
end
|
230
|
+
listing = JSON.parse(resp.body)
|
231
|
+
versions = listing['versions'] || []
|
232
|
+
# find all entries matching the major.minor prefix
|
233
|
+
matches = versions.map { |v| v['version'] }
|
234
|
+
.select { |ver| ver.start_with?("#{browser_version}.") }
|
235
|
+
if matches.empty?
|
236
|
+
raise "Chrome version '#{browser_version}' not found in known-good versions list"
|
237
|
+
end
|
238
|
+
# pick the highest patch/build by semantic compare
|
239
|
+
full_version = matches
|
240
|
+
.map { |ver| ver.split('.').map(&:to_i) }
|
241
|
+
.max
|
242
|
+
.join('.')
|
243
|
+
|
244
|
+
# 1) Hacemos GeoIP sobre la IP del proxy
|
245
|
+
geo = geolocate(proxy_config[:ip])
|
246
|
+
lang = COUNTRY_LANG[geo[:country_code]] || "en-US"
|
247
|
+
screen_res = "1920_1080"
|
175
248
|
|
176
249
|
with_lock do
|
177
250
|
url = "#{adspower_listener}:#{port}/api/v2/browser-profile/create"
|
178
251
|
body = {
|
252
|
+
# ─── GENERAL & PROXY ─────────────────────────────
|
179
253
|
'name' => name,
|
180
254
|
'group_id' => group_id,
|
181
255
|
'user_proxy_config' => {
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
256
|
+
'proxy_soft' => proxy_config[:proxy_soft] || 'other',
|
257
|
+
'proxy_type' => proxy_config[:proxy_type] || 'socks5',
|
258
|
+
'proxy_host' => proxy_config[:ip],
|
259
|
+
'proxy_port' => proxy_config[:port].to_s,
|
260
|
+
'proxy_user' => proxy_config[:user],
|
261
|
+
'proxy_password' => proxy_config[:password],
|
262
|
+
|
263
|
+
# ─── FORCE ALL DNS THROUGH PROXY ─────────────────
|
264
|
+
# Avoid DNS-Leak
|
265
|
+
"proxy_dns": 1, # 1 = yes, 0 = no
|
266
|
+
"dns_servers": ["8.8.8.8","8.8.4.4"] # optional: your choice of DNS
|
188
267
|
},
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
268
|
+
|
269
|
+
# ─── PLATFORM ─────────────────────────────────────
|
270
|
+
'platform' => platform, # must be one of AdsPower’s supported “sites”
|
271
|
+
'tabs' => tabs, # array of URLs to open
|
272
|
+
'username' => username,
|
273
|
+
'password' => password,
|
274
|
+
'fakey' => fakey, # 2FA, if any
|
275
|
+
|
276
|
+
# ─── FINGERPRINT ──────────────────────────────────
|
277
|
+
"fingerprint_config" => fingerprint || {
|
278
|
+
|
279
|
+
# ─── 0) DNS Leak Prevention ───────────────────────────
|
280
|
+
# Even with “proxy_dns” forced on, a few ISPs will still
|
281
|
+
# silently intercept every UDP:53 out of your AdsPower VPS
|
282
|
+
# and shove it into their own resolver farm (the classic
|
283
|
+
# “transparent DNS proxy” attack that BrowserScan is warning you about).
|
284
|
+
#
|
285
|
+
# Because you refuse to hot-patch your Chrome via extra args or CDP,
|
286
|
+
# the only way to survive an ISP-level hijack is to push all name lookups
|
287
|
+
# into an encrypted channel that the ISP simply can’t touch: DNS-over-HTTPS (DoH).
|
288
|
+
#
|
289
|
+
# Here’s the minimal change you need to bake into your AdsPower profile at
|
290
|
+
# creation time so that every DNS query happens inside Chrome’s DoH stack:
|
291
|
+
#
|
292
|
+
"extra_launch_flags" => [
|
293
|
+
# === DNS over HTTPS only ===
|
294
|
+
"--enable-features=DnsOverHttps",
|
295
|
+
"--dns-over-https-mode=secure",
|
296
|
+
"--dns-over-https-templates=https://cloudflare-dns.com/dns-query",
|
297
|
+
"--disable-ipv6",
|
298
|
+
|
299
|
+
# === hide “Chrome is being controlled…” banner ===
|
300
|
+
#
|
301
|
+
# Even though you baked in the DoH flags under extra_launch_flags,
|
302
|
+
# you never told Chrome to hide its “automation” banners or black-hole
|
303
|
+
# all other DNS lookups — and BrowserScan still sees those UDP:53 calls
|
304
|
+
# leaking out.
|
305
|
+
#
|
306
|
+
# What you need is to push three more flags into your profile creation,
|
307
|
+
# and then attach with the exact same flags when Selenium hooks in.
|
308
|
+
#
|
309
|
+
"--disable-blink-features=AutomationControlled",
|
310
|
+
"--disable-infobars",
|
311
|
+
"--disable-features=TranslateUI", # optional but reduces tell-tale infobars
|
312
|
+
"--host-resolver-rules=MAP * 0.0.0.0,EXCLUDE localhost,EXCLUDE cloudflare-dns.com"
|
313
|
+
],
|
314
|
+
|
315
|
+
# ─── 1) Kernel & versión ───────────────────────────
|
316
|
+
"browser_kernel_config" => {
|
317
|
+
"version" => browser_version, # aquí usamos el parámetro
|
318
|
+
"type" => "chrome"
|
319
|
+
},
|
320
|
+
|
321
|
+
# ─── 2) Timezone & locale ──────────────────────────
|
322
|
+
"automatic_timezone" => "1",
|
323
|
+
#"timezone" => geo[:time_zone],
|
324
|
+
"language" => [ lang ],
|
325
|
+
|
326
|
+
# ─── 3) User-Agent coherente ───────────────────────
|
327
|
+
"ua_category" => "desktop",
|
328
|
+
'ua' => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/#{full_version} Safari/537.36",
|
329
|
+
"is_mobile" => false,
|
330
|
+
|
331
|
+
# ─── 4) Pantalla y plataforma ──────────────────────
|
332
|
+
# It turns out that “Based on User-Agent” is purely a UI setting
|
333
|
+
#"screen_resolution" => screen_res, "1920_1080"
|
334
|
+
"platform" => "Linux x86_64",
|
335
|
+
|
336
|
+
# ─── 5) Canvas & WebGL custom ─────────────────────
|
337
|
+
"canvas" => "1",
|
338
|
+
"webgl_image" => "1",
|
339
|
+
"webgl" => "0", # 0=deshabilitado, 2=modo custom, 3=modo random-match
|
340
|
+
"webgl_config" => {
|
341
|
+
"unmasked_vendor" => "Intel Inc.",
|
342
|
+
"unmasked_renderer" => "ANGLE (Intel, Mesa Intel(R) Xe Graphics (TGL GT2), OpenGL 4.6)",
|
343
|
+
"webgpu" => { "webgpu_switch" => "1" }
|
194
344
|
},
|
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
|
345
|
+
|
346
|
+
# ─── 6) Resto de ajustes ───────────────────────────
|
347
|
+
"webrtc" => "disabled", # WebRTC sí admite “disabled”
|
348
|
+
"flash" => "block", # Flash únicamente “allow” o “block”
|
349
|
+
"fonts" => [] # usar fonts por defecto
|
209
350
|
}
|
210
351
|
}
|
211
352
|
|
@@ -295,33 +436,44 @@ class AdsPowerClient
|
|
295
436
|
driver
|
296
437
|
end
|
297
438
|
|
298
|
-
# Attach to the existing browser session with Selenium WebDriver.
|
299
439
|
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']
|
440
|
+
return @@drivers[id] if @@drivers[id]
|
441
|
+
|
442
|
+
# 1) start the AdsPower profile / grab its WebSocket URL
|
443
|
+
data = start(id, headless)['data']
|
444
|
+
ws = data['ws']['selenium'] # e.g. "127.0.0.1:XXXXX"
|
445
|
+
|
446
|
+
# 2) attach with DevTools (no more excludeSwitches or caps!)
|
309
447
|
opts = Selenium::WebDriver::Chrome::Options.new
|
310
|
-
opts.
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
448
|
+
opts.debugger_address = ws
|
449
|
+
opts.add_argument('--headless') if headless
|
450
|
+
|
451
|
+
http = Selenium::WebDriver::Remote::Http::Default.new
|
452
|
+
http.read_timeout = read_timeout
|
453
|
+
|
454
|
+
driver = Selenium::WebDriver.for(:chrome, options: opts, http_client: http)
|
455
|
+
|
456
|
+
driver.execute_cdp(
|
457
|
+
'Page.addScriptToEvaluateOnNewDocument',
|
458
|
+
source: <<~JS
|
459
|
+
// 1) remove any leftover cdc_… / webdriver hooks
|
460
|
+
for (const k of Object.getOwnPropertyNames(window)) {
|
461
|
+
if (k.startsWith('cdc_') || k.includes('webdriver')) {
|
462
|
+
try { delete window[k]; } catch(e){}
|
463
|
+
}
|
464
|
+
}
|
315
465
|
|
316
|
-
|
317
|
-
|
466
|
+
// 2) stub out window.chrome so Chrome-based detection thinks this is “normal” Chrome
|
467
|
+
window.chrome = { runtime: {} };
|
468
|
+
JS
|
469
|
+
)
|
318
470
|
|
319
|
-
# Save the driver
|
320
471
|
@@drivers[id] = driver
|
321
|
-
|
322
|
-
# Return the driver
|
323
472
|
driver
|
324
473
|
end
|
474
|
+
|
475
|
+
|
476
|
+
|
325
477
|
|
326
478
|
# DEPRECATED - Use Zyte instead of this method.
|
327
479
|
#
|
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.16
|
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: []
|