class Excon::Connection
def batch_requests(pipeline_params, limit = nil)
-
pipeline_params
(Array
) -- An array of one or more optional params, override defaults set in Connection.new, see #request for details
def batch_requests(pipeline_params, limit = nil) limit ||= Process.respond_to?(:getrlimit) ? Process.getrlimit(:NOFILE).first : 256 responses = [] pipeline_params.each_slice(limit) do |params| responses.concat(requests(params)) end responses end
def connection
def connection Excon.display_warning('Excon::Connection#connection is deprecated use Excon::Connection#data instead.') @data end
def connection=(new_params)
def connection=(new_params) Excon.display_warning('Excon::Connection#connection= is deprecated. Use of this method may cause unexpected results.') @data = new_params end
def detect_content_length(body)
def detect_content_length(body) if body.respond_to?(:size) # IO object: File, Tempfile, StringIO, etc. body.size elsif body.respond_to?(:stat) # for 1.8.7 where file does not have size body.stat.size else 0 end end
def error_call(datum)
def error_call(datum) if datum[:error] raise(datum[:error]) end end
def initialize(params = {})
(**params)
-
:instrumentor_name
(String
) -- Name prefix for #instrument events. Defaults to 'excon' -
:instrumentor
(Class
) -- Responds to #instrument as in ActiveSupport::Notifications -
:retry_interval
(Fixnum
) -- Set how long to wait between retries. (Default 0) -
:retry_limit
(Fixnum
) -- Set how many times we'll retry a failed request. (Default 4) -
:proxy
(String
) -- Proxy server; e.g. 'http://myproxy.com:8888' -
:ciphers
(String
) -- Only use the specified SSL/TLS cipher suites; use OpenSSL cipher spec format e.g. 'HIGH:!aNULL:!3DES' or 'AES256-SHA:DES-CBC3-SHA' -
:socket
(String
) -- The path to the unix socket (required for 'unix://' connections) -
:scheme
(String
) -- The protocol; 'https' causes OpenSSL to be used -
:query
(Hash
) -- Default query; appended to the 'scheme://host:port/path/' in the form of '?key=value'. Will only be used if params[:query] is not supplied to Connection#request -
:port
(Fixnum
) -- The port on which to connect, to the destination host -
:path
(String
) -- Default path; appears after 'scheme://host:port/'. Only used if params[:path] is not supplied to Connection#request -
:hostname
(String
) -- Same as host, but usable for socket connections. IPv6 addresses must not be wrapped (e.g. ::1). See URI#hostname. -
:host
(String
) -- The destination host's reachable DNS name or IP, in the form of a String. IPv6 addresses must be wrapped (e.g. [::1]). See URI#host. -
:headers
(Hash
) -- The default headers to supply in a request. Only used if params[:headers] is not supplied to Connection#request -
:body
(String
) -- Default text to be sent over a socket. Only used if :body absent in Connection#request params
Parameters:
-
params
(Hash
) -- One or more optional params
def initialize(params = {}) @data = Excon.defaults.dup # merge does not deep-dup, so make sure headers is not the original @data[:headers] = @data[:headers].dup # the same goes for :middlewares @data[:middlewares] = @data[:middlewares].dup @data.merge!(params) validate_params(:connection, @data, @data[:middlewares]) if @data.key?(:host) && !@data.key?(:hostname) Excon.display_warning('hostname is missing! For IPv6 support, provide both host and hostname: Excon::Connection#new(:host => uri.host, :hostname => uri.hostname, ...).') @data[:hostname] = @data[:host] end setup_proxy if ENV.has_key?('EXCON_STANDARD_INSTRUMENTOR') @data[:instrumentor] = Excon::StandardInstrumentor end if @data[:debug] || ENV.has_key?('EXCON_DEBUG') @data[:debug_request] = @data[:debug_response] = true @data[:instrumentor] = Excon::StandardInstrumentor end if @data[:scheme] == UNIX if @data[:host] raise ArgumentError, "The `:host` parameter should not be set for `unix://` connections.\n" + "When supplying a `unix://` URI, it should start with `unix:/` or `unix:///`." elsif !@data[:socket] raise ArgumentError, 'You must provide a `:socket` for `unix://` connections' else @socket_key = "#{@data[:scheme]}://#{@data[:socket]}" end else @socket_key = "#{@data[:scheme]}://#{@data[:host]}#{port_string(@data)}" end reset end
def inspect
def inspect vars = instance_variables.inject({}) do |accum, var| accum.merge!(var.to_sym => instance_variable_get(var)) end vars[:'@data'] = Utils.redact(vars[:'@data']) inspection = '#<Excon::Connection:' inspection += (object_id << 1).to_s(16) vars.each do |key, value| inspection += " #{key}=#{value.inspect}" end inspection += '>' inspection end
def logger
def logger if @data[:instrumentor] && @data[:instrumentor].respond_to?(:logger) @data[:instrumentor].logger end end
def logger=(logger)
def logger=(logger) @data[:instrumentor] = Excon::LoggingInstrumentor @data[:logger] = logger end
def params
def params Excon.display_warning('Excon::Connection#params is deprecated use Excon::Connection#data instead.') @data end
def params=(new_params)
def params=(new_params) Excon.display_warning('Excon::Connection#params= is deprecated. Use of this method may cause unexpected results.') @data = new_params end
def proxy
def proxy Excon.display_warning('Excon::Connection#proxy is deprecated use Excon::Connection#data[:proxy] instead.') @data[:proxy] end
def proxy=(new_proxy)
def proxy=(new_proxy) Excon.display_warning('Excon::Connection#proxy= is deprecated. Use of this method may cause unexpected results.') @data[:proxy] = new_proxy end
def proxy_from_env
def proxy_from_env if (no_proxy_env = ENV['no_proxy'] || ENV['NO_PROXY']) no_proxy_list = no_proxy_env.scan(/\s*(?:\[([\dA-Fa-f:\/]+)\]|\*?\.?([^\s,:]+))(?::(\d+))?\s*/i).map { |e| if e[0] begin [IPAddr.new(e[0]), e[2]] rescue IPAddr::Error nil end else begin [IPAddr.new(e[1]), e[2]] rescue IPAddr::Error [e[1], e[2]] end end }.reject { |e| e.nil? || e[0].nil? } end unless no_proxy_env && no_proxy_list.index { |h| proxy_match_host_port(h[0], h[1]) } if @data[:scheme] == HTTPS && (ENV.has_key?('https_proxy') || ENV.has_key?('HTTPS_PROXY')) @data[:proxy] = ENV['https_proxy'] || ENV['HTTPS_PROXY'] elsif (ENV.has_key?('http_proxy') || ENV.has_key?('HTTP_PROXY')) @data[:proxy] = ENV['http_proxy'] || ENV['HTTP_PROXY'] end end end
def proxy_match_host_port(host, port)
def proxy_match_host_port(host, port) host_match = if host.is_a? IPAddr begin host.include? @data[:host] rescue IPAddr::Error false end else /(^|\.)#{host}$/.match(@data[:host]) end host_match && (port.nil? || port.to_i == @data[:port]) end
def raise_socket_error(error)
def raise_socket_error(error) if error.message =~ /certificate verify failed/ raise(Excon::Errors::CertificateError.new(error)) else raise(Excon::Errors::SocketError.new(error)) end end
def request(params={}, &block)
(**params)
-
:query
(Hash
) -- appended to the 'scheme://host:port/path/' in the form of '?key=value' -
:path
(String
) -- appears after 'scheme://host:port/' -
:headers
(Hash
) -- The default headers to supply in a request -
:body
(String
) -- text to be sent over a socket
Parameters:
-
params
(Hash
) -- One or more optional params, override defaults set in Connection.new
Other tags:
- Yield: - @see Response#self.parse
def request(params={}, &block) # @data has defaults, merge in new params to override datum = @data.merge(params) datum[:headers] = @data[:headers].merge(datum[:headers] || {}) validate_params(:request, params, datum[:middlewares]) # If the user passed in new middleware, we want to validate that the original connection parameters # are still valid with the provided middleware. if params[:middlewares] validate_params(:connection, @data, datum[:middlewares]) end if datum[:user] || datum[:password] user, pass = Utils.unescape_uri(datum[:user].to_s), Utils.unescape_uri(datum[:password].to_s) datum[:headers]['Authorization'] ||= 'Basic ' + ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL) end if datum[:scheme] == UNIX datum[:headers]['Host'] ||= '' else datum[:headers]['Host'] ||= datum[:host] + port_string(datum) end # RFC 7230, section 5.4, states that the Host header SHOULD be the first one # to be present. # Some web servers will reject the request if it comes too late, so let's hoist it to the top. if (host = datum[:headers].delete('Host')) datum[:headers] = { 'Host' => host }.merge(datum[:headers]) end # default to GET if no method specified unless datum[:method] datum[:method] = :get end # if path is empty or doesn't start with '/', insert one unless datum[:path][0, 1] == '/' datum[:path] = datum[:path].dup.insert(0, '/') end if block_given? Excon.display_warning('Excon requests with a block are deprecated, pass :response_block instead.') datum[:response_block] = block end datum[:connection] = self # cleanup data left behind on persistent connection after interrupt if datum[:persistent] && !@persistent_socket_reusable reset end datum[:stack] = datum[:middlewares].map do |middleware| lambda {|stack| middleware.new(stack)} end.reverse.inject(self) do |middlewares, middleware| middleware.call(middlewares) end datum = datum[:stack].request_call(datum) unless datum[:pipeline] @persistent_socket_reusable = false datum = response(datum) @persistent_socket_reusable = true if datum[:persistent] if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 }) if datum[:response][:headers][key].casecmp('close') == 0 reset end end else reset end Excon::Response.new(datum[:response]) else datum end rescue => error reset # If we didn't get far enough to initialize datum and the middleware stack, just raise raise error if !datum datum[:error] = error if datum[:stack] datum[:stack].error_call(datum) else raise error end end
def request_call(datum)
def request_call(datum) begin if datum.has_key?(:response) # we already have data from a middleware, so bail return datum else socket(datum).data = datum # start with "METHOD /path" request = datum[:method].to_s.upcase + ' ' if datum[:proxy] && datum[:scheme] != HTTPS request << datum[:scheme] << '://' << datum[:host] << port_string(datum) end request << datum[:path] # add query to path, if there is one request << query_string(datum) # finish first line with "HTTP/1.1\r\n" request << HTTP_1_1 if datum.has_key?(:request_block) datum[:headers]['Transfer-Encoding'] = 'chunked' else body = datum[:body].is_a?(String) ? StringIO.new(datum[:body]) : datum[:body] # The HTTP spec isn't clear on it, but specifically, GET requests don't usually send bodies; # if they don't, sending Content-Length:0 can cause issues. unless datum[:method].to_s.casecmp('GET') == 0 && body.nil? unless datum[:headers].has_key?('Content-Length') datum[:headers]['Content-Length'] = detect_content_length(body) end end end # add headers to request request << Utils.headers_hash_to_s(datum[:headers]) # add additional "\r\n" to indicate end of headers request << CR_NL if datum.has_key?(:request_block) socket(datum).write(request) # write out request + headers while true # write out body with chunked encoding chunk = datum[:request_block].call chunk = binary_encode(chunk) if chunk.length > 0 socket(datum).write(chunk.length.to_s(16) << CR_NL << chunk << CR_NL) else socket(datum).write(String.new("0#{CR_NL}#{CR_NL}")) break end end elsif body.nil? socket(datum).write(request) # write out request + headers else # write out body if body.respond_to?(:binmode) && !body.is_a?(StringIO) body.binmode end if body.respond_to?(:rewind) body.rewind rescue nil end # if request + headers is less than chunk size, fill with body request = binary_encode(request) chunk = body.read([datum[:chunk_size] - request.length, 0].max) if chunk chunk = binary_encode(chunk) socket(datum).write(request << chunk) else socket(datum).write(request) # write out request + headers end while (chunk = body.read(datum[:chunk_size])) socket(datum).write(chunk) end end end rescue => error case error when Excon::Errors::InvalidHeaderKey, Excon::Errors::InvalidHeaderValue, Excon::Errors::StubNotFound, Excon::Errors::Timeout raise(error) when Errno::EPIPE # Read whatever remains in the pipe to aid in debugging response = socket.read error = Excon::Error.new(response + error.message) raise_socket_error(error) else raise_socket_error(error) end end datum end
def requests(pipeline_params)
-
pipeline_params
(Array
) -- An array of one or more optional params, override defaults set in Connection.new, see #request for details
def requests(pipeline_params) pipeline_params.each {|params| params.merge!(:pipeline => true, :persistent => true) } pipeline_params.last.merge!(:persistent => @data[:persistent]) responses = pipeline_params.map do |params| request(params) end.map do |datum| Excon::Response.new(response(datum)[:response]) end if @data[:persistent] if (key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 }) if responses.last[:headers][key].casecmp('close') == 0 reset end end else reset end responses end
def reset
def reset if (old_socket = sockets.delete(@socket_key)) old_socket.close rescue nil end @persistent_socket_reusable = true end
def response(datum={})
def response(datum={}) datum[:stack].response_call(datum) rescue => error case error when Excon::Errors::HTTPStatusError, Excon::Errors::Timeout, Excon::Errors::TooManyRedirects raise(error) else raise_socket_error(error) end end
def response_call(datum)
def response_call(datum) # ensure response_block is yielded to and body is empty from middlewares if datum.has_key?(:response_block) && !(datum[:response][:body].nil? || datum[:response][:body].empty?) response_body = datum[:response][:body].dup datum[:response][:body] = '' content_length = remaining = response_body.bytesize while remaining > 0 datum[:response_block].call(response_body.slice!(0, [datum[:chunk_size], remaining].min), [remaining - datum[:chunk_size], 0].max, content_length) remaining -= datum[:chunk_size] end end datum end
def retry_limit
def retry_limit Excon.display_warning('Excon::Connection#retry_limit is deprecated, use Excon::Connection#data[:retry_limit].') @data[:retry_limit] ||= DEFAULT_RETRY_LIMIT end
def retry_limit=(new_retry_limit)
def retry_limit=(new_retry_limit) Excon.display_warning('Excon::Connection#retry_limit= is deprecated, pass :retry_limit to the initializer.') @data[:retry_limit] = new_retry_limit end
def setup_proxy
def setup_proxy if @data[:disable_proxy] if @data[:proxy] raise ArgumentError, "`:disable_proxy` parameter and `:proxy` parameter cannot both be set at the same time." end return end return if @data[:scheme] == UNIX proxy_from_env case @data[:proxy] when nil @data.delete(:proxy) when '' @data.delete(:proxy) when Hash # no processing needed when String, URI uri = @data[:proxy].is_a?(String) ? URI.parse(@data[:proxy]) : @data[:proxy] @data[:proxy] = { :host => uri.host, :hostname => uri.hostname, # path is only sensible for a Unix socket proxy :path => uri.scheme == UNIX ? uri.path : nil, :port => uri.port, :scheme => uri.scheme, } if uri.password @data[:proxy][:password] = uri.password end if uri.user @data[:proxy][:user] = uri.user end if @data[:ssl_proxy_headers] && !@data[:ssl_uri_schemes].include?(@data[:scheme]) raise ArgumentError, "The `:ssl_proxy_headers` parameter should only be used with HTTPS requests." end if @data[:proxy][:scheme] == UNIX if @data[:proxy][:host] raise ArgumentError, "The `:host` parameter should not be set for `unix://` proxies.\n" + "When supplying a `unix://` URI, it should start with `unix:/` or `unix:///`." end else unless uri.host && uri.port && uri.scheme raise Excon::Errors::ProxyParse, "Proxy is invalid" end end else raise Excon::Errors::ProxyParse, "Proxy is invalid" end if @data.has_key?(:proxy) && @data[:scheme] == 'http' @data[:headers]['Proxy-Connection'] ||= 'Keep-Alive' # https credentials happen in handshake if @data[:proxy].has_key?(:user) || @data[:proxy].has_key?(:password) user, pass = Utils.unescape_form(@data[:proxy][:user].to_s), Utils.unescape_form(@data[:proxy][:password].to_s) auth = ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL) @data[:headers]['Proxy-Authorization'] = 'Basic ' + auth end end end
def socket(datum = @data)
def socket(datum = @data) unix_proxy = datum[:proxy] ? datum[:proxy][:scheme] == UNIX : false sockets[@socket_key] ||= if datum[:scheme] == UNIX || unix_proxy Excon::UnixSocket.new(datum) elsif datum[:ssl_uri_schemes].include?(datum[:scheme]) Excon::SSLSocket.new(datum) else Excon::Socket.new(datum) end end
def sockets
def sockets @_excon_sockets ||= {} @_excon_sockets.compare_by_identity if @data[:thread_safe_sockets] # In a multi-threaded world, if the same connection is used by multiple # threads at the same time to connect to the same destination, they may # stomp on each other's sockets. This ensures every thread gets their # own socket cache, within the context of a single connection. @_excon_sockets[Thread.current] ||= {} else @_excon_sockets end end
def valid_middleware_keys(middlewares)
def valid_middleware_keys(middlewares) middlewares.flat_map do |middleware| if middleware.respond_to?(:valid_parameter_keys) middleware.valid_parameter_keys else Excon.display_warning( "Excon middleware #{middleware} does not define #valid_parameter_keys" ) [] end end end
def valid_request_keys(middlewares)
def valid_request_keys(middlewares) valid_middleware_keys(middlewares) + Excon::VALID_REQUEST_KEYS end
def validate_params(validation, params, middlewares)
def validate_params(validation, params, middlewares) valid_keys = case validation when :connection valid_middleware_keys(middlewares) + Excon::VALID_CONNECTION_KEYS when :request valid_request_keys(middlewares) else raise ArgumentError.new("Invalid validation type '#{validation}'") end invalid_keys = params.keys - valid_keys unless invalid_keys.empty? Excon.display_warning("Invalid Excon #{validation} keys: #{invalid_keys.map(&:inspect).join(', ')}") if validation == :request deprecated_keys = invalid_keys & Excon::DEPRECATED_VALID_REQUEST_KEYS.keys mw_msg = deprecated_keys.map do |k| "#{k}: #{Excon::DEPRECATED_VALID_REQUEST_KEYS[k]}" end.join(', ') Excon.display_warning( "The following request keys are only valid with the associated middleware: #{mw_msg}" ) end end end