browserstack-local 1.4.3 → 1.5.1

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: 5d8908a1d1804c2e04da139de1fc8a3bb7631780f0a8ee99753cd78f5c4e8134
4
- data.tar.gz: 2d113c112835b279292b5aac0042efa5d77fce7fbc2d3bf775b817bea0b7b440
3
+ metadata.gz: e32e052d39ddb27d0c6d4e65796a8d7155896124874e6b98cb7a187f8ac2580b
4
+ data.tar.gz: a3708dc83e2be4f34aa09efb80a3a7a414449fecef23f19d2cf58db933f114fb
5
5
  SHA512:
6
- metadata.gz: aad48f4723959de92ee40fc564167aea7d78ea98423557feda42e793d75acb57bb26a9f2944595693460aaae273c5b7058ebdcf6bbae8a70923335efa6ad742f
7
- data.tar.gz: bd759d61834aacc1647e0c21caf07fc274a5aef426ec92ee71836153cf9f6d312dfd480c9392527c81d3b018e32b60be544dbacebbdfa6ccb90f015d254a0d59
6
+ metadata.gz: dfc70f0d1c49f0aeda327cd85bb6ebdd2e920c3757cfea81098a35b1c28925d7b2c621c2b7bb630d781f9a80b8bc003feccb675958594a97b66c50e79842c76a
7
+ data.tar.gz: 21b7f15d270f6163aa930f424831f5b286ad36560416cfe602ac559c4e13ee62177824e52680aaa94f19f7c70ed0132fd13fa980e999a29da6c2ba87351cc41c
@@ -0,0 +1,63 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'json'
4
+ require 'openssl'
5
+ require 'browserstack/localexception'
6
+
7
+ module BrowserStack
8
+ module FetchDownloadSourceUrl
9
+ BS_HOST = 'local.browserstack.com'.freeze
10
+ ENDPOINT_PATH = '/binary/api/v1/endpoint'.freeze
11
+
12
+ def self.call(auth_token:, user_agent:, fallback: false, error_message: nil,
13
+ proxy_host: nil, proxy_port: nil)
14
+ uri = URI::HTTPS.build(host: BS_HOST, path: ENDPOINT_PATH)
15
+
16
+ body = { 'auth_token' => auth_token }
17
+ body['error_message'] = error_message if fallback && error_message
18
+
19
+ http_class = if proxy_host && proxy_port
20
+ Net::HTTP::Proxy(proxy_host, proxy_port.to_i)
21
+ else
22
+ Net::HTTP
23
+ end
24
+
25
+ http = http_class.new(uri.host, uri.port)
26
+ http.use_ssl = true
27
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
28
+ http.open_timeout = 10
29
+ http.read_timeout = 15
30
+
31
+ req = Net::HTTP::Post.new(uri.request_uri)
32
+ req['Content-Type'] = 'application/json'
33
+ req['User-Agent'] = user_agent
34
+ req['X-Local-Fallback-Cloudflare'] = 'true' if fallback
35
+ req.body = JSON.dump(body)
36
+
37
+ res = http.request(req)
38
+
39
+ begin
40
+ parsed = JSON.parse(res.body.to_s)
41
+ rescue JSON::ParserError => e
42
+ raise BrowserStack::LocalException.new(
43
+ "Failed to parse binary endpoint API response (HTTP #{res.code}): #{e.message}"
44
+ )
45
+ end
46
+
47
+ if parsed.is_a?(Hash) && parsed['error']
48
+ raise BrowserStack::LocalException.new(
49
+ "Binary endpoint API returned error: #{parsed['error']}"
50
+ )
51
+ end
52
+
53
+ endpoint = parsed.is_a?(Hash) ? parsed.dig('data', 'endpoint') : nil
54
+ if endpoint.nil? || endpoint.to_s.empty?
55
+ raise BrowserStack::LocalException.new(
56
+ "Binary endpoint API returned no endpoint (HTTP #{res.code})"
57
+ )
58
+ end
59
+
60
+ endpoint
61
+ end
62
+ end
63
+ end
@@ -1,6 +1,7 @@
1
1
  require 'browserstack/localbinary'
2
2
  require 'browserstack/localexception'
3
3
  require 'json'
4
+ require 'fileutils'
4
5
 
5
6
  module BrowserStack
6
7
 
@@ -64,15 +65,25 @@ class Local
64
65
  end
65
66
 
66
67
  @binary_path = if @binary_path.nil?
67
- BrowserStack::LocalBinary.new.binary_path
68
+ BrowserStack::LocalBinary.new(
69
+ auth_token: @key,
70
+ proxy_host: @proxy_host,
71
+ proxy_port: @proxy_port
72
+ ).binary_path
68
73
  else
69
74
  @binary_path
70
75
  end
71
76
 
72
- if @is_windows
73
- system("echo > #{@logfile}")
74
- else
75
- system("echo '' > '#{@logfile}'")
77
+ # Create/truncate the logfile without a shell. The previous
78
+ # `system("echo ... > #{@logfile}")` passed @logfile to /bin/sh (or cmd.exe),
79
+ # so shell metacharacters in a caller-supplied logfile path executed as commands
80
+ # (CWE-78). File.write treats the path purely as a filename.
81
+ logfile_dir = File.dirname(@logfile)
82
+ FileUtils.mkdir_p(logfile_dir) unless File.directory?(logfile_dir)
83
+ begin
84
+ File.write(@logfile, "")
85
+ rescue SystemCallError => e
86
+ raise BrowserStack::LocalException.new("Unable to open logfile: #{e.message}")
76
87
  end
77
88
 
78
89
  if defined? spawn
@@ -117,12 +128,27 @@ class Local
117
128
  @pid = nil
118
129
  end
119
130
 
131
+ # Public accessor used by callers for debugging/logging. Return the command
132
+ # with the access key masked so it is never written to logs, CI artifacts or
133
+ # error trackers (CWE-312). The real key is still used for execution via
134
+ # start_command_args / start_command(false).
120
135
  def command
121
- start_command
136
+ start_command(true)
137
+ end
138
+
139
+ # Prevent Ruby's default #inspect from dumping @key when a Local instance is
140
+ # logged or included in an exception payload (CWE-312).
141
+ def inspect
142
+ redacted = instance_variables.map do |var|
143
+ value = var == :@key && !@key.to_s.empty? ? "[REDACTED]" : instance_variable_get(var)
144
+ "#{var}=#{value.inspect}"
145
+ end.join(", ")
146
+ "#<#{self.class}:0x#{format('%016x', object_id << 1)} #{redacted}>"
122
147
  end
123
148
 
124
- def start_command
125
- cmd = "#{@binary_path} -d start -logFile '#{@logfile}' #{@folder_flag} #{@key} #{@folder_path} #{@force_local_flag}"
149
+ def start_command(redact = false)
150
+ key = redact && !@key.to_s.empty? ? "[REDACTED]" : @key
151
+ cmd = "#{@binary_path} -d start -logFile '#{@logfile}' #{@folder_flag} #{key} #{@folder_path} #{@force_local_flag}"
126
152
  cmd += " -localIdentifier #{@local_identifier_flag}" if @local_identifier_flag
127
153
  cmd += " #{@only_flag} #{@only_automate_flag}"
128
154
  cmd += " -proxyHost #{@proxy_host}" if @proxy_host
@@ -3,28 +3,27 @@ require 'net/https'
3
3
  require 'rbconfig'
4
4
  require 'openssl'
5
5
  require 'tmpdir'
6
+ require 'fileutils'
6
7
  require 'browserstack/localexception'
8
+ require 'browserstack/fetch_download_source_url'
9
+ require 'browserstack/version'
7
10
 
8
11
  module BrowserStack
9
-
12
+
10
13
  class LocalBinary
11
- def initialize
12
- host_os = RbConfig::CONFIG['host_os']
13
- @http_path = case host_os
14
- when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
15
- @windows = true
16
- "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal.exe"
17
- when /darwin|mac os/
18
- "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-darwin-x64"
19
- when /linux-musl/
20
- "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-alpine"
21
- when /linux/
22
- if 1.size == 8
23
- "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-linux-x64"
24
- else
25
- "https://www.browserstack.com/local-testing/downloads/binaries/BrowserStackLocal-linux-ia32"
26
- end
27
- end
14
+ BASE_RETRIES = 9
15
+ FALLBACK_TRIGGER_RETRY = 4
16
+
17
+ def initialize(conf = {})
18
+ @auth_token = conf[:auth_token] || ENV['BROWSERSTACK_ACCESS_KEY']
19
+ @proxy_host = conf[:proxy_host]
20
+ @proxy_port = conf[:proxy_port]
21
+ @user_agent = conf[:user_agent] || "browserstack-local-ruby/#{BrowserStack::VERSION}"
22
+
23
+ @windows = !!(RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/)
24
+ @binary_filename = compute_binary_filename
25
+ @source_url = nil
26
+ @download_error_message = nil
28
27
 
29
28
  @ordered_paths = [
30
29
  File.join(File.expand_path('~'), '.browserstack'),
@@ -33,79 +32,132 @@ class LocalBinary
33
32
  ]
34
33
  end
35
34
 
36
- def download(dest_parent_dir)
37
- unless File.exist? dest_parent_dir
38
- Dir.mkdir dest_parent_dir
39
- end
40
- uri = URI.parse(@http_path)
41
- binary_path = File.join(dest_parent_dir, "BrowserStackLocal#{".exe" if @windows}")
42
- http = Net::HTTP.new(uri.host, uri.port)
43
- http.use_ssl = true
44
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
45
-
46
- res = http.get(uri.path)
47
- file = open(binary_path, 'wb')
48
- file.write(res.body)
49
- file.close
50
- FileUtils.chmod 0755, binary_path
51
-
52
- binary_path
53
- end
35
+ def binary_path
36
+ dest_parent_dir = get_available_dirs
37
+ bin_path = File.join(dest_parent_dir, dest_binary_name)
54
38
 
55
- def verify_binary(binary_path)
56
- binary_response = IO.popen(binary_path + " --version").readline
57
- binary_response =~ /BrowserStack Local version \d+\.\d+/
58
- rescue Exception => e
59
- false
39
+ return bin_path if File.exist?(bin_path) && verify_binary(bin_path)
40
+
41
+ File.delete(bin_path) if File.exist?(bin_path)
42
+ download_with_retries(bin_path)
60
43
  end
61
44
 
62
- def binary_path
63
- dest_parent_dir = get_available_dirs
64
- binary_path = File.join(dest_parent_dir, "BrowserStackLocal#{".exe" if @windows}")
45
+ private
65
46
 
66
- if File.exist? binary_path
67
- binary_path
47
+ def dest_binary_name
48
+ @windows ? 'BrowserStackLocal.exe' : 'BrowserStackLocal'
49
+ end
50
+
51
+ def compute_binary_filename
52
+ host_os = RbConfig::CONFIG['host_os']
53
+ host_cpu = RbConfig::CONFIG['host_cpu']
54
+ case host_os
55
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
56
+ 'BrowserStackLocal.exe'
57
+ when /darwin|mac os/
58
+ 'BrowserStackLocal-darwin-x64'
59
+ when /linux/
60
+ if host_cpu =~ /arm64|aarch64/
61
+ 'BrowserStackLocal-linux-arm64'
62
+ elsif host_os =~ /linux-musl/
63
+ 'BrowserStackLocal-alpine'
64
+ elsif 1.size == 8
65
+ 'BrowserStackLocal-linux-x64'
66
+ else
67
+ 'BrowserStackLocal-linux-ia32'
68
+ end
68
69
  else
69
- binary_path = download(dest_parent_dir)
70
+ raise BrowserStack::LocalException.new("Unsupported host OS: #{host_os}")
70
71
  end
72
+ end
71
73
 
72
- valid_binary = verify_binary(binary_path)
73
-
74
- if valid_binary
75
- binary_path
76
- else
77
- binary_path = download(dest_parent_dir)
78
- valid_binary = verify_binary(binary_path)
79
- if valid_binary
80
- binary_path
81
- else
82
- raise BrowserStack::LocalException.new('BrowserStack Local binary is corrupt')
74
+ def download_with_retries(bin_path)
75
+ retries = BASE_RETRIES
76
+ while retries > 0
77
+ refresh_source_url(retries) if retries == BASE_RETRIES || retries == FALLBACK_TRIGGER_RETRY
78
+ begin
79
+ download_to(@source_url + '/' + @binary_filename, bin_path)
80
+ return bin_path if verify_binary(bin_path)
81
+ @download_error_message = 'Downloaded binary failed verification'
82
+ rescue StandardError => e
83
+ @download_error_message = "Download failed: #{e.message}"
83
84
  end
85
+ File.delete(bin_path) if File.exist?(bin_path)
86
+ retries -= 1
84
87
  end
88
+
89
+ raise BrowserStack::LocalException.new(
90
+ "Failed to download BrowserStack Local binary after #{BASE_RETRIES} attempts. " \
91
+ "Last error: #{@download_error_message}"
92
+ )
85
93
  end
86
94
 
87
- private
95
+ def refresh_source_url(retries)
96
+ is_fallback = (retries == FALLBACK_TRIGGER_RETRY) && !@download_error_message.nil?
97
+ begin
98
+ @source_url = BrowserStack::FetchDownloadSourceUrl.call(
99
+ auth_token: @auth_token,
100
+ user_agent: @user_agent,
101
+ fallback: is_fallback,
102
+ error_message: @download_error_message,
103
+ proxy_host: @proxy_host,
104
+ proxy_port: @proxy_port
105
+ )
106
+ rescue StandardError => e
107
+ raise if @source_url.nil?
108
+ @download_error_message = "Source URL refresh failed: #{e.message}"
109
+ end
110
+ end
111
+
112
+ def download_to(url, bin_path)
113
+ uri = URI.parse(url)
114
+ http_class = if @proxy_host && @proxy_port
115
+ Net::HTTP::Proxy(@proxy_host, @proxy_port.to_i)
116
+ else
117
+ Net::HTTP
118
+ end
119
+ http = http_class.new(uri.host, uri.port)
120
+ http.use_ssl = (uri.scheme == 'https')
121
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
122
+ http.open_timeout = 10
123
+ http.read_timeout = 30
124
+
125
+ req = Net::HTTP::Get.new(uri.request_uri)
126
+ req['User-Agent'] = @user_agent
127
+
128
+ res = http.request(req)
129
+ raise "HTTP #{res.code}" unless res.is_a?(Net::HTTPSuccess)
130
+
131
+ File.open(bin_path, 'wb') { |f| f.write(res.body) }
132
+ FileUtils.chmod 0755, bin_path
133
+ end
134
+
135
+ def verify_binary(bin_path)
136
+ # Array form: exec's the binary directly, so a path containing shell
137
+ # metacharacters or spaces is never interpreted by /bin/sh (CWE-78).
138
+ #
139
+ # The scanner rule below fires on any non-static first argument to IO.popen
140
+ # and does not model the array form -- which is exactly the fix here, since
141
+ # no shell is spawned at all. Suppressed for this rule only.
142
+ # nosemgrep: ruby.lang.security.dangerous-exec.dangerous-exec
143
+ binary_response = IO.popen([bin_path, '--version']).readline
144
+ !!(binary_response =~ /BrowserStack Local version \d+\.\d+/)
145
+ rescue StandardError
146
+ false
147
+ end
88
148
 
89
149
  def get_available_dirs
90
- i = 0
91
- while i < @ordered_paths.size
92
- path = @ordered_paths[i]
93
- if make_path(path)
94
- return path
95
- else
96
- i += 1
97
- end
150
+ @ordered_paths.each do |path|
151
+ return path if make_path(path)
98
152
  end
99
153
  raise BrowserStack::LocalException.new('Error trying to download BrowserStack Local binary')
100
154
  end
101
155
 
102
156
  def make_path(path)
103
- begin
104
- FileUtils.mkdir_p path if !File.directory?(path)
105
- return true
106
- rescue Exception
107
- return false
108
- end
157
+ FileUtils.mkdir_p(path) unless File.directory?(path)
158
+ true
159
+ rescue StandardError
160
+ false
109
161
  end
110
162
  end
111
163
 
@@ -0,0 +1,3 @@
1
+ module BrowserStack
2
+ VERSION = '1.5.1'.freeze
3
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: browserstack-local
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.3
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - BrowserStack
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-24 00:00:00.000000000 Z
11
+ date: 2026-06-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Ruby bindings for BrowserStack Local
14
14
  email: support@browserstack.com
@@ -16,14 +16,16 @@ executables: []
16
16
  extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
+ - lib/browserstack/fetch_download_source_url.rb
19
20
  - lib/browserstack/local.rb
20
21
  - lib/browserstack/localbinary.rb
21
22
  - lib/browserstack/localexception.rb
23
+ - lib/browserstack/version.rb
22
24
  homepage: http://rubygems.org/gems/browserstack-local
23
25
  licenses:
24
26
  - MIT
25
27
  metadata: {}
26
- post_install_message:
28
+ post_install_message:
27
29
  rdoc_options: []
28
30
  require_paths:
29
31
  - lib
@@ -39,7 +41,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
39
41
  version: '0'
40
42
  requirements: []
41
43
  rubygems_version: 3.0.3.1
42
- signing_key:
44
+ signing_key:
43
45
  specification_version: 4
44
46
  summary: BrowserStack Local
45
47
  test_files: []