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