Skip to content

Commit

Permalink
MTOM support, nori passthrough (#1012)
Browse files Browse the repository at this point in the history
* 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: Архипов Дмитрий <da@netcitylife.ru>
  • Loading branch information
pcai and ekzobrain committed Jul 15, 2024
1 parent b1b738c commit 08f4d44
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 34 deletions.
41 changes: 29 additions & 12 deletions lib/savon/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,23 @@ def pretty
end

def build_document
xml_result = build_xml
# check if xml was already provided
if @locals.include? :xml
xml_result = @locals[:xml]
else
xml_result = build_xml

# if we have a signature sign the document
if @signature
@signature.document = xml_result
# if we have a signature sign the document
if @signature
@signature.document = xml_result

2.times do
@header = nil
@signature.document = build_xml
end
2.times do
@header = nil
@signature.document = build_xml
end

xml_result = @signature.document
xml_result = @signature.document
end
end

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

def to_s
return @locals[:xml] if @locals.include? :xml
build_document
end

Expand Down Expand Up @@ -254,15 +258,28 @@ def build_multipart_message(message_xml)

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

multipart_message.body.encoded(multipart_message.content_transfer_encoding)
end

def init_multipart_message(message_xml)
multipart_message = Mail.new

# MTOM differs from general SOAP attachments:
# 1. binary encoding
# 2. application/xop+xml mime type
if @locals[:mtom]
type = "application/xop+xml; charset=#{@globals[:encoding]}; type=\"text/xml\""

multipart_message.transport_encoding = 'binary'
message_xml.force_encoding('BINARY')
else
type = 'text/xml'
end

xml_part = Mail::Part.new do
content_type 'text/xml'
content_type type
body message_xml
# in Content-Type the start parameter is recommended (RFC 2387)
content_id '<soap-request-body@soap>'
Expand Down
18 changes: 11 additions & 7 deletions lib/savon/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Operation
1 => "text/xml",
2 => "application/soap+xml"
}
SOAP_REQUEST_TYPE_MTOM = "application/xop+xml"

def self.create(operation_name, wsdl, globals)
if wsdl.document?
Expand Down Expand Up @@ -118,18 +119,21 @@ def build_connection(builder)
:headers => @locals[:headers]
) do |connection|
if builder.multipart
connection.request :gzip
connection.headers["Content-Type"] = %W[multipart/related
type="#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}",
start="#{builder.multipart[:start]}",
boundary="#{builder.multipart[:multipart_boundary]}"].join("; ")
ctype_headers = ["multipart/related"]
if @locals[:mtom]
ctype_headers << "type=\"#{SOAP_REQUEST_TYPE_MTOM}\""
ctype_headers << "start-info=\"text/xml\""
else
ctype_headers << "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\""
connection.request :gzip
end
connection.headers["Content-Type"] = (ctype_headers + ["start=\"#{builder.multipart[:start]}\"",
"boundary=\"#{builder.multipart[:multipart_boundary]}\""]).join("; ")
connection.headers["MIME-Version"] = "1.0"
end

connection.headers["Content-Length"] = @locals[:body].bytesize.to_s
end


end

def soap_action
Expand Down
13 changes: 12 additions & 1 deletion lib/savon/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@ def initialize(options = {})
:advanced_typecasting => true,
:response_parser => :nokogiri,
:multipart => false,
:body => false
:body => false,
:mtom => false
}

super defaults.merge(options)
Expand Down Expand Up @@ -460,6 +461,11 @@ def attachments(attachments)
@options[:attachments] = attachments
end

# Instruct Savon to send attachments using MTOM https://www.w3.org/TR/soap12-mtom/
def mtom(mtom)
@options[:mtom] = mtom
end

# Value of the SOAPAction HTTP header.
def soap_action(soap_action)
@options[:soap_action] = soap_action
Expand Down Expand Up @@ -489,6 +495,11 @@ def response_parser(parser)
@options[:response_parser] = parser
end

# Pass already configured Nori instance.
def nori(nori)
@options[:nori] = nori
end

# Instruct Savon to create a multipart response if available.
def multipart(multipart)
@options[:multipart] = multipart
Expand Down
2 changes: 1 addition & 1 deletion lib/savon/request_logger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def headers_to_log(headers)
end

def body_to_log(body)
LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s
LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s.force_encoding(@globals[:encoding])
end

end
Expand Down
22 changes: 9 additions & 13 deletions lib/savon/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,16 @@ def xml_namespaces
end

def nori
return @nori if @nori
return @locals[:nori] if @locals[:nori]

nori_options = {
:delete_namespace_attributes => @globals[:delete_namespace_attributes],
:strip_namespaces => @globals[:strip_namespaces],
:convert_tags_to => @globals[:convert_response_tags_to],
:convert_attributes_to => @globals[:convert_attributes_to],
:advanced_typecasting => @locals[:advanced_typecasting],
:parser => @locals[:response_parser]
}

non_nil_nori_options = nori_options.reject { |_, value| value.nil? }
@nori = Nori.new(non_nil_nori_options)
@nori ||= Nori.new({
:delete_namespace_attributes => @globals[:delete_namespace_attributes],
:strip_namespaces => @globals[:strip_namespaces],
:convert_tags_to => @globals[:convert_response_tags_to],
:convert_attributes_to => @globals[:convert_attributes_to],
:advanced_typecasting => @locals[:advanced_typecasting],
:parser => @locals[:response_parser]
}.reject { |_, value| value.nil? })
end

end
end
72 changes: 72 additions & 0 deletions spec/savon/operation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,78 @@ def new_operation(operation_name, wsdl, globals)
end
end

describe "attachments" do
context "soap_version 1" do
it "sends requests with content-type text/xml" do
globals.endpoint @server.url(:multipart)
operation = new_operation(:example, no_wsdl, globals)
req = operation.request do
attachments [
{ filename: 'x1.xml', content: '<xml>abc</xml>'},
{ filename: 'x2.xml', content: '<xml>cde</xml>'},
]
end
expect(req.headers["Content-Type"]).to start_with "multipart/related; type=\"text/xml\"; "
end
end
context "soap_version 2" do
it "sends requests with content-type application/soap+xml" do
globals.endpoint @server.url(:multipart)
globals.soap_version 2
operation = new_operation(:example, no_wsdl, globals)
req = operation.request do
attachments [
{ filename: 'x1.xml', content: '<xml>abc</xml>'},
{ filename: 'x2.xml', content: '<xml>cde</xml>'},
]
end
expect(req.headers["Content-Type"]).to start_with "multipart/related; type=\"application/soap+xml\"; "
end
end
context "MTOM" do
it "sends request with content-type header application/xop+xml" do
globals.endpoint @server.url(:multipart)
operation = new_operation(:example, no_wsdl, globals)
req = operation.request do
mtom true
attachments [
{ filename: 'x1.xml', content: '<xml>abc</xml>'},
{ filename: 'x2.xml', content: '<xml>cde</xml>'},
]
end
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_"
end

it "sends attachments with Content-Transfer-Encoding: binary" do
globals.endpoint @server.url(:multipart)
operation = new_operation(:example, no_wsdl, globals)
req = operation.request do
mtom true
attachments [
{ filename: 'x1.xml', content: '<xml>abc</xml>'},
{ filename: 'x2.xml', content: '<xml>cde</xml>'},
]
end
expect(req.body.to_s).to include("filename=x1.xml\r\nContent-Transfer-Encoding: binary")
end

it "successfully makes request" do
globals.endpoint @server.url(:multipart)
operation = new_operation(:example, no_wsdl, globals)
response = operation.call do
mtom true
attachments [
{ filename: 'x1.xml', content: '<xml>abc</xml>'},
{ filename: 'x2.xml', content: '<xml>cde</xml>'},
]
end

expect(response.multipart?).to be true
expect(response.attachments.first.content_id).to include('attachment1')
end
end
end

def inspect_request(response)
hash = JSON.parse(response.http.body)
OpenStruct.new(hash)
Expand Down

0 comments on commit 08f4d44

Please sign in to comment.