Skip to content

Commit

Permalink
rip out httpi; pass the connection around and use faraday post since …
Browse files Browse the repository at this point in the history
…building a faraday request requires accessing internal apis and is very nasty; solve ntlm handshake; convert various httpi fields to their faraday equivalents and mark any that don't have one
  • Loading branch information
LukeIGS committed Jan 4, 2024
1 parent 118125c commit 6f2d6aa
Show file tree
Hide file tree
Showing 6 changed files with 435 additions and 375 deletions.
70 changes: 43 additions & 27 deletions lib/savon/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
require "savon/request_logger"
require "savon/http_error"
require "mail"
require 'faraday/gzip'


module Savon
class Operation
Expand Down Expand Up @@ -58,16 +60,20 @@ def call(locals = {}, &block)
builder = build(locals, &block)

response = Savon.notify_observers(@name, builder, @globals, @locals)
response ||= call_with_logging build_request(builder)
response ||= call_with_logging build_connection(builder)

raise_expected_httpi_response! unless response.kind_of?(HTTPI::Response)
raise_expected_faraday_response! unless response.kind_of?(Faraday::Response)

create_response(response)
end

def request(locals = {}, &block)
builder = build(locals, &block)
build_request(builder)
connection = build_connection(builder)
connection.build_request(:post) do |req|
req.url(@globals[:endpoint])
req.body = @locals[:body]
end
end

private
Expand All @@ -83,37 +89,47 @@ def set_locals(locals, block)
@locals = locals
end

def call_with_logging(request)
@logger.log(request) { HTTPI.post(request, @globals[:adapter]) }
def call_with_logging(connection)
ntlm_auth = handle_ntlm(connection) if @globals.include?(:ntlm)
@logger.log_response(connection.post(@globals[:endpoint]) { |request|
request.body = @locals[:body]
request.headers['Authorization'] = "NTLM #{auth.encode64}" if ntlm_auth
@logger.log_request(request)
})
end

def build_request(builder)
@locals[:soap_action] ||= soap_action
@globals[:endpoint] ||= endpoint
def handle_ntlm(connection)
ntlm_message = Net::NTLM::Message
response = connection.get(@globals[:endpoint]) do |request|
request.headers['Authorization'] = 'NTLM ' + ntlm_message::Type1.new.encode64
end
challenge = response.headers['www-authenticate'][/(?:NTLM|Negotiate) (.*)$/, 1]
message = ntlm_message::Type2.decode64(challenge)
message.response([:user, :password, :domain].zip(@globals[:ntlm]).to_h)
end

request = SOAPRequest.new(@globals).build(
def build_connection(builder)
@globals[:endpoint] ||= endpoint
@locals[:soap_action] ||= soap_action
@locals[:body] = builder.to_s
@connection = SOAPRequest.new(@globals).build(
:soap_action => soap_action,
:cookies => @locals[:cookies],
:headers => @locals[:headers]
)

request.url = endpoint
request.body = builder.to_s

if builder.multipart
request.gzip
request.headers["Content-Type"] = ["multipart/related",
"type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"",
"start=\"#{builder.multipart[:start]}\"",
"boundary=\"#{builder.multipart[:multipart_boundary]}\""].join("; ")
request.headers["MIME-Version"] = "1.0"
) do |connection|
if builder.multipart
connection.request :gzip
connection.headers["Content-Type"] = %W[multipart/related
type="#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}",
start="#{builder.multipart[:start]}",
boundary="#{builder.multipart[:multipart_boundary]}"].join("; ")
connection.headers["MIME-Version"] = "1.0"
end

connection.headers["Content-Length"] = @locals[:body].bytesize.to_s
end

# TODO: could HTTPI do this automatically in case the header
# was not specified manually? [dh, 2013-01-04]
request.headers["Content-Length"] = request.body.bytesize.to_s

request
end

def soap_action
Expand All @@ -138,8 +154,8 @@ def endpoint
end
end

def raise_expected_httpi_response!
raise Error, "Observers need to return an HTTPI::Response to mock " \
def raise_expected_faraday_response!
raise Error, "Observers need to return an Faraday::Response to mock " \
"the request or nil to execute the request."
end

Expand Down
161 changes: 97 additions & 64 deletions lib/savon/request.rb
Original file line number Diff line number Diff line change
@@ -1,80 +1,101 @@
# frozen_string_literal: true
require "httpi"
require "faraday"

module Savon
class HTTPRequest

def initialize(globals, http_request = nil)
def initialize(globals, connection = nil)
@globals = globals
@http_request = http_request || HTTPI::Request.new
end

def build
@http_request
@connection = connection || Faraday::Connection.new
end

private

def configure_proxy
@http_request.proxy = @globals[:proxy] if @globals.include? :proxy
def configure_proxy(connection)
connection.proxy = @globals[:proxy] if @globals.include? :proxy
end

def configure_timeouts
@http_request.open_timeout = @globals[:open_timeout] if @globals.include? :open_timeout
@http_request.read_timeout = @globals[:read_timeout] if @globals.include? :read_timeout
@http_request.write_timeout = @globals[:write_timeout] if @globals.include? :write_timeout
def configure_timeouts(connection)
connection.options.open_timeout = @globals[:open_timeout] if @globals.include? :open_timeout
connection.options.read_timeout = @globals[:read_timeout] if @globals.include? :read_timeout
connection.options.write_timeout = @globals[:write_timeout] if @globals.include? :write_timeout
end

def configure_ssl
@http_request.auth.ssl.ssl_version = @globals[:ssl_version] if @globals.include? :ssl_version
@http_request.auth.ssl.min_version = @globals[:ssl_min_version] if @globals.include? :ssl_min_version
@http_request.auth.ssl.max_version = @globals[:ssl_max_version] if @globals.include? :ssl_max_version

@http_request.auth.ssl.verify_mode = @globals[:ssl_verify_mode] if @globals.include? :ssl_verify_mode
@http_request.auth.ssl.ciphers = @globals[:ssl_ciphers] if @globals.include? :ssl_ciphers

@http_request.auth.ssl.cert_key_file = @globals[:ssl_cert_key_file] if @globals.include? :ssl_cert_key_file
@http_request.auth.ssl.cert_key = @globals[:ssl_cert_key] if @globals.include? :ssl_cert_key
@http_request.auth.ssl.cert_file = @globals[:ssl_cert_file] if @globals.include? :ssl_cert_file
@http_request.auth.ssl.cert = @globals[:ssl_cert] if @globals.include? :ssl_cert
@http_request.auth.ssl.ca_cert_file = @globals[:ssl_ca_cert_file] if @globals.include? :ssl_ca_cert_file
@http_request.auth.ssl.ca_cert_path = @globals[:ssl_ca_cert_path] if @globals.include? :ssl_ca_cert_path
@http_request.auth.ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include? :ssl_ca_cert
@http_request.auth.ssl.cert_store = @globals[:ssl_cert_store] if @globals.include? :ssl_cert_store

@http_request.auth.ssl.cert_key_password = @globals[:ssl_cert_key_password] if @globals.include? :ssl_cert_key_password
def configure_ssl(connection)
connection.ssl.verify = @globals[:ssl_verify] if @globals.include? :ssl_verify
connection.ssl.ca_file = @globals[:ssl_ca_cert_file] if @globals.include? :ssl_ca_cert_file
connection.ssl.verify_hostname = @globals[:verify_hostname] if @globals.include? :verify_hostname
connection.ssl.ca_path = @globals[:ssl_ca_cert_path] if @globals.include? :ssl_ca_cert_path
connection.ssl.verify_mode = @globals[:ssl_verify_mode] if @globals.include? :ssl_verify_mode
connection.ssl.cert_store = @globals[:ssl_cert_store] if @globals.include? :ssl_cert_store
connection.ssl.client_cert = @globals[:ssl_cert] if @globals.include? :ssl_cert
connection.ssl.client_key = @globals[:ssl_cert_key] if @globals.include? :ssl_cert_key
connection.ssl.certificate = @globals[:ssl_certificate] if @globals.include? :ssl_certificate
connection.ssl.private_key = @globals[:ssl_private_key] if @globals.include? :ssl_private_key
connection.ssl.verify_depth = @globals[:verify_depth] if @globals.include? :verify_depth
connection.ssl.version = @globals[:ssl_version] if @globals.include? :ssl_version
connection.ssl.min_version = @globals[:ssl_min_version] if @globals.include? :ssl_min_version
connection.ssl.max_version = @globals[:ssl_max_version] if @globals.include? :ssl_max_version

# No Faraday Equivalent out of box, see: https://lostisland.github.io/faraday/#/customization/ssl-options
# connection.ssl.cert_file = @globals[:ssl_cert_file] if @globals.include? :ssl_cert_file
# connection.ssl.cert_key_file = @globals[:ssl_cert_key_file] if @globals.include? :ssl_cert_key_file
# connection.ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include? :ssl_ca_cert
# connection.ssl.ciphers = @globals[:ssl_ciphers] if @globals.include? :ssl_ciphers
# connection.ssl.cert_key_password = @globals[:ssl_cert_key_password] if @globals.include? :ssl_cert_key_password

# deprecated and seems to break faraday via infinite stack recursion... Expected to use max_version and min_version
# see: https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/SSL/SSLContext.html#method-i-ssl_version-3D
end

def configure_auth
@http_request.auth.basic(*@globals[:basic_auth]) if @globals.include? :basic_auth
@http_request.auth.digest(*@globals[:digest_auth]) if @globals.include? :digest_auth
@http_request.auth.ntlm(*@globals[:ntlm]) if @globals.include? :ntlm
def configure_auth(connection)
connection.request :authorization, :basic, *@globals[:basic_auth] if @globals.include? :basic_auth
if @globals.include? :digest_auth
begin
require 'faraday/digestauth'
connection.request :digest, *@globals[:digest_auth]
rescue LoadError => e
raise LoadError, 'Using Digest Auth requests `faraday-digestauth`'
end
end
if @globals.include?(:ntlm)
begin
require 'rubyntlm'
require 'faraday/net_http_persistent'
connection.adapter :net_http_persistent, pool_size: 5
rescue LoadError => e
raise LoadError, 'Using NTLM Auth requires both `rubyntlm` and `faraday-net_http_persistent` to be installed.'
end
end
end

def configure_redirect_handling
if @globals.include? :follow_redirects
@http_request.follow_redirect = @globals[:follow_redirects]
def configure_redirect_handling(connection)
if @globals[:follow_redirects]
require 'faraday/follow_redirects'
connection.response :follow_redirects
end
end
end

class WSDLRequest < HTTPRequest

def build
configure_proxy
configure_timeouts
configure_headers
configure_ssl
configure_auth
configure_redirect_handling

@http_request
@connection.yield_self do |connection|
configure_proxy(connection)
configure_timeouts(connection)
configure_ssl(connection)
configure_auth(connection)
connection.adapter *@globals[:adapter] if !@globals[:adapter].nil?
connection.response :logger, nil, headers: @globals[:log_headers], level: @globals[:logger].level if @globals[:log]
configure_headers(connection)
end
@connection
end

private

def configure_headers
@http_request.headers = @globals[:headers] if @globals.include? :headers
def configure_headers(connection)
connection.headers = @globals[:headers] if @globals.include? :headers
end
end

Expand All @@ -85,29 +106,41 @@ class SOAPRequest < HTTPRequest
2 => "application/soap+xml;charset=%s"
}



def build(options = {})
configure_proxy
configure_timeouts
configure_headers options[:soap_action], options[:headers]
configure_cookies options[:cookies]
configure_ssl
configure_auth
configure_redirect_handling

@http_request
@connection.yield_self do |connection|
configure_proxy(connection)
configure_timeouts(connection)
configure_ssl(connection)
configure_auth(connection)
configure_headers(connection, options[:soap_action], options[:headers])
configure_cookies(connection, options[:cookies])
connection.adapter *@globals[:adapter] unless @globals[:adapter].nil?
connection.response :logger, nil, headers: @globals[:log_headers], level: @globals[:logger].level if @globals[:log]
configure_redirect_handling(connection)
yield(connection) if block_given?
end
@connection
end

private

def configure_cookies(cookies)
@http_request.set_cookies(cookies) if cookies
def configure_cookies(connection, cookies)
connection.headers['Cookie'] = cookies.map do |key, value|
if key == :_
value.join('; ')
else
"#{key}=#{value}"
end
end.join('; ') if cookies
end

def configure_headers(soap_action, headers)
@http_request.headers = @globals[:headers] if @globals.include? :headers
@http_request.headers.merge!(headers) if headers
@http_request.headers["SOAPAction"] ||= %{"#{soap_action}"} if soap_action
@http_request.headers["Content-Type"] ||= CONTENT_TYPE[@globals[:soap_version]] % @globals[:encoding]
def configure_headers(connection, soap_action, headers)
connection.headers = @globals[:headers] if @globals.include? :headers
connection.headers.merge!(headers) if headers
connection.headers["SOAPAction"] ||= %{"#{soap_action}"} if soap_action
connection.headers["Content-Type"] ||= CONTENT_TYPE[@globals[:soap_version]] % @globals[:encoding]
end
end
end
13 changes: 8 additions & 5 deletions lib/savon/request_logger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,24 @@ def log?
def log_headers?
@globals[:log_headers]
end

private

def log_request(request)
logger.info { "SOAP request: #{request.url}" }
return unless log?
logger.info { "SOAP request: #{request.path}" }
logger.info { headers_to_log(request.headers) } if log_headers?
logger.debug { body_to_log(request.body) }
end

def log_response(response)
logger.info { "SOAP response (status #{response.code})" }
return response unless log?
logger.info { "SOAP response (status #{response.status})" }
logger.debug { headers_to_log(response.headers) } if log_headers?
logger.debug { body_to_log(response.body) }
response
end

private


def headers_to_log(headers)
headers.map { |key, value| "#{key}: #{value}" }.join("\n")
end
Expand Down
Loading

0 comments on commit 6f2d6aa

Please sign in to comment.