savon 2.16.0 → 2.17.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +110 -7
- data/README.md +54 -48
- data/Rakefile +6 -2
- data/lib/savon/addressing.rb +60 -0
- data/lib/savon/block_interface.rb +3 -4
- data/lib/savon/builder.rb +54 -23
- data/lib/savon/client.rb +44 -13
- data/lib/savon/effective_options.rb +124 -0
- data/lib/savon/faraday_migration_hint.rb +186 -0
- data/lib/savon/header.rb +87 -40
- data/lib/savon/http_error.rb +8 -8
- data/lib/savon/log_message.rb +2 -3
- data/lib/savon/message.rb +6 -7
- data/lib/savon/mock/expectation.rb +37 -23
- data/lib/savon/mock/spec_helper.rb +7 -11
- data/lib/savon/mock.rb +1 -0
- data/lib/savon/model.rb +25 -25
- data/lib/savon/operation.rb +110 -74
- data/lib/savon/options.rb +274 -157
- data/lib/savon/qualified_message.rb +2 -1
- data/lib/savon/response.rb +40 -31
- data/lib/savon/soap_fault.rb +2 -3
- data/lib/savon/string_utils.rb +3 -4
- data/lib/savon/transport/faraday.rb +101 -0
- data/lib/savon/transport/httpi.rb +138 -0
- data/lib/savon/transport/logging.rb +60 -0
- data/lib/savon/transport/response.rb +73 -0
- data/lib/savon/version.rb +2 -1
- data/lib/savon.rb +2 -3
- metadata +85 -83
- data/lib/savon/request.rb +0 -113
- data/lib/savon/request_logger.rb +0 -54
data/lib/savon/client.rb
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "savon/operation"
|
|
3
|
-
require "savon/
|
|
4
|
+
require "savon/transport/httpi"
|
|
5
|
+
require "savon/transport/faraday"
|
|
4
6
|
require "savon/options"
|
|
5
7
|
require "savon/block_interface"
|
|
6
8
|
require "wasabi"
|
|
7
9
|
|
|
8
10
|
module Savon
|
|
11
|
+
# The main entry point for Savon.
|
|
12
|
+
#
|
|
13
|
+
# Holds global configuration, owns the WSDL document, and dispatches
|
|
14
|
+
# named operations. A single Client instance is typically shared across
|
|
15
|
+
# multiple calls to the same service.
|
|
9
16
|
class Client
|
|
10
|
-
|
|
11
17
|
def initialize(globals = {}, &block)
|
|
12
|
-
unless globals.
|
|
18
|
+
unless globals.is_a? Hash
|
|
13
19
|
raise_version1_initialize_error! globals
|
|
14
20
|
end
|
|
15
21
|
|
|
16
22
|
set_globals(globals, block)
|
|
23
|
+
@globals.validate_transport!
|
|
17
24
|
|
|
18
25
|
unless wsdl_or_endpoint_and_namespace_specified?
|
|
19
26
|
raise_initialization_error!
|
|
@@ -24,13 +31,25 @@ module Savon
|
|
|
24
31
|
|
|
25
32
|
attr_reader :globals, :wsdl
|
|
26
33
|
|
|
34
|
+
# Returns the memoized Faraday::Connection for this client.
|
|
35
|
+
# Callers use this to configure middleware, SSL, auth, timeouts, and any
|
|
36
|
+
# other transport-level concern before making calls.
|
|
37
|
+
# Raises ArgumentError if transport is not :faraday.
|
|
38
|
+
def faraday
|
|
39
|
+
unless @globals[:transport] == :faraday
|
|
40
|
+
raise ArgumentError, "client.faraday is only available when transport: :faraday is set"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@faraday ||= ::Faraday.new
|
|
44
|
+
end
|
|
45
|
+
|
|
27
46
|
def operations
|
|
28
47
|
raise_missing_wsdl_error! unless @wsdl.document?
|
|
29
48
|
@wsdl.soap_actions
|
|
30
49
|
end
|
|
31
50
|
|
|
32
51
|
def operation(operation_name)
|
|
33
|
-
Operation.create(operation_name, @wsdl, @globals)
|
|
52
|
+
Operation.create(operation_name, @wsdl, @globals, build_transport)
|
|
34
53
|
end
|
|
35
54
|
|
|
36
55
|
def call(operation_name, locals = {}, &block)
|
|
@@ -48,6 +67,15 @@ module Savon
|
|
|
48
67
|
|
|
49
68
|
private
|
|
50
69
|
|
|
70
|
+
# Builds the transport for a single operation.
|
|
71
|
+
def build_transport
|
|
72
|
+
if @globals[:transport] == :faraday
|
|
73
|
+
Transport::Faraday.new(faraday, @globals)
|
|
74
|
+
else
|
|
75
|
+
Transport::HTTPI.new(@globals)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
51
79
|
def set_globals(globals, block)
|
|
52
80
|
globals = GlobalOptions.new(globals)
|
|
53
81
|
BlockInterface.new(globals).evaluate(block) if block
|
|
@@ -58,12 +86,16 @@ module Savon
|
|
|
58
86
|
def build_wsdl_document
|
|
59
87
|
@wsdl = Wasabi::Document.new
|
|
60
88
|
|
|
61
|
-
@wsdl.document
|
|
62
|
-
@wsdl.endpoint
|
|
63
|
-
@wsdl.namespace
|
|
64
|
-
@wsdl.adapter = @globals[:adapter] if @globals.include? :adapter
|
|
89
|
+
@wsdl.document = @globals[:wsdl] if @globals.include? :wsdl
|
|
90
|
+
@wsdl.endpoint = @globals[:endpoint] if @globals.include? :endpoint
|
|
91
|
+
@wsdl.namespace = @globals[:namespace] if @globals.include? :namespace
|
|
65
92
|
|
|
66
|
-
@
|
|
93
|
+
if @globals[:transport] == :faraday
|
|
94
|
+
@wsdl.request = faraday
|
|
95
|
+
else
|
|
96
|
+
@wsdl.adapter = @globals[:adapter] if @globals.include? :adapter
|
|
97
|
+
@wsdl.request = Transport::HTTPI.new(@globals).wsdl_request
|
|
98
|
+
end
|
|
67
99
|
end
|
|
68
100
|
|
|
69
101
|
def wsdl_or_endpoint_and_namespace_specified?
|
|
@@ -72,9 +104,9 @@ module Savon
|
|
|
72
104
|
|
|
73
105
|
def raise_version1_initialize_error!(object)
|
|
74
106
|
raise InitializationError,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
107
|
+
"Some code tries to initialize Savon with the #{object.inspect} (#{object.class}) \n" \
|
|
108
|
+
"Savon 2 expects a Hash of options for creating a new client and executing requests.\n" \
|
|
109
|
+
"Please read the updated documentation for version 2: http://savonrb.com/version2.html"
|
|
78
110
|
end
|
|
79
111
|
|
|
80
112
|
def raise_initialization_error!
|
|
@@ -88,6 +120,5 @@ module Savon
|
|
|
88
120
|
def raise_missing_wsdl_error!
|
|
89
121
|
raise "Unable to inspect the service without a WSDL document."
|
|
90
122
|
end
|
|
91
|
-
|
|
92
123
|
end
|
|
93
124
|
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gyoku"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Savon
|
|
7
|
+
# Resolves the effective value of options whose answer combines more than
|
|
8
|
+
# one source: the per-request options, the client options, the WSDL
|
|
9
|
+
# document, and built-in defaults.
|
|
10
|
+
#
|
|
11
|
+
# {Savon::GlobalOptions} and {Savon::LocalOptions} store what the caller
|
|
12
|
+
# said. EffectiveOptions answers what the request uses. It owns the
|
|
13
|
+
# precedence rules and exposes one reader per resolvable option, so every
|
|
14
|
+
# consumer of an option resolves it identically. Resolution is a pure read.
|
|
15
|
+
# It never mutates the options or the WSDL document.
|
|
16
|
+
class EffectiveOptions
|
|
17
|
+
# @param operation_name [Symbol] the SOAP operation being called
|
|
18
|
+
# @param wsdl [Wasabi::Document] the parsed WSDL, or an empty document
|
|
19
|
+
# when the client was configured without one
|
|
20
|
+
# @param globals [Savon::GlobalOptions] the client-level options
|
|
21
|
+
# @param locals [Savon::LocalOptions] the per-request options
|
|
22
|
+
def initialize(operation_name, wsdl, globals, locals)
|
|
23
|
+
@operation_name = operation_name
|
|
24
|
+
@wsdl = wsdl
|
|
25
|
+
@globals = globals
|
|
26
|
+
@locals = locals
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Resolves the SOAPAction of the request (SOAP 1.1 §6.1.1).
|
|
30
|
+
#
|
|
31
|
+
# An explicit local +:soap_action+ wins. A local +false+ or +nil+ disables
|
|
32
|
+
# the action, so no SOAPAction HTTP header is sent and an enabled
|
|
33
|
+
# +wsa:Action+ header stays empty. Without a local value the WSDL provides
|
|
34
|
+
# the soapAction of the operation. Without a WSDL document the operation
|
|
35
|
+
# name is converted to an XML tag as a best-effort default.
|
|
36
|
+
#
|
|
37
|
+
# @return [String, nil] the action, or +nil+ when explicitly disabled
|
|
38
|
+
def soap_action
|
|
39
|
+
return if @locals.include?(:soap_action) && !@locals[:soap_action]
|
|
40
|
+
|
|
41
|
+
@locals[:soap_action] ||
|
|
42
|
+
(@wsdl.document? && @wsdl.soap_action(@operation_name.to_sym)) ||
|
|
43
|
+
Gyoku.xml_tag(@operation_name, key_converter: @globals[:convert_request_keys_to])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Resolves the endpoint URL the request is sent to.
|
|
47
|
+
#
|
|
48
|
+
# A global +:endpoint+ wins over the service address of the WSDL. The
|
|
49
|
+
# global +:host+ option replaces host and port of the WSDL address and
|
|
50
|
+
# keeps scheme, path and query. The override is applied to a copy. The
|
|
51
|
+
# WSDL document keeps its parsed address.
|
|
52
|
+
#
|
|
53
|
+
# @return [URI, String, nil] the endpoint as provided by the winning source
|
|
54
|
+
def endpoint
|
|
55
|
+
return @globals[:endpoint] if @globals[:endpoint]
|
|
56
|
+
return @wsdl.endpoint unless @globals[:host]
|
|
57
|
+
|
|
58
|
+
host_url = URI.parse(@globals[:host])
|
|
59
|
+
url = @wsdl.endpoint.dup
|
|
60
|
+
url.host = host_url.host
|
|
61
|
+
url.port = host_url.port
|
|
62
|
+
url
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Resolves the WSSE auth credentials passed to Akami. A local value takes
|
|
66
|
+
# precedence over the global one. A local +false+ disables auth even when a
|
|
67
|
+
# global value is set. A local +nil+ leaves the option unset and falls
|
|
68
|
+
# through to the global value.
|
|
69
|
+
#
|
|
70
|
+
# @return [Array<String>, false, nil] the credentials, +false+ to disable,
|
|
71
|
+
# or +nil+ when unset in both scopes
|
|
72
|
+
def wsse_auth
|
|
73
|
+
prefer_local(:wsse_auth)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Resolves whether Akami emits a +wsu:Timestamp+ header, using the same
|
|
77
|
+
# local-over-global rule as {#wsse_auth}. A local +false+ disables it and a
|
|
78
|
+
# local +nil+ keeps the global value.
|
|
79
|
+
#
|
|
80
|
+
# @return [Boolean, nil]
|
|
81
|
+
def wsse_timestamp
|
|
82
|
+
prefer_local(:wsse_timestamp)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Resolves the WSSE signature used to sign the request. This option is a
|
|
86
|
+
# signature object or +nil+ and has no "disable" value, so any falsy local
|
|
87
|
+
# value falls back to the global one. {Savon::Builder} and {Savon::Header}
|
|
88
|
+
# both read it and must resolve it identically.
|
|
89
|
+
#
|
|
90
|
+
# @return [Akami::WSSE::Signature, nil]
|
|
91
|
+
def wsse_signature
|
|
92
|
+
@locals[:wsse_signature] || @globals[:wsse_signature]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Resolves the SOAP header content. When both scopes provide a Hash the two
|
|
96
|
+
# are merged and local keys win. Otherwise the local value is preferred and
|
|
97
|
+
# falls back to the global one. A Hash is rendered to XML by Gyoku. A String,
|
|
98
|
+
# or any object responding to +#to_s+, is used verbatim.
|
|
99
|
+
#
|
|
100
|
+
# @return [Hash, String, nil]
|
|
101
|
+
def soap_header
|
|
102
|
+
global = @globals[:soap_header]
|
|
103
|
+
local = @locals[:soap_header]
|
|
104
|
+
|
|
105
|
+
if global.is_a?(Hash) && local.is_a?(Hash)
|
|
106
|
+
global.merge(local)
|
|
107
|
+
else
|
|
108
|
+
local || global
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Resolves an option where a local +false+ is meaningful and disables it.
|
|
115
|
+
# Only a local +nil+ falls through to the global value. Contrast with
|
|
116
|
+
# {#wsse_signature}, which treats any falsy local as unset.
|
|
117
|
+
#
|
|
118
|
+
# @param key [Symbol] the option name
|
|
119
|
+
# @return [Object, nil] the local value unless it is +nil+, else the global
|
|
120
|
+
def prefer_local(key)
|
|
121
|
+
@locals[key].nil? ? @globals[key] : @locals[key]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
data/lib/savon/header.rb
CHANGED
|
@@ -1,89 +1,136 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "akami"
|
|
3
4
|
require "gyoku"
|
|
4
|
-
require "
|
|
5
|
+
require "savon/addressing"
|
|
5
6
|
|
|
6
7
|
module Savon
|
|
8
|
+
# Assembles the SOAP +Header+ element of a request envelope and renders it to
|
|
9
|
+
# XML.
|
|
10
|
+
#
|
|
11
|
+
# A SOAP Header carries a request's out-of-band metadata. This object is
|
|
12
|
+
# responsible for every header block Savon can emit, concatenated in this
|
|
13
|
+
# order:
|
|
14
|
+
#
|
|
15
|
+
# 1. application headers supplied by the caller (the +soap_header+ option),
|
|
16
|
+
# 2. the WS-Addressing headers, rendered by {Savon::Addressing},
|
|
17
|
+
# 3. the WS-Security header with UsernameToken credentials, a +wsu:Timestamp+
|
|
18
|
+
# and an XML Signature (OASIS WSS SOAP Message Security 1.1).
|
|
19
|
+
#
|
|
20
|
+
# WS-Security markup is delegated to Akami and Hash-to-XML conversion to Gyoku.
|
|
7
21
|
class Header
|
|
22
|
+
# @param globals [Savon::GlobalOptions] client-level options, read for the
|
|
23
|
+
# Gyoku key converter (+:convert_request_keys_to+) and the WS-Addressing
|
|
24
|
+
# toggle (+:use_wsa_headers+)
|
|
25
|
+
# @param effective [Savon::EffectiveOptions] resolves the WSSE, soap_header,
|
|
26
|
+
# soap_action and endpoint values for the request
|
|
27
|
+
def initialize(globals, effective)
|
|
28
|
+
@gyoku_options = { key_converter: globals[:convert_request_keys_to] }
|
|
29
|
+
|
|
30
|
+
@wsse_auth = effective.wsse_auth
|
|
31
|
+
@wsse_timestamp = effective.wsse_timestamp
|
|
32
|
+
@wsse_signature = effective.wsse_signature
|
|
33
|
+
@soap_header = effective.soap_header
|
|
34
|
+
|
|
35
|
+
@addressing = build_addressing(globals, effective)
|
|
36
|
+
@header = build
|
|
37
|
+
end
|
|
8
38
|
|
|
9
|
-
|
|
10
|
-
@gyoku_options = { :key_converter => globals[:convert_request_keys_to] }
|
|
11
|
-
|
|
12
|
-
@wsse_auth = locals[:wsse_auth].nil? ? globals[:wsse_auth] : locals[:wsse_auth]
|
|
13
|
-
@wsse_timestamp = locals[:wsse_timestamp].nil? ? globals[:wsse_timestamp] : locals[:wsse_timestamp]
|
|
14
|
-
@wsse_signature = locals[:wsse_signature].nil? ? globals[:wsse_signature] : locals[:wsse_signature]
|
|
15
|
-
|
|
16
|
-
@global_header = globals[:soap_header]
|
|
17
|
-
@local_header = locals[:soap_header]
|
|
18
|
-
|
|
19
|
-
@globals = globals
|
|
20
|
-
@locals = locals
|
|
39
|
+
attr_reader :gyoku_options, :wsse_auth, :wsse_timestamp, :wsse_signature
|
|
21
40
|
|
|
22
|
-
|
|
41
|
+
# Returns the XML attributes of the enclosing Header element. Only the
|
|
42
|
+
# WS-Addressing section declares one, the +xmlns:wsa+ prefix.
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash{String => String}]
|
|
45
|
+
def attributes
|
|
46
|
+
@addressing.namespace_attributes
|
|
23
47
|
end
|
|
24
48
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
49
|
+
# @return [Boolean] whether the rendered header is empty, i.e. there is no
|
|
50
|
+
# header content to include in the envelope
|
|
28
51
|
def empty?
|
|
29
52
|
@header.empty?
|
|
30
53
|
end
|
|
31
54
|
|
|
55
|
+
# Returns the rendered SOAP header XML. The header is built once during
|
|
56
|
+
# construction and cached, so this is a cheap, repeatable read.
|
|
57
|
+
#
|
|
58
|
+
# @return [String] the header XML (may be empty)
|
|
32
59
|
def to_s
|
|
33
60
|
@header
|
|
34
61
|
end
|
|
35
62
|
|
|
36
63
|
private
|
|
37
64
|
|
|
65
|
+
# Constructs the WS-Addressing section from the resolved request values.
|
|
66
|
+
# Resolving the endpoint raises without a WSDL document and without a
|
|
67
|
+
# global +:endpoint+, so action and endpoint are only resolved when the
|
|
68
|
+
# +:use_wsa_headers+ global asks for the headers.
|
|
69
|
+
#
|
|
70
|
+
# @return [Savon::Addressing]
|
|
71
|
+
def build_addressing(globals, effective)
|
|
72
|
+
return Addressing.new(enabled: false) unless globals[:use_wsa_headers]
|
|
73
|
+
|
|
74
|
+
Addressing.new(
|
|
75
|
+
enabled: true,
|
|
76
|
+
action: effective.soap_action,
|
|
77
|
+
to: effective.endpoint
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Concatenates the three header sections in document order: caller content,
|
|
82
|
+
# WS-Addressing, then WSSE security.
|
|
83
|
+
#
|
|
84
|
+
# @return [String]
|
|
38
85
|
def build
|
|
39
|
-
build_header +
|
|
86
|
+
build_header + @addressing.to_xml + build_wsse_header
|
|
40
87
|
end
|
|
41
88
|
|
|
89
|
+
# Renders the resolved soap_header. {Savon::EffectiveOptions#soap_header}
|
|
90
|
+
# merges the global and local values, so there is nothing to combine here.
|
|
91
|
+
# Hash-to-XML conversion only. Strings pass through.
|
|
92
|
+
#
|
|
93
|
+
# @return [String]
|
|
42
94
|
def build_header
|
|
43
|
-
|
|
44
|
-
if global_header.kind_of?(Hash) && local_header.kind_of?(Hash)
|
|
45
|
-
global_header.merge(local_header)
|
|
46
|
-
elsif local_header
|
|
47
|
-
local_header
|
|
48
|
-
else
|
|
49
|
-
global_header
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
convert_to_xml(header)
|
|
95
|
+
convert_to_xml(@soap_header)
|
|
53
96
|
end
|
|
54
97
|
|
|
98
|
+
# Builds the WS-Security header via Akami, or an empty string when no WSSE
|
|
99
|
+
# content (credentials, timestamp or signature) applies.
|
|
100
|
+
#
|
|
101
|
+
# @return [String]
|
|
55
102
|
def build_wsse_header
|
|
56
103
|
wsse_header = akami
|
|
57
104
|
wsse_header.respond_to?(:to_xml) ? wsse_header.to_xml : ""
|
|
58
105
|
end
|
|
59
106
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
'wsa:MessageID' => "urn:uuid:#{SecureRandom.uuid}"
|
|
66
|
-
})
|
|
67
|
-
end
|
|
68
|
-
|
|
107
|
+
# Renders a header value to XML. A Hash goes through Gyoku (honouring the
|
|
108
|
+
# configured key converter), anything else is coerced with +#to_s+.
|
|
109
|
+
#
|
|
110
|
+
# @param hash_or_string [Hash, #to_s] the header content
|
|
111
|
+
# @return [String]
|
|
69
112
|
def convert_to_xml(hash_or_string)
|
|
70
|
-
if hash_or_string.
|
|
113
|
+
if hash_or_string.is_a? Hash
|
|
71
114
|
Gyoku.xml(hash_or_string, gyoku_options)
|
|
72
115
|
else
|
|
73
116
|
hash_or_string.to_s
|
|
74
117
|
end
|
|
75
118
|
end
|
|
76
119
|
|
|
120
|
+
# Configures an Akami WSSE object from the resolved WSSE options. Sets the
|
|
121
|
+
# UsernameToken credentials, enables the wsu:Timestamp, and attaches the XML
|
|
122
|
+
# Signature when a document has been assigned to it.
|
|
123
|
+
#
|
|
124
|
+
# @return [Akami::WSSE] configured, not yet serialized
|
|
77
125
|
def akami
|
|
78
126
|
wsse = Akami.wsse
|
|
79
127
|
wsse.credentials(*wsse_auth) if wsse_auth
|
|
80
128
|
wsse.timestamp = wsse_timestamp if wsse_timestamp
|
|
81
|
-
if wsse_signature
|
|
129
|
+
if wsse_signature&.have_document?
|
|
82
130
|
wsse.signature = wsse_signature
|
|
83
131
|
end
|
|
84
132
|
|
|
85
133
|
wsse
|
|
86
134
|
end
|
|
87
|
-
|
|
88
135
|
end
|
|
89
136
|
end
|
data/lib/savon/http_error.rb
CHANGED
|
@@ -2,26 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
module Savon
|
|
4
4
|
class HTTPError < Error
|
|
5
|
-
|
|
6
5
|
def self.present?(http)
|
|
7
6
|
http.error?
|
|
8
7
|
end
|
|
9
8
|
|
|
10
|
-
def initialize(
|
|
11
|
-
@
|
|
9
|
+
def initialize(transport_response)
|
|
10
|
+
@transport_response = transport_response
|
|
12
11
|
end
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
def http
|
|
14
|
+
@transport_response
|
|
15
|
+
end
|
|
15
16
|
|
|
16
17
|
def to_s
|
|
17
|
-
String.new("HTTP error (#{
|
|
18
|
-
str_error << ": #{
|
|
18
|
+
String.new("HTTP error (#{http.code})").tap do |str_error|
|
|
19
|
+
str_error << ": #{http.body}" unless http.body.empty?
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def to_hash
|
|
23
|
-
{ :
|
|
24
|
+
{ code: http.code, headers: http.headers, body: http.body }
|
|
24
25
|
end
|
|
25
|
-
|
|
26
26
|
end
|
|
27
27
|
end
|
data/lib/savon/log_message.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "nokogiri"
|
|
3
4
|
|
|
4
5
|
module Savon
|
|
5
6
|
class LogMessage
|
|
6
|
-
|
|
7
7
|
def initialize(message, filters = [], pretty_print = false)
|
|
8
8
|
@message = message
|
|
9
9
|
@filters = filters
|
|
@@ -46,8 +46,7 @@ module Savon
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def nokogiri_options
|
|
49
|
-
@pretty_print ? { :
|
|
49
|
+
@pretty_print ? { indent: 2 } : { save_with: Nokogiri::XML::Node::SaveOptions::AS_XML }
|
|
50
50
|
end
|
|
51
|
-
|
|
52
51
|
end
|
|
53
52
|
end
|
data/lib/savon/message.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "savon/qualified_message"
|
|
3
4
|
require "gyoku"
|
|
4
5
|
|
|
5
6
|
module Savon
|
|
6
7
|
class Message
|
|
7
|
-
|
|
8
8
|
def initialize(message_tag, namespace_identifier, types, used_namespaces, message, element_form_default, key_converter, unwrap)
|
|
9
9
|
@message_tag = message_tag
|
|
10
10
|
@namespace_identifier = namespace_identifier
|
|
@@ -18,21 +18,20 @@ module Savon
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def to_s
|
|
21
|
-
return @message.to_s unless @message.
|
|
21
|
+
return @message.to_s unless @message.is_a? Hash
|
|
22
22
|
|
|
23
23
|
if @element_form_default == :qualified
|
|
24
24
|
@message = QualifiedMessage.new(@types, @used_namespaces, @key_converter).to_hash(@message, [@message_tag.to_s])
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
gyoku_options = {
|
|
28
|
-
:
|
|
29
|
-
:
|
|
30
|
-
:
|
|
31
|
-
:
|
|
28
|
+
element_form_default: @element_form_default,
|
|
29
|
+
namespace: @namespace_identifier,
|
|
30
|
+
key_converter: @key_converter,
|
|
31
|
+
unwrap: @unwrap
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
Gyoku.xml(@message, gyoku_options)
|
|
35
35
|
end
|
|
36
|
-
|
|
37
36
|
end
|
|
38
37
|
end
|