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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78c1201b2d2e5a524b67b7cac64506ba9afc2538e9890a387a02252387dd1259
4
- data.tar.gz: c82ad9106e87929604fc7242e13680cdeddef6e0ede4e1bfe8181eb7e5c042d9
3
+ metadata.gz: 0b196d617707b812d5e87b904ccd55beb1230522d8d5b2bc81c38ac390e19083
4
+ data.tar.gz: a5a2da96222347a99953c923a31fa34d73f9c5b1cbff0426cd31b00c095cd9a1
5
5
  SHA512:
6
- metadata.gz: 4a9ef2494df3d2c2adbd7d9d16286ff2b5e5b05f0f1872c32cb3d0439db1d915df5c4de9b2ff1a2321e09f96dccd4122da266f749c4c7f9fd89d45490144e2f4
7
- data.tar.gz: a1d62383908125640ea807fcd96e38577c821ee7f4785b37936666aa892fa2462d6897c06861f084f265a431391dcf523424cff2a4799244e0b5fdd24cb59266
6
+ metadata.gz: ea9fc969ed0823856448a27a66ad1bf1b96553d85fa710772abd7035449111f3e430f2bcead285b05c3bad3ad74f716620fb1f065b04e1a2d4a81351e3553767
7
+ data.tar.gz: 9b46991822d21c1ea0c2ac3f107a083fb0d77d7e776a9cfbb2a9ab79a19050cf74bf5e55078e7e16581a3df240d9265b00f83525a19a2c7950bbab88a1e561a6
@@ -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.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
@@ -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
- # 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 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(name:, proxy_config:, group_id: '0', browser_version: nil)
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
- '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]
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
- 'fingerprint_config' => {
190
- # 1) Chrome kernel version → must match your Chromedriver
191
- 'browser_kernel_config' => {
192
- 'version' => browser_version,
193
- 'type' => 'chrome'
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
- # 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
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
- # 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']
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.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
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
- # Connect to the existing browser
317
- driver = Selenium::WebDriver.for(:chrome, options: opts, http_client: client)
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.15
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-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: []