adspower-client 1.0.14 → 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: 8651920c7a5df1c4fdda39fd7c02617c0966b52cc53dcffa12e0ece7b1183063
4
- data.tar.gz: a96b2d9886d8ae404ec7fd9a02af4687ae5bb6c77b7dd8f00133e77488c1d1ba
3
+ metadata.gz: 0b196d617707b812d5e87b904ccd55beb1230522d8d5b2bc81c38ac390e19083
4
+ data.tar.gz: a5a2da96222347a99953c923a31fa34d73f9c5b1cbff0426cd31b00c095cd9a1
5
5
  SHA512:
6
- metadata.gz: 4006c80cdb711dd9f260da80c5b27d9e585c6b7b86bc898aafc9aef79c8b859ad5bac6cc1ef9ebef7ba576441d10e2c6772e80b613be7abf138c851378738989
7
- data.tar.gz: 1ab657b853fb51acffcab69850eb8fe4506d0ca9fe56b5982d5bbd8b9210bbeb83c5e33564e4614f73596f706c12ef62b3b04e45a6ce13a89a229d441d79575b
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.14'
4
- s.date = '2024-10-29'
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,11 +5,22 @@ require 'blackstack-core'
5
5
  require 'selenium-webdriver'
6
6
  require 'watir'
7
7
  require 'fileutils'
8
+ require 'countries'
9
+
10
+ class AdsPowerClient
11
+ CLOUD_API_BASE = 'https://api.adspower.com/v1'
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
8
20
 
9
- class AdsPowerClient
10
21
  # reference: https://localapi-doc-en.adspower.com/
11
22
  # reference: https://localapi-doc-en.adspower.com/docs/Rdw7Iu
12
- attr_accessor :key, :port, :server_log, :adspower_listener, :adspower_default_browser_version
23
+ attr_accessor :key, :port, :server_log, :adspower_listener, :adspower_default_browser_version, :cloud_token
13
24
 
14
25
  # control over the drivers created, in order to not create the same driver twice and not generate memory leaks.
15
26
  # reference: https://github.com/leandrosardi/adspower-client/issues/4
@@ -22,7 +33,12 @@ class AdsPowerClient
22
33
  self.port = h[:port] || '50325'
23
34
  self.server_log = h[:server_log] || '~/adspower-client.log'
24
35
  self.adspower_listener = h[:adspower_listener] || 'http://127.0.0.1'
36
+
37
+ # DEPRECATED
25
38
  self.adspower_default_browser_version = h[:adspower_default_browser_version] || '116'
39
+
40
+ # PENDING
41
+ self.cloud_token = h[:cloud_token]
26
42
  end
27
43
 
28
44
  # Acquire the lock
@@ -89,6 +105,54 @@ class AdsPowerClient
89
105
  end
90
106
  end
91
107
 
108
+ # Count current profiles (optionally filtered by group)
109
+ def profile_count(group_id: nil)
110
+ count = 0
111
+ page = 1
112
+
113
+ loop do
114
+ params = { page: page, limit: 100 }
115
+ params[:group_id] = group_id if group_id
116
+ url = "#{adspower_listener}:#{port}/api/v2/browser-profile/list"
117
+ res = BlackStack::Netting.call_post(url, params)
118
+ data = JSON.parse(res.body)
119
+ raise "Error listing profiles: #{data['msg']}" unless data['code'] == 0
120
+
121
+ list = data['data']['list']
122
+ count += list.size
123
+ break if list.size < 100
124
+
125
+ page += 1
126
+ end
127
+
128
+ count
129
+ end
130
+
131
+ # Return a hash with:
132
+ # • :limit ⇒ total profile slots allowed (-1 = unlimited)
133
+ # • :used ⇒ number of profiles currently created
134
+ # • :remaining ⇒ slots left (nil if unlimited)
135
+ # Fetch your real profile quota from the Cloud API
136
+ def cloud_profile_quota
137
+ uri = URI("#{CLOUD_API_BASE}/account/get_info")
138
+ req = Net::HTTP::Get.new(uri)
139
+ req['Authorization'] = "Bearer #{self.cloud_token}"
140
+
141
+ res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
142
+ http.request(req)
143
+ end
144
+ data = JSON.parse(res.body)
145
+ raise "Cloud API error: #{data['msg']}" unless data['code'] == 0
146
+
147
+ allowed = data['data']['total_profiles_allowed'].to_i
148
+ used = data['data']['profiles_used'].to_i
149
+ remaining = allowed < 0 ? nil : (allowed - used)
150
+
151
+ { limit: allowed,
152
+ used: used,
153
+ remaining: remaining }
154
+ end # cloud_profile_quota
155
+
92
156
  # Create a new user profile via API call and return the ID of the created user.
93
157
  def create
94
158
  with_lock do
@@ -106,8 +170,194 @@ class AdsPowerClient
106
170
  raise "Error: #{ret.to_s}" if ret['msg'].to_s.downcase != 'success'
107
171
  ret['data']['id']
108
172
  end
173
+ end # def create
174
+
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 }
109
190
  end
110
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
197
+ #
198
+ # @param name [String] the profile’s display name
199
+ # @param proxy_config [Hash] keys: :ip, :port, :user, :password, :proxy_soft (default 'other'), :proxy_type (default 'http')
200
+ # @param group_id [String] which AdsPower group to assign (default '0')
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
208
+ # @return String the new profile’s ID
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
+ )
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"
248
+
249
+ with_lock do
250
+ url = "#{adspower_listener}:#{port}/api/v2/browser-profile/create"
251
+ body = {
252
+ # ─── GENERAL & PROXY ─────────────────────────────
253
+ 'name' => name,
254
+ 'group_id' => group_id,
255
+ 'user_proxy_config' => {
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
267
+ },
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" }
344
+ },
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
350
+ }
351
+ }
352
+
353
+ res = BlackStack::Netting.call_post(url, body)
354
+ ret = JSON.parse(res.body)
355
+ raise "Error creating profile: #{ret['msg']}" unless ret['code'] == 0
356
+
357
+ ret['data']['profile_id']
358
+ end
359
+ end # def create2
360
+
111
361
  # Delete a user profile via API call.
112
362
  def delete(id)
113
363
  with_lock do
@@ -186,34 +436,47 @@ class AdsPowerClient
186
436
  driver
187
437
  end
188
438
 
189
- # Attach to the existing browser session with Selenium WebDriver.
190
439
  def driver2(id, headless: false, read_timeout: 180)
191
- # Return the existing driver if it's still active.
192
- old = @@drivers[id]
193
- return old if old
194
-
195
- # Otherwise, start the driver
196
- ret = self.start(id, headless)
197
-
198
- # Attach test execution to the existing browser
199
- 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!)
200
447
  opts = Selenium::WebDriver::Chrome::Options.new
201
- opts.add_option("debuggerAddress", url)
202
-
203
- # Set up the custom HTTP client with a longer timeout
204
- client = Selenium::WebDriver::Remote::Http::Default.new
205
- 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
+ }
206
465
 
207
- # Connect to the existing browser
208
- 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
+ )
209
470
 
210
- # Save the driver
211
471
  @@drivers[id] = driver
212
-
213
- # Return the driver
214
472
  driver
215
473
  end
474
+
475
+
476
+
216
477
 
478
+ # DEPRECATED - Use Zyte instead of this method.
479
+ #
217
480
  # Create a new profile, start the browser, visit a page, grab the HTML, and clean up.
218
481
  def html(url)
219
482
  ret = {
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.14
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: 2024-10-29 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: []