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