1313# limitations under the License.
1414
1515require 'uri'
16+ require 'socket'
17+ require 'ipaddr'
18+ require 'timeout'
1619
1720module Appium
1821 # The struct for 'location'
@@ -98,6 +101,59 @@ def initialize(capabilities)
98101 @port = capabilities [ W3C_KEYS [ :port ] ] || capabilities [ KEYS [ :port ] ]
99102 @path = capabilities [ W3C_KEYS [ :path ] ] || capabilities [ KEYS [ :path ] ]
100103 end
104+
105+ def valid?
106+ return false unless [ @protocol , @host , @port , @path ] . none? ( &:nil? )
107+
108+ addresses = resolve_addresses ( @host )
109+ return false if addresses . empty?
110+
111+ addresses . find { |ip | disallowed? ip } . nil?
112+ end
113+
114+ private
115+
116+ # Do not allow loopback, link-local, unspecified and multicast addresses for
117+ # direct connect since they are not accessible from outside of the server.
118+ DNS_RESOLVE_TIMEOUT_SECONDS = 30
119+ LOOPBACK_RANGES = [ IPAddr . new ( '127.0.0.0/8' ) , IPAddr . new ( '::1/128' ) ] . freeze
120+ LINK_LOCAL_RANGES = [ IPAddr . new ( '169.254.0.0/16' ) , IPAddr . new ( 'fe80::/10' ) ] . freeze
121+ UNSPECIFIED_RANGES = [ IPAddr . new ( '0.0.0.0/32' ) , IPAddr . new ( '::/128' ) ] . freeze
122+ MULTICAST_RANGES = [ IPAddr . new ( '224.0.0.0/4' ) , IPAddr . new ( 'ff00::/8' ) ] . freeze
123+ DISALLOWED_RANGES = [
124+ *LOOPBACK_RANGES ,
125+ *LINK_LOCAL_RANGES ,
126+ *UNSPECIFIED_RANGES ,
127+ *MULTICAST_RANGES
128+ ] . freeze
129+
130+ def resolve_addresses ( host )
131+ normalized_host = host . to_s . delete_prefix ( '[' ) . delete_suffix ( ']' )
132+ # If the host is already an IP literal, skip DNS resolution to avoid blocking calls
133+ return [ normalized_host ] if ip_literal? ( normalized_host )
134+
135+ Timeout . timeout ( DNS_RESOLVE_TIMEOUT_SECONDS ) do
136+ Socket . getaddrinfo ( normalized_host , nil ) . map { |entry | entry [ 3 ] } . uniq
137+ end
138+ rescue Timeout ::Error
139+ ::Appium ::Logger . warn ( "DNS resolution for '#{ host } ' timed out after #{ DNS_RESOLVE_TIMEOUT_SECONDS } s" )
140+ [ ]
141+ rescue SocketError => e
142+ ::Appium ::Logger . warn ( "Failed to resolve host '#{ host } ' for direct connect: #{ e . message } " )
143+ [ ]
144+ end
145+
146+ def ip_literal? ( host )
147+ IPAddr . new ( host )
148+ true
149+ rescue IPAddr ::InvalidAddressError
150+ false
151+ end
152+
153+ def disallowed? ( ip )
154+ address = IPAddr . new ip
155+ DISALLOWED_RANGES . any? { |range | range . include? address }
156+ end
101157 end
102158
103159 class Driver
@@ -158,8 +214,7 @@ class Driver
158214 # @return [Appium::Core::Base::Driver]
159215 attr_reader :driver
160216
161- # <b>[Experimental feature]</b><br>
162- # Enable an experimental feature updating Http client endpoint following below keys by Appium/Selenium server.<br>
217+ # Enable updating Http client endpoint following below keys by Appium/Selenium server.<br>
163218 # This works with {Appium::Core::Base::Http::Default}.
164219 #
165220 # If your Selenium/Appium server decorates the new session capabilities response with the following keys:<br>
@@ -171,6 +226,12 @@ class Driver
171226 # ignore them if this parameter is <code>false</code>. Defaults to true.
172227 # These keys can have <code>appium:</code> prefix.
173228 #
229+ # Note that the server should provide the keys with valid values. The host value must not be
230+ # - loopback (for example `127.0.0.1`, `::1`)
231+ # - link-local (for example `169.254.x.x`, `fe80::/10`)
232+ # - unspecified/wildcard (`0.0.0.0`, `::`)
233+ # - multicast (`224.0.0.0/4`, `ff00::/8`)
234+ #
174235 # @return [Bool]
175236 attr_reader :direct_connect
176237
@@ -409,7 +470,14 @@ def start_driver(server_url: nil,
409470
410471 if @direct_connect
411472 d_c = DirectConnections . new ( @driver . capabilities )
412- @driver . update_sending_request_to ( protocol : d_c . protocol , host : d_c . host , port : d_c . port , path : d_c . path )
473+ if d_c . valid?
474+ @driver . update_sending_request_to ( protocol : d_c . protocol , host : d_c . host , port : d_c . port , path : d_c . path )
475+ else
476+ ::Appium ::Logger . warn (
477+ "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 } ). " \
478+ "Continue with the original URL (#{ @custom_url } )"
479+ )
480+ end
413481 end
414482 rescue Errno ::ECONNREFUSED => e
415483 raise "ERROR: Unable to connect to Appium. Is the server running on #{ @custom_url } ? Error: #{ e } "
0 commit comments