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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78c1201b2d2e5a524b67b7cac64506ba9afc2538e9890a387a02252387dd1259
4
- data.tar.gz: c82ad9106e87929604fc7242e13680cdeddef6e0ede4e1bfe8181eb7e5c042d9
3
+ metadata.gz: 0fa9af194c360323a9ddb4533f0be4df21414e2a3645fdebb03a9a13eb315183
4
+ data.tar.gz: 474d47d140310200db7919c293e260761e90f33bbeef72ad923b27cfdbb180e2
5
5
  SHA512:
6
- metadata.gz: 4a9ef2494df3d2c2adbd7d9d16286ff2b5e5b05f0f1872c32cb3d0439db1d915df5c4de9b2ff1a2321e09f96dccd4122da266f749c4c7f9fd89d45490144e2f4
7
- data.tar.gz: a1d62383908125640ea807fcd96e38577c821ee7f4785b37936666aa892fa2462d6897c06861f084f265a431391dcf523424cff2a4799244e0b5fdd24cb59266
6
+ metadata.gz: 3e39840a574b00918fc9365be4ee9ffde97c8d9bdbcc4017184a2b4589dd99de31c6c6cc0e7a4eb14f6506b0e8e686ffe96f256d69dd7688dd379de9bab61b0d
7
+ data.tar.gz: 93957f83998d804fe74a423337fb13afa8bff70ed1eddb74a4da0f7cd9722b857eed3870797670ce71c8387e3f7cab745752c48df7f6c5b9520e9f3d8b21c152
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'adspower-client'
3
- s.version = '1.0.15'
4
- s.date = '2025-07-17'
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
@@ -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
- # Create a new desktop profile with custom name, proxy, and fingerprint settings
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), defaults to adspower_default_browser_version
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(name:, proxy_config:, group_id: '0', browser_version: nil)
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
- 'proxy_soft' => proxy_config[:proxy_soft] || 'other',
183
- 'proxy_type' => proxy_config[:proxy_type] || 'http',
184
- 'proxy_host' => proxy_config[:ip],
185
- 'proxy_port' => proxy_config[:port].to_s,
186
- 'proxy_user' => proxy_config[:user],
187
- 'proxy_password' => proxy_config[:password]
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
- 'fingerprint_config' => {
190
- # 1) Chrome kernel version → must match your Chromedriver
191
- 'browser_kernel_config' => {
192
- 'version' => browser_version,
193
- 'type' => 'chrome'
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
- # 2) Auto‐detect timezone (and locale) from proxy IP
196
- 'automatic_timezone' => '1',
197
- 'timezone' => '',
198
- 'language' => [],
199
- # 3) Force desktop UA (no mobile): empty random_ua & default UA settings
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
- # Return the existing driver if it's still active.
301
- old = @@drivers[id]
302
- return old if old
303
-
304
- # Otherwise, start the driver
305
- ret = self.start(id, headless)
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.add_option("debuggerAddress", url)
311
-
312
- # Set up the custom HTTP client with a longer timeout
313
- client = Selenium::WebDriver::Remote::Http::Default.new
314
- client.read_timeout = read_timeout # Set this to the desired timeout in seconds
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
- # Connect to the existing browser
317
- driver = Selenium::WebDriver.for(:chrome, options: opts, http_client: client)
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.15
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-17 00:00:00.000000000 Z
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: []