Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions lib/appium_lib_core/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
require 'uri'
require 'socket'
require 'ipaddr'
require 'timeout'

module Appium
# The struct for 'location'
Expand Down Expand Up @@ -115,6 +116,7 @@ def valid?

# Do not allow loopback, link-local, unspecified and multicast addresses for
# direct connect since they are not accessible from outside of the server.
DNS_RESOLVE_TIMEOUT_SECONDS = 30
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
Expand All @@ -128,13 +130,27 @@ def valid?

def resolve_addresses(host)
normalized_host = host.to_s.delete_prefix('[').delete_suffix(']')
Socket.getaddrinfo(normalized_host, nil).map { |entry| entry[3] }.uniq
# If the host is already an IP literal, skip DNS resolution to avoid blocking calls
return [normalized_host] if ip_literal?(normalized_host)

Timeout.timeout(DNS_RESOLVE_TIMEOUT_SECONDS) do
Socket.getaddrinfo(normalized_host, nil).map { |entry| entry[3] }.uniq
end
rescue Timeout::Error
::Appium::Logger.warn("DNS resolution for '#{host}' timed out after #{DNS_RESOLVE_TIMEOUT_SECONDS}s")
[]
rescue SocketError => e
error_message = "Failed to resolve host '#{host}' for direct connect: #{e.message}"
::Appium::Logger.warn(error_message)
::Appium::Logger.warn("Failed to resolve host '#{host}' for direct connect: #{e.message}")
[]
end

def ip_literal?(host)
IPAddr.new(host)
true
rescue IPAddr::InvalidAddressError
false
end

def disallowed?(ip)
address = IPAddr.new ip
DISALLOWED_RANGES.any? { |range| range.include? address }
Expand Down
43 changes: 43 additions & 0 deletions test/unit/driver_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ def test_default_timeout_for_http_client_with_direct_appium_prefix
appActivity: 'io.appium.android.apis.ApiDemos',
someCapability: 'some_capability',
'appium:directConnectProtocol' => 'https',
# Not the best, but to include tests with host name
'appium:directConnectHost' => 'appium.io',
'appium:directConnectPort' => '8888',
'appium:directConnectPath' => '/wd/hub'
Expand Down Expand Up @@ -331,6 +332,48 @@ def test_direct_connect_loopback_host_falls_back_to_original_url
assert_not_requested(:post, 'http://localhost:8888/wd/hub/session/1234567890/timeouts')
end

def test_direct_connect_dns_timeout_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: 'slow.example.internal',
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)

Socket.stub(:getaddrinfo, ->(*) { raise Timeout::Error }) do
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_requested(:post, 'http://127.0.0.1:4723/session', times: 1)
assert_not_requested(:post, 'http://slow.example.internal:8888/wd/hub/session/1234567890/timeouts')
end
end

def test_default_timeout_for_http_client_with_direct_no_path
android_mock_create_session_w3c_direct_no_path = lambda do |core|
response = {
Expand Down
Loading