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,67 +1,54 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require "savon/options"
4
- require "savon/effective_options"
5
3
  require "savon/block_interface"
4
+ require "savon/request"
6
5
  require "savon/builder"
7
6
  require "savon/response"
7
+ require "savon/request_logger"
8
8
  require "savon/http_error"
9
- require "savon/transport/httpi"
10
- require "savon/transport/faraday"
11
9
  require "mail"
10
+ require 'faraday/gzip'
11
+
12
12
 
13
13
  module Savon
14
- # Represents a single named SOAP operation.
15
- #
16
- # Bridges the SOAP layer (envelope building, action headers, multipart) and the
17
- # transport layer (execution, logging). Knows nothing about transport internals
18
- # such as proxy, SSL, or auth.
19
14
  class Operation
20
- # SOAP Content-Type values indexed by SOAP version.
21
- # SOAP 1.1 §6 (HTTP binding), SOAP 1.2 Part 2 §7.1.4 (HTTP media type)
22
- CONTENT_TYPE = {
23
- 1 => "text/xml;charset=%s",
24
- 2 => "application/soap+xml;charset=%s"
25
- }.freeze
26
-
27
- # Maps SOAP version to the base MIME type used in multipart requests.
28
- # RFC 2387 §3.1 (multipart/related Content-Type parameter)
15
+
29
16
  SOAP_REQUEST_TYPE = {
30
17
  1 => "text/xml",
31
18
  2 => "application/soap+xml"
32
- }.freeze
19
+ }
33
20
 
34
- def self.create(operation_name, wsdl, globals, transport)
21
+ def self.create(operation_name, wsdl, globals)
35
22
  if wsdl.document?
36
23
  ensure_name_is_symbol! operation_name
37
- ensure_exists! operation_name, wsdl, transport
24
+ ensure_exists! operation_name, wsdl
38
25
  end
39
26
 
40
- new(operation_name, wsdl, globals, transport)
27
+ new(operation_name, wsdl, globals)
41
28
  end
42
29
 
43
- # Verifies the operation is provided by the WSDL.
44
- def self.ensure_exists!(operation_name, wsdl, transport)
30
+ def self.ensure_exists!(operation_name, wsdl)
45
31
  unless wsdl.soap_actions.include? operation_name
46
32
  raise UnknownOperationError, "Unable to find SOAP operation: #{operation_name.inspect}\n" \
47
33
  "Operations provided by your service: #{wsdl.soap_actions.inspect}"
48
34
  end
49
35
  rescue Wasabi::Resolver::HTTPError => e
50
- raise HTTPError, transport.normalize_response(e.response)
36
+ raise HTTPError.new(e.response)
51
37
  end
52
38
 
53
39
  def self.ensure_name_is_symbol!(operation_name)
54
- return if operation_name.is_a? Symbol
55
-
56
- raise ArgumentError, "Expected the first parameter (the name of the operation to call) to be a symbol\n" \
57
- "Actual: #{operation_name.inspect} (#{operation_name.class})"
40
+ unless operation_name.kind_of? Symbol
41
+ raise ArgumentError, "Expected the first parameter (the name of the operation to call) to be a symbol\n" \
42
+ "Actual: #{operation_name.inspect} (#{operation_name.class})"
43
+ end
58
44
  end
59
45
 
60
- def initialize(name, wsdl, globals, transport)
61
- @name = name
62
- @wsdl = wsdl
63
- @globals = globals
64
- @transport = transport
46
+ def initialize(name, wsdl, globals)
47
+ @name = name
48
+ @wsdl = wsdl
49
+ @globals = globals
50
+
51
+ @logger = RequestLogger.new(globals)
65
52
  end
66
53
 
67
54
  def build(locals = {}, &block)
@@ -69,45 +56,32 @@ module Savon
69
56
  Builder.new(@name, @wsdl, @globals, @locals)
70
57
  end
71
58
 
72
- # Executes the SOAP operation and returns a Savon::Response.
73
- #
74
- # Observer short-circuit: if any registered observer returns a
75
- # Transport::Response (or legacy HTTPI::Response), the HTTP call
76
- # is skipped and that response is used directly.
77
59
  def call(locals = {}, &block)
78
60
  builder = build(locals, &block)
79
- observer_response = Savon.notify_observers(@name, builder, @globals, @locals)
80
-
81
- transport_response =
82
- if observer_response.nil?
83
- body = builder.to_s
84
- headers = soap_headers(builder)
85
- @transport.post(endpoint.to_s, headers, body, @locals)
86
- else
87
- normalize_observer_response(observer_response)
88
- end
89
61
 
90
- Response.new(transport_response, @globals, @locals)
62
+ response = Savon.notify_observers(@name, builder, @globals, @locals)
63
+ response ||= call_with_logging build_connection(builder)
64
+
65
+ raise_expected_faraday_response! unless response.kind_of?(Faraday::Response)
66
+
67
+ create_response(response)
91
68
  end
92
69
 
93
- # Builds and returns the HTTPI::Request that would be sent for this
94
- # operation, without executing it. Useful for inspection and debugging.
95
- # Not supported with transport: :faraday.
96
70
  def request(locals = {}, &block)
97
- if @globals[:transport] == :faraday
98
- raise ArgumentError, "#request returns an HTTPI::Request and is not supported " \
99
- "with transport: :faraday. Use client.faraday to configure " \
100
- "the connection"
101
- end
102
-
103
71
  builder = build(locals, &block)
104
- # Build the body before soap_headers - see #call.
105
- body = builder.to_s
106
- @transport.to_httpi_request(endpoint.to_s, soap_headers(builder), body, @locals)
72
+ connection = build_connection(builder)
73
+ connection.build_request(:post) do |req|
74
+ req.url(@globals[:endpoint])
75
+ req.body = @locals[:body]
76
+ end
107
77
  end
108
78
 
109
79
  private
110
80
 
81
+ def create_response(response)
82
+ Response.new(response, @globals, @locals)
83
+ end
84
+
111
85
  def set_locals(locals, block)
112
86
  locals = LocalOptions.new(locals)
113
87
  BlockInterface.new(locals).evaluate(block) if block
@@ -115,69 +89,75 @@ module Savon
115
89
  @locals = locals
116
90
  end
117
91
 
118
- # The effective options view for the current request. Built on demand
119
- # instead of being cached, so it always reflects the current per-request
120
- # options.
121
- def effective_options
122
- EffectiveOptions.new(@name, @wsdl, @globals, @locals)
92
+ def call_with_logging(connection)
93
+ ntlm_auth = handle_ntlm(connection) if @globals.include?(:ntlm)
94
+ @logger.log_response(connection.post(@globals[:endpoint]) { |request|
95
+ request.body = @locals[:body]
96
+ request.headers['Authorization'] = "NTLM #{auth.encode64}" if ntlm_auth
97
+ @logger.log_request(request)
98
+ })
99
+ end
100
+
101
+ def handle_ntlm(connection)
102
+ ntlm_message = Net::NTLM::Message
103
+ response = connection.get(@globals[:endpoint]) do |request|
104
+ request.headers['Authorization'] = 'NTLM ' + ntlm_message::Type1.new.encode64
105
+ end
106
+ challenge = response.headers['www-authenticate'][/(?:NTLM|Negotiate) (.*)$/, 1]
107
+ message = ntlm_message::Type2.decode64(challenge)
108
+ message.response([:user, :password, :domain].zip(@globals[:ntlm]).to_h)
123
109
  end
124
110
 
125
- # Assembles the SOAP-level request headers for the given builder.
126
- #
127
- # Our responsibility regardless of transport:
128
- # * Content-Type (SOAP 1.1 §6 / SOAP 1.2 Part 2 §7.1.4)
129
- # * SOAPAction (SOAP 1.1 §6.1.1)
130
- # * Multipart Content-Type (RFC 2387), MIME-Version (RFC 2045 §4), Accept-Encoding (RFC 9110 §12.5.3)
131
- def soap_headers(builder)
132
- headers = {}
133
-
134
- if builder.multipart
135
- # RFC 2387 §3 (multipart/related) - SOAP envelope is the root body part
136
- headers["Content-Type"] = [
137
- "multipart/related",
138
- "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"",
139
- "start=\"#{builder.multipart[:start]}\"",
140
- "boundary=\"#{builder.multipart[:multipart_boundary]}\""
141
- ].join("; ")
142
- headers["MIME-Version"] = "1.0"
143
- headers["Accept-Encoding"] = "gzip,deflate"
144
- else
145
- headers["Content-Type"] = CONTENT_TYPE[@globals[:soap_version]] % @globals[:encoding]
111
+ def build_connection(builder)
112
+ @globals[:endpoint] ||= endpoint
113
+ @locals[:soap_action] ||= soap_action
114
+ @locals[:body] = builder.to_s
115
+ @connection = SOAPRequest.new(@globals).build(
116
+ :soap_action => soap_action,
117
+ :cookies => @locals[:cookies],
118
+ :headers => @locals[:headers]
119
+ ) do |connection|
120
+ if builder.multipart
121
+ connection.request :gzip
122
+ connection.headers["Content-Type"] = %W[multipart/related
123
+ type="#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}",
124
+ start="#{builder.multipart[:start]}",
125
+ boundary="#{builder.multipart[:multipart_boundary]}"].join("; ")
126
+ connection.headers["MIME-Version"] = "1.0"
127
+ end
128
+
129
+ connection.headers["Content-Length"] = @locals[:body].bytesize.to_s
146
130
  end
147
131
 
148
- action = soap_action
149
- headers["SOAPAction"] = %("#{action}") if action
150
132
 
151
- headers
152
133
  end
153
134
 
154
- # The resolved SOAPAction of the request, used for the HTTP header.
155
- # {Savon::EffectiveOptions#soap_action} owns the precedence rules.
156
135
  def soap_action
157
- effective_options.soap_action
136
+ # soap_action explicitly set to something falsy
137
+ return if @locals.include?(:soap_action) && !@locals[:soap_action]
138
+
139
+ # get the soap_action from local options
140
+ @locals[:soap_action] ||
141
+ # with no local option, but a wsdl, ask it for the soap_action
142
+ @wsdl.document? && @wsdl.soap_action(@name.to_sym) ||
143
+ # if there is no soap_action up to this point, fallback to a simple default
144
+ Gyoku.xml_tag(@name, :key_converter => @globals[:convert_request_keys_to])
158
145
  end
159
146
 
160
- # The resolved endpoint URL the request is sent to.
161
- # {Savon::EffectiveOptions#endpoint} owns the precedence rules.
162
147
  def endpoint
163
- effective_options.endpoint
164
- end
165
-
166
- # Normalizes an observer return value into a Transport::Response.
167
- #
168
- # Accepts Transport::Response directly (current contract), wraps
169
- # HTTPI::Response with a deprecation warning (legacy observer support),
170
- # and raises on anything else.
171
- def normalize_observer_response(observer_response)
172
- return observer_response if observer_response.is_a?(Transport::Response)
173
-
174
- if observer_response.is_a?(HTTPI::Response)
175
- warn "Observers returning HTTPI::Response is deprecated - return Savon::Transport::Response instead.", uplevel: 1
176
- return Transport::Response.from_httpi(observer_response)
148
+ @globals[:endpoint] || @wsdl.endpoint.tap do |url|
149
+ if @globals[:host]
150
+ host_url = URI.parse(@globals[:host])
151
+ url.host = host_url.host
152
+ url.port = host_url.port
153
+ end
177
154
  end
155
+ end
178
156
 
179
- raise Error, "Observers need to return a Savon::Transport::Response " \
180
- "to mock the request or nil to execute the request."
157
+ def raise_expected_faraday_response!
158
+ raise Error, "Observers need to return a Faraday::Response to mock " \
159
+ "the request or nil to execute the request."
181
160
  end
161
+
182
162
  end
183
163
  end