Skip to content

Commit 08f4d44

Browse files
pcaiekzobrain
andauthoredJul 15, 2024··
MTOM support, nori passthrough (#1012)
* Add MTOM support for SOAP attachments * Fix request logging when message contains non-ascii characters * Fix using attachments together with 'xml' option --------- Co-authored-by: Архипов Дмитрий <[email protected]>
1 parent b1b738c commit 08f4d44

File tree

6 files changed

+134
-34
lines changed

6 files changed

+134
-34
lines changed
 

‎lib/savon/builder.rb

+29-12
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,23 @@ def pretty
3838
end
3939

4040
def build_document
41-
xml_result = build_xml
41+
# check if xml was already provided
42+
if @locals.include? :xml
43+
xml_result = @locals[:xml]
44+
else
45+
xml_result = build_xml
4246

43-
# if we have a signature sign the document
44-
if @signature
45-
@signature.document = xml_result
47+
# if we have a signature sign the document
48+
if @signature
49+
@signature.document = xml_result
4650

47-
2.times do
48-
@header = nil
49-
@signature.document = build_xml
50-
end
51+
2.times do
52+
@header = nil
53+
@signature.document = build_xml
54+
end
5155

52-
xml_result = @signature.document
56+
xml_result = @signature.document
57+
end
5358
end
5459

5560
# if there are attachments for the request, we should build a multipart message according to
@@ -70,7 +75,6 @@ def body_attributes
7075
end
7176

7277
def to_s
73-
return @locals[:xml] if @locals.include? :xml
7478
build_document
7579
end
7680

@@ -254,15 +258,28 @@ def build_multipart_message(message_xml)
254258

255259
# the mail.body.encoded algorithm reorders the parts, default order is [ "text/plain", "text/enriched", "text/html" ]
256260
# should redefine the sort order, because the soap request xml should be the first
257-
multipart_message.body.set_sort_order [ "text/xml" ]
261+
multipart_message.body.set_sort_order ['application/xop+xml', 'text/xml']
258262

259263
multipart_message.body.encoded(multipart_message.content_transfer_encoding)
260264
end
261265

262266
def init_multipart_message(message_xml)
263267
multipart_message = Mail.new
268+
269+
# MTOM differs from general SOAP attachments:
270+
# 1. binary encoding
271+
# 2. application/xop+xml mime type
272+
if @locals[:mtom]
273+
type = "application/xop+xml; charset=#{@globals[:encoding]}; type=\"text/xml\""
274+
275+
multipart_message.transport_encoding = 'binary'
276+
message_xml.force_encoding('BINARY')
277+
else
278+
type = 'text/xml'
279+
end
280+
264281
xml_part = Mail::Part.new do
265-
content_type 'text/xml'
282+
content_type type
266283
body message_xml
267284
# in Content-Type the start parameter is recommended (RFC 2387)
268285
content_id '<soap-request-body@soap>'

‎lib/savon/operation.rb

+11-7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Operation
1717
1 => "text/xml",
1818
2 => "application/soap+xml"
1919
}
20+
SOAP_REQUEST_TYPE_MTOM = "application/xop+xml"
2021

2122
def self.create(operation_name, wsdl, globals)
2223
if wsdl.document?
@@ -118,18 +119,21 @@ def build_connection(builder)
118119
:headers => @locals[:headers]
119120
) do |connection|
120121
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("; ")
122+
ctype_headers = ["multipart/related"]
123+
if @locals[:mtom]
124+
ctype_headers << "type=\"#{SOAP_REQUEST_TYPE_MTOM}\""
125+
ctype_headers << "start-info=\"text/xml\""
126+
else
127+
ctype_headers << "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\""
128+
connection.request :gzip
129+
end
130+
connection.headers["Content-Type"] = (ctype_headers + ["start=\"#{builder.multipart[:start]}\"",
131+
"boundary=\"#{builder.multipart[:multipart_boundary]}\""]).join("; ")
126132
connection.headers["MIME-Version"] = "1.0"
127133
end
128134

129135
connection.headers["Content-Length"] = @locals[:body].bytesize.to_s
130136
end
131-
132-
133137
end
134138

135139
def soap_action

‎lib/savon/options.rb

+12-1
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,8 @@ def initialize(options = {})
397397
:advanced_typecasting => true,
398398
:response_parser => :nokogiri,
399399
:multipart => false,
400-
:body => false
400+
:body => false,
401+
:mtom => false
401402
}
402403

403404
super defaults.merge(options)
@@ -460,6 +461,11 @@ def attachments(attachments)
460461
@options[:attachments] = attachments
461462
end
462463

464+
# Instruct Savon to send attachments using MTOM https://www.w3.org/TR/soap12-mtom/
465+
def mtom(mtom)
466+
@options[:mtom] = mtom
467+
end
468+
463469
# Value of the SOAPAction HTTP header.
464470
def soap_action(soap_action)
465471
@options[:soap_action] = soap_action
@@ -489,6 +495,11 @@ def response_parser(parser)
489495
@options[:response_parser] = parser
490496
end
491497

498+
# Pass already configured Nori instance.
499+
def nori(nori)
500+
@options[:nori] = nori
501+
end
502+
492503
# Instruct Savon to create a multipart response if available.
493504
def multipart(multipart)
494505
@options[:multipart] = multipart

‎lib/savon/request_logger.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def headers_to_log(headers)
5050
end
5151

5252
def body_to_log(body)
53-
LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s
53+
LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s.force_encoding(@globals[:encoding])
5454
end
5555

5656
end

‎lib/savon/response.rb

+9-13
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,16 @@ def xml_namespaces
142142
end
143143

144144
def nori
145-
return @nori if @nori
145+
return @locals[:nori] if @locals[:nori]
146146

147-
nori_options = {
148-
:delete_namespace_attributes => @globals[:delete_namespace_attributes],
149-
:strip_namespaces => @globals[:strip_namespaces],
150-
:convert_tags_to => @globals[:convert_response_tags_to],
151-
:convert_attributes_to => @globals[:convert_attributes_to],
152-
:advanced_typecasting => @locals[:advanced_typecasting],
153-
:parser => @locals[:response_parser]
154-
}
155-
156-
non_nil_nori_options = nori_options.reject { |_, value| value.nil? }
157-
@nori = Nori.new(non_nil_nori_options)
147+
@nori ||= Nori.new({
148+
:delete_namespace_attributes => @globals[:delete_namespace_attributes],
149+
:strip_namespaces => @globals[:strip_namespaces],
150+
:convert_tags_to => @globals[:convert_response_tags_to],
151+
:convert_attributes_to => @globals[:convert_attributes_to],
152+
:advanced_typecasting => @locals[:advanced_typecasting],
153+
:parser => @locals[:response_parser]
154+
}.reject { |_, value| value.nil? })
158155
end
159-
160156
end
161157
end

‎spec/savon/operation_spec.rb

+72
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,78 @@ def new_operation(operation_name, wsdl, globals)
197197
end
198198
end
199199

200+
describe "attachments" do
201+
context "soap_version 1" do
202+
it "sends requests with content-type text/xml" do
203+
globals.endpoint @server.url(:multipart)
204+
operation = new_operation(:example, no_wsdl, globals)
205+
req = operation.request do
206+
attachments [
207+
{ filename: 'x1.xml', content: '<xml>abc</xml>'},
208+
{ filename: 'x2.xml', content: '<xml>cde</xml>'},
209+
]
210+
end
211+
expect(req.headers["Content-Type"]).to start_with "multipart/related; type=\"text/xml\"; "
212+
end
213+
end
214+
context "soap_version 2" do
215+
it "sends requests with content-type application/soap+xml" do
216+
globals.endpoint @server.url(:multipart)
217+
globals.soap_version 2
218+
operation = new_operation(:example, no_wsdl, globals)
219+
req = operation.request do
220+
attachments [
221+
{ filename: 'x1.xml', content: '<xml>abc</xml>'},
222+
{ filename: 'x2.xml', content: '<xml>cde</xml>'},
223+
]
224+
end
225+
expect(req.headers["Content-Type"]).to start_with "multipart/related; type=\"application/soap+xml\"; "
226+
end
227+
end
228+
context "MTOM" do
229+
it "sends request with content-type header application/xop+xml" do
230+
globals.endpoint @server.url(:multipart)
231+
operation = new_operation(:example, no_wsdl, globals)
232+
req = operation.request do
233+
mtom true
234+
attachments [
235+
{ filename: 'x1.xml', content: '<xml>abc</xml>'},
236+
{ filename: 'x2.xml', content: '<xml>cde</xml>'},
237+
]
238+
end
239+
expect(req.headers["Content-Type"]).to start_with "multipart/related; type=\"application/xop+xml\"; start-info=\"text/xml\"; start=\"<soap-request-body@soap>\"; boundary=\"--==_mimepart_"
240+
end
241+
242+
it "sends attachments with Content-Transfer-Encoding: binary" do
243+
globals.endpoint @server.url(:multipart)
244+
operation = new_operation(:example, no_wsdl, globals)
245+
req = operation.request do
246+
mtom true
247+
attachments [
248+
{ filename: 'x1.xml', content: '<xml>abc</xml>'},
249+
{ filename: 'x2.xml', content: '<xml>cde</xml>'},
250+
]
251+
end
252+
expect(req.body.to_s).to include("filename=x1.xml\r\nContent-Transfer-Encoding: binary")
253+
end
254+
255+
it "successfully makes request" do
256+
globals.endpoint @server.url(:multipart)
257+
operation = new_operation(:example, no_wsdl, globals)
258+
response = operation.call do
259+
mtom true
260+
attachments [
261+
{ filename: 'x1.xml', content: '<xml>abc</xml>'},
262+
{ filename: 'x2.xml', content: '<xml>cde</xml>'},
263+
]
264+
end
265+
266+
expect(response.multipart?).to be true
267+
expect(response.attachments.first.content_id).to include('attachment1')
268+
end
269+
end
270+
end
271+
200272
def inspect_request(response)
201273
hash = JSON.parse(response.http.body)
202274
OpenStruct.new(hash)

0 commit comments

Comments
 (0)
Please sign in to comment.