From 485f378e9a439344464953a0a28fb1f6770835d4 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/3] 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 | 5 +++-- lib/savon/options.rb | 8 +++++++- lib/savon/request_logger.rb | 2 +- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/lib/savon/builder.rb b/lib/savon/builder.rb index a213fcf0..7a187c3f 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 @@ -242,15 +246,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 00e40d87..48872baa 100644 --- a/lib/savon/operation.rb +++ b/lib/savon/operation.rb @@ -101,9 +101,10 @@ def build_request(builder) request.body = builder.to_s if builder.multipart - request.gzip + type = @locals[:mtom] ? 'application/xop+xml"; start-info="text/xml' : SOAP_REQUEST_TYPE[@globals[:soap_version]] + request.gzip unless @locals[:mtom] request.headers["Content-Type"] = ["multipart/related", - "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"", + "type=\"#{type}\"", "start=\"#{builder.multipart[:start]}\"", "boundary=\"#{builder.multipart[:multipart_boundary]}\""].join("; ") request.headers["MIME-Version"] = "1.0" diff --git a/lib/savon/options.rb b/lib/savon/options.rb index 738e2504..416ec292 100644 --- a/lib/savon/options.rb +++ b/lib/savon/options.rb @@ -383,7 +383,8 @@ def initialize(options = {}) defaults = { :advanced_typecasting => true, :response_parser => :nokogiri, - :multipart => false + :multipart => false, + :mtom => false } super defaults.merge(options) @@ -446,6 +447,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 44ac30d9..7ebe691f 100644 --- a/lib/savon/request_logger.rb +++ b/lib/savon/request_logger.rb @@ -43,7 +43,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 f56393c1a0a5dc5c8ba29dbe8231eb2ca03adc1a 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/3] Allow to pass option :empty_tag_value to Nori via local options --- .gitignore | 1 + lib/savon/options.rb | 13 +++++++++---- lib/savon/response.rb | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 59101611..a3e1d4ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .rvmrc .DS_Store .yardoc +.ruby-version doc rdox coverage diff --git a/lib/savon/options.rb b/lib/savon/options.rb index 416ec292..25655300 100644 --- a/lib/savon/options.rb +++ b/lib/savon/options.rb @@ -84,8 +84,8 @@ def initialize(options = {}) :raise_errors => true, :strip_namespaces => true, :delete_namespace_attributes => false, - :convert_response_tags_to => lambda { |tag| tag.snakecase.to_sym}, - :convert_attributes_to => lambda { |k,v| [k,v] }, + :convert_response_tags_to => lambda { |tag| tag.snakecase.to_sym }, + :convert_attributes_to => lambda { |k, v| [k, v] }, :multipart => false, :adapter => nil, :use_wsa_headers => false, @@ -197,13 +197,13 @@ def raise_errors(raise_errors) # Whether or not to log. def log(log) - HTTPI.log = log + HTTPI.log = log @options[:log] = log end # The logger to use. Defaults to a Savon::Logger instance. def logger(logger) - HTTPI.logger = logger + HTTPI.logger = logger @options[:logger] = logger end @@ -477,6 +477,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 2e7a1bea..d4aa5035 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 7ca8292013729b0b6ebb6079a1b341c0b977da4c 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/3] Allow to pass already configured Nori instance and not mess Nori options with Savon options --- lib/savon/options.rb | 6 +++--- lib/savon/response.rb | 23 +++++++++-------------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/lib/savon/options.rb b/lib/savon/options.rb index 25655300..f6f5619c 100644 --- a/lib/savon/options.rb +++ b/lib/savon/options.rb @@ -477,9 +477,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 d4aa5035..a610515a 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