savon 2.17.4 → 3.0.0.rc1

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,186 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "uri"
4
-
5
- module Savon
6
- # Formats the replacement Faraday setup for one HTTPI-only option.
7
- class FaradayMigrationHint
8
- VALUE_AWARE_OPTIONS = %i[
9
- proxy
10
- open_timeout
11
- read_timeout
12
- write_timeout
13
- ssl_version
14
- ssl_min_version
15
- ssl_max_version
16
- ssl_verify_mode
17
- ssl_cert_key_file
18
- ssl_cert_file
19
- ssl_ca_cert_file
20
- ssl_ciphers
21
- ssl_ca_cert_path
22
- adapter
23
- ].freeze
24
-
25
- STATIC_HINTS = {
26
- ssl_cert_key: "client.faraday.ssl.client_key = key",
27
- ssl_cert_key_password: [
28
- "key = OpenSSL::PKey.read(File.read(key_path), password)",
29
- "client.faraday.ssl.client_key = key"
30
- ],
31
- ssl_cert: "client.faraday.ssl.client_cert = cert",
32
- ssl_ca_cert: [
33
- "store = OpenSSL::X509::Store.new",
34
- "store.set_default_paths",
35
- "store.add_cert(cert)",
36
- "client.faraday.ssl.cert_store = store"
37
- ],
38
- ssl_cert_store: "client.faraday.ssl.cert_store = store",
39
- basic_auth: "client.faraday.request :authorization, :basic, user, pass",
40
- digest_auth: [
41
- "gem 'faraday-digestauth'",
42
- "require 'faraday/digestauth'",
43
- "client.faraday.request :digest, user, pass"
44
- ],
45
- ntlm: [
46
- "gem 'faraday-ntlm_auth'",
47
- "require 'faraday/ntlm_auth'",
48
- "client.faraday.adapter :net_http_persistent",
49
- "client.faraday.request :ntlm_auth, auth: [user, pass, domain]"
50
- ],
51
- follow_redirects: [
52
- "gem 'faraday-follow_redirects'",
53
- "require 'faraday/follow_redirects'",
54
- "client.faraday.response :follow_redirects"
55
- ]
56
- }.freeze
57
-
58
- OPTIONS = (VALUE_AWARE_OPTIONS + STATIC_HINTS.keys).freeze
59
-
60
- def initialize(option, value)
61
- @option = option
62
- @value = value
63
- end
64
-
65
- def message
66
- hint = hint_lines
67
- return " #{option} - Use: #{hint}" unless hint.is_a?(Array)
68
-
69
- " #{option} - Use:\n#{hint.map { |line| " #{line}" }.join("\n")}"
70
- end
71
-
72
- private
73
-
74
- attr_reader :option, :value
75
-
76
- def hint_lines
77
- case option
78
- when :proxy
79
- "client.faraday.proxy = #{redacted_proxy_value}"
80
- when :open_timeout, :read_timeout, :write_timeout
81
- "client.faraday.options.#{option} = #{value.inspect}"
82
- when :ssl_version, :ssl_min_version, :ssl_max_version
83
- ssl_version_hint
84
- when :ssl_verify_mode
85
- ssl_verify_mode_hint
86
- when :ssl_cert_key_file
87
- ssl_cert_key_file_hint
88
- when :ssl_cert_file
89
- ssl_cert_file_hint
90
- when :ssl_ca_cert_file
91
- "client.faraday.ssl.ca_file = #{value.inspect}"
92
- when :ssl_ciphers
93
- "client.faraday.ssl.ciphers = #{value.inspect}"
94
- when :ssl_ca_cert_path
95
- "client.faraday.ssl.ca_path = #{value.inspect}"
96
- when :adapter
97
- adapter_hint
98
- else
99
- STATIC_HINTS.fetch(option)
100
- end
101
- end
102
-
103
- def redacted_proxy_value
104
- uri = URI.parse(value.to_s)
105
- return value.inspect unless uri.absolute? && uri.host
106
-
107
- redacted = uri.dup
108
- redacted.userinfo = "..." if redacted.userinfo
109
- redacted.path = "" if redacted.respond_to?(:path=)
110
- redacted.query = nil if redacted.respond_to?(:query=)
111
- redacted.fragment = nil if redacted.respond_to?(:fragment=)
112
- redacted.to_s.inspect
113
- rescue URI::InvalidURIError
114
- '"[redacted proxy URL]"'
115
- end
116
-
117
- def ssl_version_hint
118
- faraday_option = {
119
- ssl_version: "version",
120
- ssl_min_version: "min_version",
121
- ssl_max_version: "max_version"
122
- }.fetch(option)
123
-
124
- "client.faraday.ssl.#{faraday_option} = #{value.inspect}"
125
- end
126
-
127
- def ssl_cert_key_file_hint
128
- [
129
- "key = OpenSSL::PKey.read(File.read(#{value.inspect}))",
130
- "client.faraday.ssl.client_key = key"
131
- ]
132
- end
133
-
134
- def ssl_cert_file_hint
135
- [
136
- "cert = OpenSSL::X509::Certificate.new(File.read(#{value.inspect}))",
137
- "client.faraday.ssl.client_cert = cert"
138
- ]
139
- end
140
-
141
- def adapter_hint
142
- case value
143
- when :net_http
144
- "client.faraday.adapter :net_http"
145
- when :httpclient
146
- [
147
- "gem 'faraday-httpclient'",
148
- "require 'faraday/httpclient'",
149
- "client.faraday.adapter :httpclient"
150
- ]
151
- when :excon
152
- [
153
- "gem 'faraday-excon'",
154
- "require 'faraday/excon'",
155
- "client.faraday.adapter :excon"
156
- ]
157
- when :net_http_persistent
158
- [
159
- "gem 'faraday-net_http_persistent'",
160
- "require 'faraday/net_http_persistent'",
161
- "client.faraday.adapter :net_http_persistent"
162
- ]
163
- else
164
- [
165
- "choose and install a Faraday adapter matching #{value.inspect}",
166
- "client.faraday.adapter :net_http"
167
- ]
168
- end
169
- end
170
-
171
- def ssl_verify_mode_hint
172
- case value
173
- when :none
174
- "client.faraday.ssl.verify_mode = OpenSSL::SSL::VERIFY_NONE"
175
- when :peer
176
- "client.faraday.ssl.verify_mode = OpenSSL::SSL::VERIFY_PEER"
177
- when :fail_if_no_peer_cert
178
- "client.faraday.ssl.verify_mode = OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT"
179
- when :client_once
180
- "client.faraday.ssl.verify_mode = OpenSSL::SSL::VERIFY_CLIENT_ONCE"
181
- else
182
- "client.faraday.ssl.verify_mode = OpenSSL::SSL::VERIFY_PEER or OpenSSL::SSL::VERIFY_NONE"
183
- end
184
- end
185
- end
186
- end
@@ -1,101 +0,0 @@
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)
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, locals)
37
-
38
- log_request(url, headers, body) if log?
39
-
40
- faraday_response = @connection.post(url, body, headers)
41
- response = normalize_response(faraday_response)
42
-
43
- log_response(response) if log?
44
- response
45
- end
46
-
47
- # Normalizes a native Faraday::Response into a Transport::Response.
48
- #
49
- # @param faraday_response [Faraday::Response]
50
- # @return [Transport::Response]
51
- def normalize_response(faraday_response)
52
- Response.from_faraday(faraday_response)
53
- end
54
-
55
- # Parses Set-Cookie headers into a Hash of name => value. Accepts both
56
- # the Array and String form. Attributes after the first ';' are discarded.
57
- def self.parse_cookies(headers)
58
- raw = headers["set-cookie"] || headers["Set-Cookie"]
59
- return {} unless raw
60
-
61
- raw_array = raw.is_a?(Array) ? raw : raw.split(/,\s*/)
62
- raw_array.each_with_object({}) do |cookie_str, hash|
63
- name_value = cookie_str.split(";", 2).first.to_s.strip
64
- name, value = name_value.split("=", 2)
65
- hash[name] = value if name && !name.empty?
66
- end
67
- end
68
-
69
- private
70
-
71
- # Merges all header sources in precedence order:
72
- # locals[:headers] > globals[:headers] > soap_headers
73
- # Appends Cookie from locals[:cookies].
74
- def build_headers(soap_headers, locals)
75
- headers = {}
76
- headers.merge!(@globals[:headers]) if @globals.include?(:headers)
77
- headers.merge!(locals[:headers]) if locals.include?(:headers)
78
-
79
- # soap_headers are lowest priority
80
- soap_headers.each do |k, v| headers[k] ||= v end
81
-
82
- cookie_header = format_cookies(locals[:cookies])
83
- headers["Cookie"] = cookie_header if cookie_header
84
-
85
- headers
86
- end
87
-
88
- # Builds the Cookie header from a given value.
89
- # Accepts:
90
- # * String - passed through verbatim
91
- # * Hash - formatted as "name=value; name=value" (browser style)
92
- # Returns nil when no cookies were supplied.
93
- def format_cookies(cookies)
94
- return nil if cookies.nil?
95
- return cookies if cookies.is_a?(String)
96
-
97
- cookies.map { |name, value| "#{name}=#{value}" }.join("; ")
98
- end
99
- end
100
- end
101
- end
@@ -1,138 +0,0 @@
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
- httpi_response = ::HTTPI.post(http_request, @globals[:adapter])
37
- response = normalize_response(httpi_response)
38
-
39
- log_response(response) if log?
40
- response
41
- end
42
-
43
- # Normalizes a native HTTPI::Response into a Transport::Response.
44
- #
45
- # @param httpi_response [HTTPI::Response]
46
- # @return [Transport::Response]
47
- def normalize_response(httpi_response)
48
- Response.from_httpi(httpi_response)
49
- end
50
-
51
- # Builds a fully-configured HTTPI::Request.
52
- #
53
- # @param url [String] the SOAP endpoint URL
54
- # @param soap_headers [Hash] SOAP-level headers
55
- # @param body [String] the serialized SOAP envelope
56
- # @param locals [Savon::LocalOptions] per-request options
57
- # @return [HTTPI::Request]
58
- def to_httpi_request(url, soap_headers, body, locals)
59
- headers = {}
60
- headers.merge!(@globals[:headers]) if @globals.include?(:headers)
61
- headers.merge!(locals[:headers]) if locals.include?(:headers)
62
-
63
- # soap_headers are lowest priority
64
- soap_headers.each do |k, v| headers[k] ||= v end
65
-
66
- http_request = ::HTTPI::Request.new
67
- http_request.url = url
68
- http_request.body = body
69
- http_request.headers = headers
70
- http_request.set_cookies(locals[:cookies]) if locals[:cookies]
71
- configure_http_request(http_request)
72
- http_request
73
- end
74
-
75
- # Returns a configured HTTPI::Request for Wasabi's WSDL resolver.
76
- # Applies global headers and all transport-level options and
77
- # leaves the rest to Wasabi.
78
- #
79
- # @return [HTTPI::Request]
80
- def wsdl_request
81
- http_request = ::HTTPI::Request.new
82
- http_request.headers = @globals[:headers].dup if @globals.include?(:headers)
83
- configure_http_request(http_request)
84
- http_request
85
- end
86
-
87
- private
88
-
89
- def configure_http_request(http_request)
90
- configure_proxy(http_request)
91
- configure_timeouts(http_request)
92
- configure_ssl(http_request)
93
- configure_auth(http_request)
94
- configure_redirect_handling(http_request)
95
- end
96
-
97
- def configure_proxy(http_request)
98
- http_request.proxy = @globals[:proxy] if @globals.include?(:proxy)
99
- end
100
-
101
- def configure_timeouts(http_request)
102
- http_request.open_timeout = @globals[:open_timeout] if @globals.include?(:open_timeout)
103
- http_request.read_timeout = @globals[:read_timeout] if @globals.include?(:read_timeout)
104
- http_request.write_timeout = @globals[:write_timeout] if @globals.include?(:write_timeout)
105
- end
106
-
107
- # Configures SSL on the HTTPI::Request from all ssl globals.
108
- # SSL option reference: https://github.com/savonrb/httpi/blob/main/lib/httpi/auth/ssl.rb
109
- def configure_ssl(http_request)
110
- ssl = http_request.auth.ssl
111
- ssl.ssl_version = @globals[:ssl_version] if @globals.include?(:ssl_version)
112
- ssl.min_version = @globals[:ssl_min_version] if @globals.include?(:ssl_min_version)
113
- ssl.max_version = @globals[:ssl_max_version] if @globals.include?(:ssl_max_version)
114
- ssl.verify_mode = @globals[:ssl_verify_mode] if @globals.include?(:ssl_verify_mode)
115
- ssl.ciphers = @globals[:ssl_ciphers] if @globals.include?(:ssl_ciphers)
116
- ssl.cert_key_file = @globals[:ssl_cert_key_file] if @globals.include?(:ssl_cert_key_file)
117
- ssl.cert_key = @globals[:ssl_cert_key] if @globals.include?(:ssl_cert_key)
118
- ssl.cert_file = @globals[:ssl_cert_file] if @globals.include?(:ssl_cert_file)
119
- ssl.cert = @globals[:ssl_cert] if @globals.include?(:ssl_cert)
120
- ssl.ca_cert_file = @globals[:ssl_ca_cert_file] if @globals.include?(:ssl_ca_cert_file)
121
- ssl.ca_cert_path = @globals[:ssl_ca_cert_path] if @globals.include?(:ssl_ca_cert_path)
122
- ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include?(:ssl_ca_cert)
123
- ssl.cert_store = @globals[:ssl_cert_store] if @globals.include?(:ssl_cert_store)
124
- ssl.cert_key_password = @globals[:ssl_cert_key_password] if @globals.include?(:ssl_cert_key_password)
125
- end
126
-
127
- def configure_auth(http_request)
128
- http_request.auth.basic(*@globals[:basic_auth]) if @globals.include?(:basic_auth)
129
- http_request.auth.digest(*@globals[:digest_auth]) if @globals.include?(:digest_auth)
130
- http_request.auth.ntlm(*@globals[:ntlm]) if @globals.include?(:ntlm)
131
- end
132
-
133
- def configure_redirect_handling(http_request)
134
- http_request.follow_redirect = @globals[:follow_redirects] if @globals.include?(:follow_redirects)
135
- end
136
- end
137
- end
138
- end
@@ -1,60 +0,0 @@
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
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "httpi"
4
- require "savon/transport/faraday"
5
-
6
- module Savon
7
- module Transport
8
- # Transport-agnostic HTTP response value object.
9
- #
10
- # Every transport produces a Transport::Response so that higher-level code
11
- # never depends on transport-specific code. Immutable once constructed.
12
- #
13
- # The shape of #cookies is transport-specific: HTTPI responses expose an
14
- # Array of HTTPI::Cookie, while Faraday responses expose a plain Hash so
15
- # Faraday users do not depend on HTTPI types.
16
- class Response
17
- # Builds a Response from an HTTPI::Response.
18
- #
19
- # @param httpi_response [HTTPI::Response]
20
- # @return [Transport::Response]
21
- def self.from_httpi(httpi_response)
22
- new(
23
- httpi_response.code,
24
- httpi_response.headers,
25
- httpi_response.body,
26
- cookies: ::HTTPI::Cookie.list_from_headers(httpi_response.headers)
27
- )
28
- end
29
-
30
- # Builds a Response from a Faraday::Response.
31
- #
32
- # @param faraday_response [Faraday::Response]
33
- # @return [Transport::Response]
34
- def self.from_faraday(faraday_response)
35
- new(
36
- faraday_response.status,
37
- faraday_response.headers.to_h,
38
- faraday_response.body,
39
- cookies: Savon::Transport::Faraday.parse_cookies(faraday_response.headers)
40
- )
41
- end
42
-
43
- # @param code [Integer] HTTP status code
44
- # @param headers [Hash] response headers
45
- # @param body [String] response body
46
- # @param cookies [Object] parsed cookies in a transport-specific shape
47
- def initialize(code, headers, body, cookies: nil)
48
- @code = code
49
- @headers = headers
50
- @body = body
51
- @cookies = cookies
52
- end
53
-
54
- # Returns the HTTP status code.
55
- attr_reader :code
56
-
57
- # Returns the response headers hash.
58
- attr_reader :headers
59
-
60
- # Returns the response body string.
61
- attr_reader :body
62
-
63
- # Returns the parsed cookies in a transport-specific shape.
64
- # See class-level docs.
65
- attr_reader :cookies
66
-
67
- # Returns true when the HTTP status code indicates an error (>= 300).
68
- def error?
69
- @code >= 300
70
- end
71
- end
72
- end
73
- end