diff --git a/lib/appium_lib_core/driver.rb b/lib/appium_lib_core/driver.rb
index 87f7ec1d..65833129 100644
--- a/lib/appium_lib_core/driver.rb
+++ b/lib/appium_lib_core/driver.rb
@@ -13,6 +13,8 @@
# limitations under the License.
require 'uri'
+require 'socket'
+require 'ipaddr'
module Appium
# The struct for 'location'
@@ -99,6 +101,44 @@ def initialize(capabilities)
@port = capabilities[W3C_KEYS[:port]] || capabilities[KEYS[:port]]
@path = capabilities[W3C_KEYS[:path]] || capabilities[KEYS[:path]]
end
+
+ def valid?
+ return false unless [@protocol, @host, @port, @path].none?(&:nil?)
+
+ addresses = resolve_addresses(@host)
+ return false if addresses.empty?
+
+ addresses.find { |ip| disallowed? ip }.nil?
+ end
+
+ private
+
+ # Do not allow loopback, link-local, unspecified and multicast addresses for
+ # direct connect since they are not accessible from outside of the server.
+ LOOPBACK_RANGES = [IPAddr.new('127.0.0.0/8'), IPAddr.new('::1/128')].freeze
+ LINK_LOCAL_RANGES = [IPAddr.new('169.254.0.0/16'), IPAddr.new('fe80::/10')].freeze
+ UNSPECIFIED_RANGES = [IPAddr.new('0.0.0.0/32'), IPAddr.new('::/128')].freeze
+ MULTICAST_RANGES = [IPAddr.new('224.0.0.0/4'), IPAddr.new('ff00::/8')].freeze
+ DISALLOWED_RANGES = [
+ *LOOPBACK_RANGES,
+ *LINK_LOCAL_RANGES,
+ *UNSPECIFIED_RANGES,
+ *MULTICAST_RANGES
+ ].freeze
+
+ def resolve_addresses(host)
+ normalized_host = host.to_s.delete_prefix('[').delete_suffix(']')
+ Socket.getaddrinfo(normalized_host, nil).map { |entry| entry[3] }.uniq
+ rescue SocketError => e
+ error_message = "Failed to resolve host '#{host}' for direct connect: #{e.message}"
+ ::Appium::Logger.warn(error_message)
+ []
+ end
+
+ def disallowed?(ip)
+ address = IPAddr.new ip
+ DISALLOWED_RANGES.any? { |range| range.include? address }
+ end
end
class Driver
@@ -164,8 +204,7 @@ class Driver
# @return [Appium::Core::Base::Driver]
attr_reader :driver
- # [Experimental feature]
- # Enable an experimental feature updating Http client endpoint following below keys by Appium/Selenium server.
+ # Enable updating Http client endpoint following below keys by Appium/Selenium server.
# This works with {Appium::Core::Base::Http::Default}.
#
# If your Selenium/Appium server decorates the new session capabilities response with the following keys:
@@ -177,6 +216,12 @@ class Driver
# ignore them if this parameter is false. Defaults to true.
# These keys can have appium: prefix.
#
+ # Note that the server should provide the keys with valid values. The host value must not be
+ # - loopback (for example `127.0.0.1`, `::1`)
+ # - link-local (for example `169.254.x.x`, `fe80::/10`)
+ # - unspecified/wildcard (`0.0.0.0`, `::`)
+ # - multicast (`224.0.0.0/4`, `ff00::/8`)
+ #
# @return [Bool]
attr_reader :direct_connect
@@ -419,7 +464,14 @@ def start_driver(server_url: nil,
if @direct_connect
d_c = DirectConnections.new(@driver.capabilities)
- @driver.update_sending_request_to(protocol: d_c.protocol, host: d_c.host, port: d_c.port, path: d_c.path)
+ if d_c.valid?
+ @driver.update_sending_request_to(protocol: d_c.protocol, host: d_c.host, port: d_c.port, path: d_c.path)
+ else
+ ::Appium::Logger.warn(
+ "Direct connect is enabled but the server did not provide valid direct connect information (#{d_c.protocol}, #{d_c.host}, #{d_c.port}, #{d_c.path}). " \
+ "Continue with the original URL (#{@custom_url})"
+ )
+ end
end
rescue Errno::ECONNREFUSED => e
raise "ERROR: Unable to connect to Appium. Is the server running on #{@custom_url}? Error: #{e}"
diff --git a/test/unit/driver_test.rb b/test/unit/driver_test.rb
index c0e7ee0c..91a39b42 100644
--- a/test/unit/driver_test.rb
+++ b/test/unit/driver_test.rb
@@ -149,7 +149,7 @@ def test_default_timeout_for_http_client_with_direct
appActivity: 'io.appium.android.apis.ApiDemos',
someCapability: 'some_capability',
directConnectProtocol: 'http',
- directConnectHost: 'localhost',
+ directConnectHost: '1.1.1.1',
directConnectPort: '8888',
directConnectPath: '/wd/hub'
}
@@ -159,14 +159,14 @@ def test_default_timeout_for_http_client_with_direct
stub_request(:post, 'http://127.0.0.1:4723/session')
.to_return(headers: HEADER, status: 200, body: response)
- stub_request(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts')
+ stub_request(:post, 'http://1.1.1.1:8888/wd/hub/session/1234567890/timeouts')
.with(body: { implicit: 30_000 }.to_json)
.to_return(headers: HEADER, status: 200, body: { value: nil }.to_json)
driver = core.start_driver
assert_requested(:post, 'http://127.0.0.1:4723/session', times: 1)
- assert_requested(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts',
+ assert_requested(:post, 'http://1.1.1.1:8888/wd/hub/session/1234567890/timeouts',
body: { implicit: 30_000 }.to_json, times: 1)
driver
end
@@ -179,7 +179,7 @@ def test_default_timeout_for_http_client_with_direct
uri = driver.send(:bridge).http.send(:server_url)
assert core.direct_connect
assert_equal 'http', uri.scheme
- assert_equal 'localhost', uri.host
+ assert_equal '1.1.1.1', uri.host
assert_equal 8888, uri.port
assert_equal '/wd/hub/', uri.path
end
@@ -198,8 +198,8 @@ def test_default_timeout_for_http_client_with_direct_appium_prefix
appPackage: 'io.appium.android.apis',
appActivity: 'io.appium.android.apis.ApiDemos',
someCapability: 'some_capability',
- 'appium:directConnectProtocol' => 'http',
- 'appium:directConnectHost' => 'localhost',
+ 'appium:directConnectProtocol' => 'https',
+ 'appium:directConnectHost' => 'appium.io',
'appium:directConnectPort' => '8888',
'appium:directConnectPath' => '/wd/hub'
}
@@ -209,14 +209,14 @@ def test_default_timeout_for_http_client_with_direct_appium_prefix
stub_request(:post, 'http://127.0.0.1:4723/session')
.to_return(headers: HEADER, status: 200, body: response)
- stub_request(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts')
+ stub_request(:post, 'https://appium.io:8888/wd/hub/session/1234567890/timeouts')
.with(body: { implicit: 30_000 }.to_json)
.to_return(headers: HEADER, status: 200, body: { value: nil }.to_json)
driver = core.start_driver
assert_requested(:post, 'http://127.0.0.1:4723/session', times: 1)
- assert_requested(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts',
+ assert_requested(:post, 'https://appium.io:8888/wd/hub/session/1234567890/timeouts',
body: { implicit: 30_000 }.to_json, times: 1)
driver
end
@@ -228,8 +228,8 @@ def test_default_timeout_for_http_client_with_direct_appium_prefix
assert_equal 999_999, driver.send(:bridge).http.read_timeout
uri = driver.send(:bridge).http.send(:server_url)
assert core.direct_connect
- assert_equal 'http', uri.scheme
- assert_equal 'localhost', uri.host
+ assert_equal 'https', uri.scheme
+ assert_equal 'appium.io', uri.host
assert_equal 8888, uri.port
assert_equal '/wd/hub/', uri.path
end
@@ -249,7 +249,7 @@ def test_default_timeout_for_http_client_with_direct_appium_prefix_prior_than_no
appActivity: 'io.appium.android.apis.ApiDemos',
someCapability: 'some_capability',
'appium:directConnectProtocol' => 'http',
- 'appium:directConnectHost' => 'localhost',
+ 'appium:directConnectHost' => '1.1.1.1',
'appium:directConnectPort' => '8888',
'appium:directConnectPath' => '/wd/hub',
directConnectProtocol: 'https',
@@ -263,14 +263,14 @@ def test_default_timeout_for_http_client_with_direct_appium_prefix_prior_than_no
stub_request(:post, 'http://127.0.0.1:4723/session')
.to_return(headers: HEADER, status: 200, body: response)
- stub_request(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts')
+ stub_request(:post, 'http://1.1.1.1:8888/wd/hub/session/1234567890/timeouts')
.with(body: { implicit: 30_000 }.to_json)
.to_return(headers: HEADER, status: 200, body: { value: nil }.to_json)
driver = core.start_driver
assert_requested(:post, 'http://127.0.0.1:4723/session', times: 1)
- assert_requested(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts',
+ assert_requested(:post, 'http://1.1.1.1:8888/wd/hub/session/1234567890/timeouts',
body: { implicit: 30_000 }.to_json, times: 1)
driver
end
@@ -283,11 +283,54 @@ def test_default_timeout_for_http_client_with_direct_appium_prefix_prior_than_no
uri = driver.send(:bridge).http.send(:server_url)
assert core.direct_connect
assert_equal 'http', uri.scheme
- assert_equal 'localhost', uri.host
+ assert_equal '1.1.1.1', uri.host
assert_equal 8888, uri.port
assert_equal '/wd/hub/', uri.path
end
+ def test_direct_connect_loopback_host_falls_back_to_original_url
+ response = {
+ value: {
+ sessionId: '1234567890',
+ capabilities: {
+ platformName: :android,
+ automationName: ENV['APPIUM_DRIVER'] || 'uiautomator2',
+ app: 'test/functional/app/ApiDemos-debug.apk',
+ platformVersion: '7.1.1',
+ deviceName: 'Android Emulator',
+ appPackage: 'io.appium.android.apis',
+ appActivity: 'io.appium.android.apis.ApiDemos',
+ someCapability: 'some_capability',
+ directConnectProtocol: 'http',
+ directConnectHost: 'localhost',
+ directConnectPort: '8888',
+ directConnectPath: '/wd/hub'
+ }
+ }
+ }.to_json
+
+ stub_request(:post, 'http://127.0.0.1:4723/session')
+ .to_return(headers: HEADER, status: 200, body: response)
+
+ stub_request(:post, 'http://127.0.0.1:4723/session/1234567890/timeouts')
+ .with(body: { implicit: 30_000 }.to_json)
+ .to_return(headers: HEADER, status: 200, body: { value: nil }.to_json)
+
+ core = ::Appium::Core.for(Caps.android_direct)
+ driver = core.start_driver
+
+ uri = driver.send(:bridge).http.send(:server_url)
+ assert_equal 'http', uri.scheme
+ assert_equal '127.0.0.1', uri.host
+ assert_equal 4723, uri.port
+ assert_equal '/', uri.path
+
+ assert_requested(:post, 'http://127.0.0.1:4723/session', times: 1)
+ assert_requested(:post, 'http://127.0.0.1:4723/session/1234567890/timeouts',
+ body: { implicit: 30_000 }.to_json, times: 1)
+ assert_not_requested(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts')
+ end
+
def test_default_timeout_for_http_client_with_direct_no_path
android_mock_create_session_w3c_direct_no_path = lambda do |core|
response = {
@@ -303,7 +346,7 @@ def test_default_timeout_for_http_client_with_direct_no_path
appActivity: 'io.appium.android.apis.ApiDemos',
someCapability: 'some_capability',
directConnectProtocol: 'http',
- directConnectHost: 'localhost',
+ directConnectHost: '1.1.1.1',
directConnectPort: '8888'
}
}
@@ -352,7 +395,7 @@ def test_default_timeout_for_http_client_with_direct_no_supported_client
appActivity: 'io.appium.android.apis.ApiDemos',
someCapability: 'some_capability',
directConnectProtocol: 'http',
- directConnectHost: 'localhost',
+ directConnectHost: '1.1.1.1',
directConnectPort: '8888',
directConnectPath: '/wd/hub'
}
@@ -517,7 +560,7 @@ def test_attach_to_an_existing_session
appActivity: 'io.appium.android.apis.ApiDemos',
someCapability: 'some_capability',
directConnectProtocol: 'http',
- directConnectHost: 'localhost',
+ directConnectHost: '1.1.1.1',
directConnectPort: '8888',
directConnectPath: '/wd/hub'
}
@@ -527,14 +570,14 @@ def test_attach_to_an_existing_session
stub_request(:post, 'http://127.0.0.1:4723/session')
.to_return(headers: HEADER, status: 200, body: response)
- stub_request(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts')
+ stub_request(:post, 'http://1.1.1.1:8888/wd/hub/session/1234567890/timeouts')
.with(body: { implicit: 30_000 }.to_json)
.to_return(headers: HEADER, status: 200, body: { value: nil }.to_json)
driver = core.start_driver
assert_requested(:post, 'http://127.0.0.1:4723/session', times: 1)
- assert_requested(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts',
+ assert_requested(:post, 'http://1.1.1.1:8888/wd/hub/session/1234567890/timeouts',
body: { implicit: 30_000 }.to_json, times: 1)
driver
end