From 201d79997e9b18f82c960b91d68a6e39364ade37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D0=BF=D0=BE=D0=B2=20=D0=94=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 3 Nov 2020 13:38:36 +0200 Subject: [PATCH 1/5] Add MTOM support for SOAP attachments (previously no support) Fix request logging when message containt non-ascii characters (log writing failure occured) Fix using attachments together with 'xml' option (when xml option was provided, attachments option was ignored) --- lib/savon/builder.rb | 41 ++++++++++++++++++++++++++----------- lib/savon/operation.rb | 22 +++++++++++++------- lib/savon/options.rb | 8 +++++++- lib/savon/request_logger.rb | 2 +- 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/lib/savon/builder.rb b/lib/savon/builder.rb index 13a17ef6..69d6dcc1 100644 --- a/lib/savon/builder.rb +++ b/lib/savon/builder.rb @@ -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 @@ -70,7 +75,6 @@ def body_attributes end def to_s - return @locals[:xml] if @locals.include? :xml build_document end @@ -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 '' diff --git a/lib/savon/operation.rb b/lib/savon/operation.rb index b28a806d..45bda39e 100644 --- a/lib/savon/operation.rb +++ b/lib/savon/operation.rb @@ -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? @@ -118,18 +119,25 @@ 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("; ") + # request.headers["Content-Type"] = ["multipart/related", + # "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"", + # "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 diff --git a/lib/savon/options.rb b/lib/savon/options.rb index 4a3c78be..e199b5d5 100644 --- a/lib/savon/options.rb +++ b/lib/savon/options.rb @@ -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) @@ -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 diff --git a/lib/savon/request_logger.rb b/lib/savon/request_logger.rb index e143d871..454878c9 100644 --- a/lib/savon/request_logger.rb +++ b/lib/savon/request_logger.rb @@ -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 From a5bdcb9b6ad13ad4d0a725fa0bf70af765aee433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D0=BF=D0=BE=D0=B2=20=D0=94=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 22 Feb 2021 13:04:56 +0200 Subject: [PATCH 2/5] Allow to pass option :empty_tag_value to Nori via local options --- .gitignore | 1 + lib/savon/options.rb | 5 +++++ lib/savon/response.rb | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7a958395..1c6eb47a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .byebug_history .DS_Store .yardoc +.ruby-version doc rdox coverage diff --git a/lib/savon/options.rb b/lib/savon/options.rb index e199b5d5..2e44acc7 100644 --- a/lib/savon/options.rb +++ b/lib/savon/options.rb @@ -495,6 +495,11 @@ def response_parser(parser) @options[:response_parser] = parser end + # Instruct Nori how to convert empty tags. + def empty_tag_value(empty_tag_value) + @options[:empty_tag_value] = empty_tag_value + end + # Instruct Savon to create a multipart response if available. def multipart(multipart) @options[:multipart] = multipart diff --git a/lib/savon/response.rb b/lib/savon/response.rb index 9ca4a35b..6bcd78f2 100644 --- a/lib/savon/response.rb +++ b/lib/savon/response.rb @@ -150,7 +150,8 @@ def nori :convert_tags_to => @globals[:convert_response_tags_to], :convert_attributes_to => @globals[:convert_attributes_to], :advanced_typecasting => @locals[:advanced_typecasting], - :parser => @locals[:response_parser] + :parser => @locals[:response_parser], + :empty_tag_value => @locals[:empty_tag_value] } non_nil_nori_options = nori_options.reject { |_, value| value.nil? } From cd1f2717f43c96871f5972a1e4c5c11146287368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D0=BF=D0=BE=D0=B2=20=D0=94=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 22 Feb 2021 13:39:05 +0200 Subject: [PATCH 3/5] Allow to pass already configured Nori instance and not mess Nori options with Savon options --- lib/savon/operation.rb | 4 ---- lib/savon/options.rb | 6 +++--- lib/savon/response.rb | 23 +++++++++-------------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/lib/savon/operation.rb b/lib/savon/operation.rb index 45bda39e..8618f8f1 100644 --- a/lib/savon/operation.rb +++ b/lib/savon/operation.rb @@ -129,10 +129,6 @@ def build_connection(builder) end connection.headers["Content-Type"] = (ctype_headers + ["start=\"#{builder.multipart[:start]}\"", "boundary=\"#{builder.multipart[:multipart_boundary]}\""]).join("; ") - # request.headers["Content-Type"] = ["multipart/related", - # "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"", - # "start=\"#{builder.multipart[:start]}\"", - # "boundary=\"#{builder.multipart[:multipart_boundary]}\""].join("; ") connection.headers["MIME-Version"] = "1.0" end diff --git a/lib/savon/options.rb b/lib/savon/options.rb index 2e44acc7..a3848b93 100644 --- a/lib/savon/options.rb +++ b/lib/savon/options.rb @@ -495,9 +495,9 @@ def response_parser(parser) @options[:response_parser] = parser end - # Instruct Nori how to convert empty tags. - def empty_tag_value(empty_tag_value) - @options[:empty_tag_value] = empty_tag_value + # Pass already configured Nori instance. + def nori(nori) + @options[:nori] = nori end # Instruct Savon to create a multipart response if available. diff --git a/lib/savon/response.rb b/lib/savon/response.rb index 6bcd78f2..e148182b 100644 --- a/lib/savon/response.rb +++ b/lib/savon/response.rb @@ -142,21 +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], - :empty_tag_value => @locals[:empty_tag_value] - } - - 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 From 985e9ce26cb87d8c2663b41670789cfbbd9e3603 Mon Sep 17 00:00:00 2001 From: Peter Cai <222655+pcai@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:50:33 +0000 Subject: [PATCH 4/5] mtom tests --- spec/savon/operation_spec.rb | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/spec/savon/operation_spec.rb b/spec/savon/operation_spec.rb index 24ee8cbc..a465b22d 100644 --- a/spec/savon/operation_spec.rb +++ b/spec/savon/operation_spec.rb @@ -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: 'abc'}, + { filename: 'x2.xml', content: 'cde'}, + ] + 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: 'abc'}, + { filename: 'x2.xml', content: 'cde'}, + ] + 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: 'abc'}, + { filename: 'x2.xml', content: 'cde'}, + ] + end + expect(req.headers["Content-Type"]).to start_with "multipart/related; type=\"application/xop+xml\"; start-info=\"text/xml\"; start=\"\"; 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: 'abc'}, + { filename: 'x2.xml', content: 'cde'}, + ] + 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: 'abc'}, + { filename: 'x2.xml', content: 'cde'}, + ] + 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) From 8720bd383471b7d6a308cacb1768aab7589d08f1 Mon Sep 17 00:00:00 2001 From: Peter Cai <222655+pcai@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:51:56 +0000 Subject: [PATCH 5/5] dont change gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1c6eb47a..7a958395 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ .byebug_history .DS_Store .yardoc -.ruby-version doc rdox coverage