savon 2.16.0 → 2.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Savon
3
4
  class SOAPFault < Error
4
-
5
5
  def self.present?(http, xml = nil)
6
6
  body = xml || http.body
7
7
  body = body.scrub('') unless body.valid_encoding?
8
8
  fault_node = body.include?("Fault>")
9
- soap1_fault = body.match(/faultcode\/?\>/) && body.match(/faultstring\/?\>/)
9
+ soap1_fault = body.match(%r{faultcode/?>}) && body.match(%r{faultstring/?>})
10
10
  soap2_fault = body.include?("Code>") && body.include?("Reason>")
11
11
 
12
12
  fault_node && (soap1_fault || soap2_fault)
@@ -45,6 +45,5 @@ module Savon
45
45
  "(#{code}) #{text}"
46
46
  end
47
47
  end
48
-
49
48
  end
50
49
  end
@@ -4,9 +4,9 @@ module Savon
4
4
  module StringUtils
5
5
  def self.snakecase(inputstring)
6
6
  str = inputstring.dup
7
- str.gsub! /::/, '/'
8
- str.gsub! /([A-Z]+)([A-Z][a-z])/, '\1_\2'
9
- str.gsub! /([a-z\d])([A-Z])/, '\1_\2'
7
+ str.gsub!(/::/, '/')
8
+ str.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
9
+ str.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
10
10
  str.tr! ".", "_"
11
11
  str.tr! "-", "_"
12
12
  str.downcase!
@@ -14,4 +14,3 @@ module Savon
14
14
  end
15
15
  end
16
16
  end
17
-
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "savon/transport/logging"
4
+ require "savon/transport/response"
5
+
6
+ module Savon
7
+ module Transport
8
+ # Faraday-backed HTTP transport for the opt-in Faraday path.
9
+ #
10
+ # Encapsulates everything Faraday-specific:
11
+ # * header assembly (SOAP + global + local + cookies + Content-Length)
12
+ # * request execution via the caller-configured Faraday::Connection
13
+ #
14
+ # Transport-level concerns (SSL, auth, proxy, timeouts, middleware) are
15
+ # the caller's responsibility via client.faraday before any call is made.
16
+ class Faraday
17
+ include Logging
18
+
19
+ # @param connection [Faraday::Connection] the memoized connection from client.faraday
20
+ # @param globals [Savon::GlobalOptions] the client-level options
21
+ def initialize(connection, globals)
22
+ @connection = connection
23
+ @globals = globals
24
+ end
25
+
26
+ # Assembles headers, executes the POST via the Faraday connection, and
27
+ # returns a Transport::Response. Logs the outbound request and inbound
28
+ # response when logging is enabled.
29
+ #
30
+ # @param url [String] the SOAP endpoint URL
31
+ # @param soap_headers [Hash] SOAP-level headers (Content-Type, SOAPAction, etc.)
32
+ # @param body [String] the serialized SOAP envelope
33
+ # @param locals [Savon::LocalOptions] per-request options
34
+ # @return [Transport::Response]
35
+ def post(url, soap_headers, body, locals)
36
+ headers = build_headers(soap_headers, body, locals)
37
+
38
+ log_request(url, headers, body) if log?
39
+
40
+ faraday_response = @connection.post(url, body, headers)
41
+ response = Response.from_faraday(faraday_response)
42
+
43
+ log_response(response) if log?
44
+
45
+ response
46
+ end
47
+
48
+ private
49
+
50
+ # Merges all header sources in precedence order:
51
+ # locals[:headers] > globals[:headers] > soap_headers
52
+ # Appends Cookie from locals[:cookies] and Content-Length from body.
53
+ def build_headers(soap_headers, body, locals)
54
+ headers = {}
55
+ headers.merge!(@globals[:headers]) if @globals.include?(:headers)
56
+ headers.merge!(locals[:headers]) if locals.include?(:headers)
57
+
58
+ # soap_headers are lowest priority
59
+ soap_headers.each do |k, v| headers[k] ||= v end
60
+
61
+ if locals[:cookies]&.any?
62
+ headers["Cookie"] = locals[:cookies].map(&:name_and_value).join(";")
63
+ end
64
+
65
+ headers["Content-Length"] = body.bytesize.to_s
66
+ headers
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpi"
4
+ require "savon/transport/logging"
5
+ require "savon/transport/response"
6
+
7
+ module Savon
8
+ module Transport
9
+ # HTTPI-backed HTTP transport for the default HTTP path.
10
+ #
11
+ # Encapsulates everything HTTPI-specific:
12
+ # * header assembly
13
+ # * proxy/SSL/auth/timeout configuration
14
+ # * request execution
15
+ class HTTPI
16
+ include Logging
17
+
18
+ # @param globals [Savon::GlobalOptions] the client-level options
19
+ def initialize(globals)
20
+ @globals = globals
21
+ end
22
+
23
+ # Assembles and executes a SOAP request, returning a Transport::Response.
24
+ # Logs the outbound request and inbound response when logging is enabled.
25
+ #
26
+ # @param url [String] the SOAP endpoint URL
27
+ # @param soap_headers [Hash] SOAP-level headers
28
+ # @param body [String] the serialized SOAP envelope
29
+ # @param locals [Savon::LocalOptions] per-request options
30
+ # @return [Transport::Response]
31
+ def post(url, soap_headers, body, locals)
32
+ http_request = to_httpi_request(url, soap_headers, body, locals)
33
+
34
+ log_request(http_request.url, http_request.headers, http_request.body) if log?
35
+
36
+ http_response = ::HTTPI.post(http_request, @globals[:adapter])
37
+ response = Response.from_httpi(http_response)
38
+
39
+ log_response(response) if log?
40
+
41
+ response
42
+ end
43
+
44
+ # Builds a fully-configured HTTPI::Request.
45
+ #
46
+ # @param url [String] the SOAP endpoint URL
47
+ # @param soap_headers [Hash] SOAP-level headers
48
+ # @param body [String] the serialized SOAP envelope
49
+ # @param locals [Savon::LocalOptions] per-request options
50
+ # @return [HTTPI::Request]
51
+ def to_httpi_request(url, soap_headers, body, locals)
52
+ headers = {}
53
+ headers.merge!(@globals[:headers]) if @globals.include?(:headers)
54
+ headers.merge!(locals[:headers]) if locals.include?(:headers)
55
+
56
+ # soap_headers are lowest priority
57
+ soap_headers.each do |k, v| headers[k] ||= v end
58
+
59
+ if locals[:cookies]&.any?
60
+ headers["Cookie"] = locals[:cookies].map(&:name_and_value).join(";")
61
+ end
62
+
63
+ headers["Content-Length"] = body.bytesize.to_s
64
+
65
+ http_request = ::HTTPI::Request.new
66
+ http_request.url = url
67
+ http_request.body = body
68
+ http_request.headers = headers
69
+ configure_http_request(http_request)
70
+ http_request
71
+ end
72
+
73
+ # Returns a configured HTTPI::Request for Wasabi's WSDL resolver.
74
+ # Applies global headers and all transport-level options and
75
+ # leaves the rest to Wasabi.
76
+ #
77
+ # @return [HTTPI::Request]
78
+ def wsdl_request
79
+ http_request = ::HTTPI::Request.new
80
+ http_request.headers = @globals[:headers].dup if @globals.include?(:headers)
81
+ configure_http_request(http_request)
82
+ http_request
83
+ end
84
+
85
+ private
86
+
87
+ def configure_http_request(http_request)
88
+ configure_proxy(http_request)
89
+ configure_timeouts(http_request)
90
+ configure_ssl(http_request)
91
+ configure_auth(http_request)
92
+ configure_redirect_handling(http_request)
93
+ end
94
+
95
+ def configure_proxy(http_request)
96
+ http_request.proxy = @globals[:proxy] if @globals.include?(:proxy)
97
+ end
98
+
99
+ def configure_timeouts(http_request)
100
+ http_request.open_timeout = @globals[:open_timeout] if @globals.include?(:open_timeout)
101
+ http_request.read_timeout = @globals[:read_timeout] if @globals.include?(:read_timeout)
102
+ http_request.write_timeout = @globals[:write_timeout] if @globals.include?(:write_timeout)
103
+ end
104
+
105
+ # Configures SSL on the HTTPI::Request from all ssl globals.
106
+ # SSL option reference: https://github.com/savonrb/httpi/blob/main/lib/httpi/auth/ssl.rb
107
+ def configure_ssl(http_request)
108
+ ssl = http_request.auth.ssl
109
+ ssl.ssl_version = @globals[:ssl_version] if @globals.include?(:ssl_version)
110
+ ssl.min_version = @globals[:ssl_min_version] if @globals.include?(:ssl_min_version)
111
+ ssl.max_version = @globals[:ssl_max_version] if @globals.include?(:ssl_max_version)
112
+ ssl.verify_mode = @globals[:ssl_verify_mode] if @globals.include?(:ssl_verify_mode)
113
+ ssl.ciphers = @globals[:ssl_ciphers] if @globals.include?(:ssl_ciphers)
114
+ ssl.cert_key_file = @globals[:ssl_cert_key_file] if @globals.include?(:ssl_cert_key_file)
115
+ ssl.cert_key = @globals[:ssl_cert_key] if @globals.include?(:ssl_cert_key)
116
+ ssl.cert_file = @globals[:ssl_cert_file] if @globals.include?(:ssl_cert_file)
117
+ ssl.cert = @globals[:ssl_cert] if @globals.include?(:ssl_cert)
118
+ ssl.ca_cert_file = @globals[:ssl_ca_cert_file] if @globals.include?(:ssl_ca_cert_file)
119
+ ssl.ca_cert_path = @globals[:ssl_ca_cert_path] if @globals.include?(:ssl_ca_cert_path)
120
+ ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include?(:ssl_ca_cert)
121
+ ssl.cert_store = @globals[:ssl_cert_store] if @globals.include?(:ssl_cert_store)
122
+ ssl.cert_key_password = @globals[:ssl_cert_key_password] if @globals.include?(:ssl_cert_key_password)
123
+ end
124
+
125
+ def configure_auth(http_request)
126
+ http_request.auth.basic(*@globals[:basic_auth]) if @globals.include?(:basic_auth)
127
+ http_request.auth.digest(*@globals[:digest_auth]) if @globals.include?(:digest_auth)
128
+ http_request.auth.ntlm(*@globals[:ntlm]) if @globals.include?(:ntlm)
129
+ end
130
+
131
+ def configure_redirect_handling(http_request)
132
+ http_request.follow_redirect = @globals[:follow_redirects] if @globals.include?(:follow_redirects)
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "savon/log_message"
4
+
5
+ module Savon
6
+ module Transport
7
+ # Shared logging behaviour for HTTP transports.
8
+ #
9
+ # Expects the including class to expose @globals (Savon::GlobalOptions)
10
+ # so that log level, filters, and pretty-print settings can be accessed.
11
+ #
12
+ # log_request and log_response are intentionally private so that each
13
+ # transport drives them from its own post method.
14
+ module Logging
15
+ private
16
+
17
+ def log?
18
+ @globals[:log]
19
+ end
20
+
21
+ def log_headers?
22
+ @globals[:log_headers]
23
+ end
24
+
25
+ def logger
26
+ @globals[:logger]
27
+ end
28
+
29
+ # Logs the outbound request at INFO (URL and optional headers)
30
+ # and DEBUG (filtered/pretty-printed body).
31
+ #
32
+ # @param url [String] the SOAP endpoint URL
33
+ # @param headers [Hash] request headers
34
+ # @param body [String] the serialized SOAP envelope
35
+ def log_request(url, headers, body)
36
+ logger.info do "SOAP request: #{url}" end
37
+ logger.info { headers_to_log(headers) } if log_headers?
38
+ logger.debug { body_to_log(body) }
39
+ end
40
+
41
+ # Logs the inbound response at INFO (status line)
42
+ # and DEBUG (headers and filtered/pretty-printed body).
43
+ #
44
+ # @param response [Transport::Response]
45
+ def log_response(response)
46
+ logger.info do "SOAP response (status #{response.code})" end
47
+ logger.debug { headers_to_log(response.headers) } if log_headers?
48
+ logger.debug { body_to_log(response.body) }
49
+ end
50
+
51
+ def headers_to_log(headers)
52
+ headers.map { |key, value| "#{key}: #{value}" }.join("\n")
53
+ end
54
+
55
+ def body_to_log(body)
56
+ LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Savon
4
+ module Transport
5
+ # Transport-agnostic HTTP response value object.
6
+ #
7
+ # Every transport produces a Transport::Response so that higher-level code
8
+ # never depends on transport-specific code. Immutable once constructed.
9
+ class Response
10
+ # Creates a Transport::Response from an HTTPI::Response.
11
+ def self.from_httpi(httpi_response)
12
+ new(httpi_response.code, httpi_response.headers, httpi_response.body)
13
+ end
14
+
15
+ # Creates a Transport::Response from a Faraday::Response.
16
+ def self.from_faraday(faraday_response)
17
+ new(faraday_response.status, faraday_response.headers.to_h, faraday_response.body)
18
+ end
19
+
20
+ # @param code [Integer] HTTP status code
21
+ # @param headers [Hash] response headers
22
+ # @param body [String] response body
23
+ def initialize(code, headers, body)
24
+ @code = code
25
+ @headers = headers
26
+ @body = body
27
+ end
28
+
29
+ # Returns the HTTP status code.
30
+ attr_reader :code
31
+
32
+ # Returns the response headers hash.
33
+ attr_reader :headers
34
+
35
+ # Returns the response body string.
36
+ attr_reader :body
37
+
38
+ # Returns true when the HTTP status code indicates an error (>= 300).
39
+ def error?
40
+ @code >= 300
41
+ end
42
+ end
43
+ end
44
+ end
data/lib/savon/version.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Savon
3
- VERSION = '2.16.0'
4
+ VERSION = '2.17.0'
4
5
  end
data/lib/savon.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
- module Savon
3
2
 
3
+ module Savon
4
4
  Error = Class.new(RuntimeError)
5
5
  InitializationError = Class.new(Error)
6
6
  UnknownOptionError = Class.new(Error)
@@ -16,11 +16,10 @@ module Savon
16
16
  end
17
17
 
18
18
  def self.notify_observers(operation_name, builder, globals, locals)
19
- observers.inject(nil) do |response, observer|
19
+ observers.inject(nil) do |_response, observer|
20
20
  observer.notify(operation_name, builder, globals, locals)
21
21
  end
22
22
  end
23
-
24
23
  end
25
24
 
26
25
  require "savon/version"