Skip to content

Commit

Permalink
Faraday (#998)
Browse files Browse the repository at this point in the history
* replace httpi with faraday, pull in rubyntlm since it'll be needed for ntlm auth handshakes
  • Loading branch information
LukeIGS authored Jul 10, 2024
1 parent 4dd4abf commit 725855d
Show file tree
Hide file tree
Showing 23 changed files with 398 additions and 555 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# Savon changelog

## Unreleased
* Changes to utilize faraday instead of http
* BC BREAKING Cookies are handled differently now
* BC BREAKING Multiple pieces of functionality will rely on faraday libraries to be provided by the consuming codebase
* BC BREAKING Adapter overrides now utilize the faraday model
* BC BREAKING Multiple hard deprecations due to a lack of feature parity between Faraday and HTTPI
* Deprecates digest auth
* Deprecates ssl_cert_key_file auth, upgrade path is to read the key
in and provide it
* Deprecates encrypted ssl keys, upgrade path is to
decrypt the key and pass it to faraday in code
* Deprecates providing a ca cert, upgrade path is to provide a ca cert file
* deprecates overriding ssl ciphers, as faraday does not support this
* Add your PR changelog line here

## 2.15.1 (2024-07-08)
Expand Down
8 changes: 8 additions & 0 deletions lib/savon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ module Savon
UnknownOperationError = Class.new(Error)
InvalidResponseError = Class.new(Error)

class DeprecatedOptionError < Error
attr_accessor :option
def initialize(option)
@option = option
super("#{option} is deprecated as it is not supported in Faraday")
end
end

def self.client(globals = {}, &block)
Client.new(globals, &block)
end
Expand Down
6 changes: 3 additions & 3 deletions lib/savon/http_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Savon
class HTTPError < Error

def self.present?(http)
http.error?
!http.success?
end

def initialize(http)
Expand All @@ -14,13 +14,13 @@ def initialize(http)
attr_reader :http

def to_s
String.new("HTTP error (#{@http.code})").tap do |str_error|
String.new("HTTP error (#{@http.status})").tap do |str_error|
str_error << ": #{@http.body}" unless @http.body.empty?
end
end

def to_hash
{ :code => @http.code, :headers => @http.headers, :body => @http.body }
{ :code => @http.status, :headers => @http.headers, :body => @http.body }
end

end
Expand Down
8 changes: 4 additions & 4 deletions lib/savon/mock/expectation.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true
require "httpi"
require "faraday"

module Savon
class MockExpectation
Expand Down Expand Up @@ -41,8 +41,8 @@ def response!
unless @response
raise ExpectationError, "This expectation was not set up with a response."
end

HTTPI::Response.new(@response[:code], @response[:headers], @response[:body])
env = Faraday::Env.from(status: @response[:code], response_headers: @response[:headers], response_body: @response[:body])
Faraday::Response.new(env)
end

private
Expand Down Expand Up @@ -75,7 +75,7 @@ def equals_except_any(msg_expected, msg_real)
next if (expected_value == :any && msg_real.include?(key))
return false if expected_value != msg_real[key]
end
return true
true
end
end
end
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
30 changes: 23 additions & 7 deletions lib/savon/options.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# frozen_string_literal: true
require "logger"
require "httpi"

module Savon
class Options
Expand All @@ -10,6 +9,10 @@ def initialize(options = {})
assign options
end

def deprecate(option)
raise DeprecatedOptionError.new(option)
end

attr_reader :option_type

def [](option)
Expand Down Expand Up @@ -127,7 +130,7 @@ def namespace(namespace)
@options[:namespace] = namespace
end

# The namespace identifer.
# The namespace identifier.
def namespace_identifier(identifier)
@options[:namespace_identifier] = identifier
end
Expand Down Expand Up @@ -198,13 +201,11 @@ def raise_errors(raise_errors)

# Whether or not to log.
def log(log)
HTTPI.log = log
@options[:log] = log
end

# The logger to use. Defaults to a Savon::Logger instance.
def logger(logger)
HTTPI.logger = logger
@options[:logger] = logger
end

Expand Down Expand Up @@ -257,6 +258,7 @@ def ssl_verify_mode(verify_mode)

# Sets the cert key file to use.
def ssl_cert_key_file(file)
deprecate('ssl_cert_key_file')
@options[:ssl_cert_key_file] = file
end

Expand All @@ -267,11 +269,13 @@ def ssl_cert_key(key)

# Sets the cert key password to use.
def ssl_cert_key_password(password)
deprecate('ssl_cert_key_password')
@options[:ssl_cert_key_password] = password
end

# Sets the cert file to use.
def ssl_cert_file(file)
deprecate('ssl_cert_file')
@options[:ssl_cert_file] = file
end

Expand All @@ -287,10 +291,12 @@ def ssl_ca_cert_file(file)

# Sets the ca cert to use.
def ssl_ca_cert(cert)
deprecate('ssl_ca_cert')
@options[:ssl_ca_cert] = cert
end

def ssl_ciphers(ciphers)
deprecate('ssl_ciphers')
@options[:ssl_ciphers] = ciphers
end

Expand All @@ -311,6 +317,7 @@ def basic_auth(*credentials)

# HTTP digest auth credentials.
def digest_auth(*credentials)
deprecate('digest_auth')
@options[:digest_auth] = credentials.flatten
end

Expand Down Expand Up @@ -389,15 +396,16 @@ def initialize(options = {})
defaults = {
:advanced_typecasting => true,
:response_parser => :nokogiri,
:multipart => false
:multipart => false,
:body => false
}

super defaults.merge(options)
end

# The local SOAP header. Expected to be a Hash or respond to #to_s.
# Will be merged with the global SOAP header if both are Hashes.
# Otherwise the local option will be prefered.
# Otherwise the local option will be preferred.
def soap_header(header)
@options[:soap_header] = header
end
Expand Down Expand Up @@ -457,7 +465,11 @@ def soap_action(soap_action)
@options[:soap_action] = soap_action
end

# Cookies to be used for the next request.
# Cookies to be used for the next request
# @param [Hash] cookies cookies associated to nil will be appended as array cookies, if you need a cookie equal to
# and empty string, set it to ""
# @example cookies({accept: 'application/json', some-cookie: 'foo', "empty-cookie": "", HttpOnly: nil})
# # => "accept=application/json; some-cookie=foo; empty-cookie=; HttpOnly"
def cookies(cookies)
@options[:cookies] = cookies
end
Expand Down Expand Up @@ -485,5 +497,9 @@ def multipart(multipart)
def headers(headers)
@options[:headers] = headers
end

def body(body)
@options[:body] = body
end
end
end
Loading

0 comments on commit 725855d

Please sign in to comment.