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 +4 -4
- data/lib/browserstack/fetch_download_source_url.rb +63 -0
- data/lib/browserstack/local.rb +34 -8
- data/lib/browserstack/localbinary.rb +125 -73
- data/lib/browserstack/version.rb +3 -0
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e32e052d39ddb27d0c6d4e65796a8d7155896124874e6b98cb7a187f8ac2580b
|
|
4
|
+
data.tar.gz: a3708dc83e2be4f34aa09efb80a3a7a414449fecef23f19d2cf58db933f114fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/browserstack/local.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
dest_parent_dir = get_available_dirs
|
|
64
|
-
binary_path = File.join(dest_parent_dir, "BrowserStackLocal#{".exe" if @windows}")
|
|
45
|
+
private
|
|
65
46
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
+
raise BrowserStack::LocalException.new("Unsupported host OS: #{host_os}")
|
|
70
71
|
end
|
|
72
|
+
end
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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
|
+
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:
|
|
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: []
|