Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MTOM support with tests #1012

Merged
merged 5 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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