diff --git a/lib/soap/element.rb b/lib/soap/element.rb index 9e6a6a9c..ca1c6dcc 100644 --- a/lib/soap/element.rb +++ b/lib/soap/element.rb @@ -9,6 +9,7 @@ require 'xsd/qname' require 'soap/baseData' +require 'soap/soapversion' module SOAP @@ -92,14 +93,148 @@ def encode(generator, ns, attrs = {}) end +# The SOAP 1.2 fault shape (env:Fault > Code{Value,Subcode?} + +# Reason{Text}+ + Node? + Role? + Detail?) is structurally different +# enough from SOAP 1.1's flat faultcode/faultstring/faultactor/detail +# (SOAPFault above, left completely untouched) that it gets its own +# class rather than trying to make one class cover both shapes. +# +# Code/Subcode/Reason and their leaf children (Value/Text/Role) are built +# as SOAPElement instances rather than SOAPStruct/SOAPQName: SOAPElement +# defaults its own encodingstyle to LiteralNamespace, which routes +# encoding through LiteralHandler instead of the RPC/Encoded-style +# SOAPHandler -- avoiding a pile of xsi:type/encodingStyle attributes and +# stray namespace declarations that SOAP 1.2 fault content, being plain +# structural XML rather than SOAP-encoded data, has no business carrying. +# SOAPElement's text-content handling also already special-cases QName +# values (used for Value's content, e.g. FaultCode12::Sender) by +# resolving them against the current namespace-to-prefix mapping at +# encode time -- exactly what's needed here, and not something +# SOAPQName's generic path does on its own. +# +# No new decode-dispatch wiring needed despite the nesting: both +# SOAPHandler's and LiteralHandler's decode_parent key struct/element +# children purely by local name (node.elename.name), never by namespace, +# so Code/Subcode/Reason/etc. decode through the exact same generic +# machinery already used for all other body content. +class SOAP12Fault < SOAPStruct + include SOAPEnvelopeElement + include SOAPCompoundtype + +public + + # Standard fault-code Values (Sender/Receiver/MustUnderstand/ + # VersionMismatch) always live in the SOAP 1.2 envelope namespace per + # spec, so those resolve back to a real XSD::QName even after a decode + # round-trip, where the wire value is just a prefixed string ("env: + # Sender") with no automatic QName-content resolution in the generic + # literal-style decode path this reuses (a real, pre-existing gap in + # this codebase, not something specific to fault codes -- no QName- + # typed element content resolves its prefix back to a namespace + # anywhere today without an explicit type-driven decode path). Custom + # Subcodes can live in arbitrary app namespaces, so code_subcode can't + # apply the same trick; it returns the resolved QName only when never + # serialized (same-process construction), and the raw wire string + # ("n1:BadArgument") after a real decode. + STANDARD_FAULT_CODE_LOCAL_NAMES = { + 'Sender' => FaultCode12::Sender, + 'Receiver' => FaultCode12::Receiver, + 'MustUnderstand' => FaultCode12::MustUnderstand, + 'VersionMismatch' => FaultCode12::VersionMismatch + } + + def code_value + v = self['Code'] && self['Code']['Value'] + return nil unless v + data = v.data + return data unless data.is_a?(String) + STANDARD_FAULT_CODE_LOCAL_NAMES[data.sub(/\A[^:]+:/, '')] || data + end + + def code_subcode + v = self['Code'] && self['Code']['Subcode'] && self['Code']['Subcode']['Value'] + v && v.data + end + + def reason_text + v = self['Reason'] && self['Reason']['Text'] + v && v.data + end + + def node + v = self['Node'] + v && v.data + end + + def role + v = self['Role'] + v && v.data + end + + def detail + self['Detail'] + end + + def detail=(rhs) + self['Detail'] = rhs + end + + # code_value/code_subcode are XSD::QName (see FaultCode12::Sender etc); + # reason_text/node/role are plain strings; detail is any SOAP/OM node. + def initialize(code_value = nil, reason_text = nil, code_subcode = nil, + role = nil, detail = nil, reason_lang = 'en') + ns_uri = SOAPVersion1_2.envelope_namespace + super(XSD::QName.new(ns_uri, EleFault)) + @elename = XSD::QName.new(ns_uri, EleFault) + @encodingstyle = LiteralNamespace + return unless code_value + + code = SOAPElement.new(XSD::QName.new(ns_uri, 'Code')) + code.add(SOAPElement.new(XSD::QName.new(ns_uri, 'Value'), code_value)) + if code_subcode + subcode = SOAPElement.new(XSD::QName.new(ns_uri, 'Subcode')) + subcode.add(SOAPElement.new(XSD::QName.new(ns_uri, 'Value'), code_subcode)) + code.add(subcode) + end + self.add('Code', code) + + reason = SOAPElement.new(XSD::QName.new(ns_uri, 'Reason')) + text = SOAPElement.new(XSD::QName.new(ns_uri, 'Text'), reason_text) + text.extraattr[XSD::QName.new(XSD::NS::Namespace, 'lang')] = reason_lang + reason.add(text) + self.add('Reason', reason) + + if role + self.add('Role', SOAPElement.new(XSD::QName.new(ns_uri, 'Role'), role)) + end + if detail + detail.elename = XSD::QName.new(ns_uri, 'Detail') if detail.respond_to?(:elename=) + self.add('Detail', detail) + end + end + + def encode(generator, ns, attrs = {}) + Generator.assign_ns(attrs, ns, SOAPVersion1_2.envelope_namespace) + name = ns.name(@elename) + generator.encode_tag(name, attrs) + yield(self['Code']) + yield(self['Reason']) + yield(self['Node']) if self['Node'] + yield(self['Role']) if self['Role'] + yield(self['Detail']) if self['Detail'] + generator.encode_tag_end(name, true) + end +end + + class SOAPBody < SOAPStruct include SOAPEnvelopeElement attr_reader :is_fault - def initialize(data = nil, is_fault = false) + def initialize(data = nil, is_fault = false, soap_version = SOAPVersion1_1) super(nil) - @elename = EleBodyName + @elename = XSD::QName.new(soap_version.envelope_namespace, EleBody) @encodingstyle = nil if data if data.respond_to?(:to_xmlpart) @@ -154,14 +289,18 @@ class SOAPHeaderItem < XSD::NSDBase attr_accessor :mustunderstand attr_accessor :encodingstyle attr_accessor :actor + attr_accessor :relay - def initialize(element, mustunderstand = true, encodingstyle = nil, actor = nil) + def initialize(element, mustunderstand = true, encodingstyle = nil, actor = nil, + relay = nil, soap_version = SOAPVersion1_1) super() @type = nil @element = element @mustunderstand = mustunderstand @encodingstyle = encodingstyle @actor = actor + @relay = relay + @soap_version = soap_version element.parent = self if element element.qualified = true end @@ -172,7 +311,8 @@ def encode(generator, ns, attrs = {}) end # to remove mustUnderstand attribute, set it to nil unless @mustunderstand.nil? - @element.extraattr[AttrMustUnderstandName] = (@mustunderstand ? '1' : '0') + @element.extraattr[@soap_version.mustunderstand_attr_name] = + (@mustunderstand ? '1' : '0') end if @encodingstyle @element.extraattr[AttrEncodingStyleName] = @encodingstyle @@ -181,7 +321,12 @@ def encode(generator, ns, attrs = {}) @element.encodingstyle = @encodingstyle end if @actor - @element.extraattr[AttrActorName] = @actor + @element.extraattr[@soap_version.role_attr_name] = @actor + end + # relay has no SOAP 1.1 equivalent -- @soap_version.relay_attr_name is + # nil there, so this is a no-op unless actually running under 1.2. + unless @relay.nil? || @soap_version.relay_attr_name.nil? + @element.extraattr[@soap_version.relay_attr_name] = (@relay ? 'true' : 'false') end yield(@element) end @@ -192,12 +337,14 @@ class SOAPHeader < SOAPStruct include SOAPEnvelopeElement attr_writer :force_encode + attr_reader :soap_version - def initialize + def initialize(soap_version = SOAPVersion1_1) super(nil) - @elename = EleHeaderName + @elename = XSD::QName.new(soap_version.envelope_namespace, EleHeader) @encodingstyle = nil @force_encode = false + @soap_version = soap_version end def encode(generator, ns, attrs = {}) @@ -210,12 +357,14 @@ def encode(generator, ns, attrs = {}) end def add(name, value) - actor = value.extraattr[AttrActorName] - mu = value.extraattr[AttrMustUnderstandName] + actor = value.extraattr[@soap_version.role_attr_name] + mu = value.extraattr[@soap_version.mustunderstand_attr_name] encstyle = value.extraattr[AttrEncodingStyleName] - mu_value = mu.nil? ? nil : (mu == '1') + mu_value = mu.nil? ? nil : (mu == '1' || mu == 'true') + relay = @soap_version.relay_attr_name && value.extraattr[@soap_version.relay_attr_name] + relay_value = relay.nil? ? nil : (relay == 'true' || relay == '1') # to remove mustUnderstand attribute, set it to nil - item = SOAPHeaderItem.new(value, mu_value, encstyle, actor) + item = SOAPHeaderItem.new(value, mu_value, encstyle, actor, relay_value, @soap_version) super(name, item) end @@ -238,10 +387,10 @@ class SOAPEnvelope < XSD::NSDBase attr_reader :body attr_reader :external_content - def initialize(header = nil, body = nil) + def initialize(header = nil, body = nil, soap_version = SOAPVersion1_1) super() @type = nil - @elename = EleEnvelopeName + @elename = XSD::QName.new(soap_version.envelope_namespace, EleEnvelope) @encodingstyle = nil @header = header @body = body diff --git a/lib/soap/mimemessage.rb b/lib/soap/mimemessage.rb index 79516b4c..c128ae7b 100644 --- a/lib/soap/mimemessage.rb +++ b/lib/soap/mimemessage.rb @@ -8,6 +8,7 @@ require 'soap/attachment' +require 'soap/soapversion' module SOAP @@ -147,11 +148,17 @@ def to_s end end - def initialize + # soap_version governs the media type this message's parts declare + # (root part and the outer multipart's own "type" parameter) -- defaults + # to SOAPVersion1_1 so parsing an incoming message (which doesn't care + # what media type the parts claim; see #root) and any other caller that + # never had a version to give aren't affected. + def initialize(soap_version = SOAPVersion1_1) @parts = [] @headers = Headers.new @root = nil @boundary = nil + @soap_version = soap_version end def self.parse(head, str) @@ -163,7 +170,7 @@ def self.parse(head, str) def close @headers.add( "Content-Type", - "multipart/related; type=\"text/xml\"; boundary=\"#{boundary}\"; start=\"#{@parts[0].contentid}\"" + "multipart/related; type=\"#{@soap_version.media_type}\"; boundary=\"#{boundary}\"; start=\"#{@parts[0].contentid}\"" ) end @@ -202,7 +209,7 @@ def boundary def add_part(content) part = Part.new part.headers.add("Content-Type", - "text/xml; charset=" + XSD::Charset.xml_encoding_label) + "#{@soap_version.media_type}; charset=" + XSD::Charset.xml_encoding_label) part.headers.add("Content-ID", Attachment.contentid(part)) part.body = content @parts.unshift(part) diff --git a/lib/soap/ns.rb b/lib/soap/ns.rb index 3b1db4e3..f80e595d 100644 --- a/lib/soap/ns.rb +++ b/lib/soap/ns.rb @@ -10,6 +10,7 @@ require 'xsd/datatypes' require 'xsd/ns' require 'soap/soap' +require 'soap/soapversion' module SOAP @@ -21,7 +22,8 @@ module SOAP class NS < XSD::NS KNOWN_TAG = XSD::NS::KNOWN_TAG.dup.update( - SOAP::EnvelopeNamespace => 'env' + SOAP::EnvelopeNamespace => 'env', + SOAP::SOAPVersion1_2.envelope_namespace => 'env' ) def initialize(tag2ns = nil) diff --git a/lib/soap/parser.rb b/lib/soap/parser.rb index e635a732..5c668443 100644 --- a/lib/soap/parser.rb +++ b/lib/soap/parser.rb @@ -9,6 +9,7 @@ require 'xsd/xmlparser' require 'soap/soap' +require 'soap/soapversion' require 'soap/ns' require 'soap/baseData' require 'soap/encodingstyle/handler' @@ -74,6 +75,7 @@ def update(ns, name, node, encodingstyle, handler) attr_accessor :default_encodingstyle attr_accessor :decode_typemap attr_accessor :allow_unqualified_element + attr_accessor :soap_version def initialize(opt = {}) @opt = opt @@ -82,7 +84,9 @@ def initialize(opt = {}) @recycleframe = nil @lastnode = nil @handlers = {} - @envelopenamespace = opt[:envelopenamespace] || EnvelopeNamespace + @soap_version = opt[:soap_version] || SOAPVersion1_1 + @envelopenamespace = + opt[:envelopenamespace] || @soap_version.envelope_namespace @default_encodingstyle = opt[:default_encodingstyle] || EncodingNamespace @decode_typemap = opt[:decode_typemap] || nil @allow_unqualified_element = opt[:allow_unqualified_element] || false @@ -202,7 +206,7 @@ def decode_text(ns, text, handler) def decode_soap_envelope(ns, ele, attrs, parent) o = nil if ele.name == EleEnvelope - o = SOAPEnvelope.new + o = SOAPEnvelope.new(nil, nil, @soap_version) if ext = @opt[:external_content] ext.each do |k, v| o.external_content[k] = v @@ -210,23 +214,24 @@ def decode_soap_envelope(ns, ele, attrs, parent) end elsif ele.name == EleHeader return nil unless parent.node.is_a?(SOAPEnvelope) - o = SOAPHeader.new + o = SOAPHeader.new(@soap_version) parent.node.header = o elsif ele.name == EleBody return nil unless parent.node.is_a?(SOAPEnvelope) - o = SOAPBody.new + o = SOAPBody.new(nil, false, @soap_version) parent.node.body = o elsif ele.name == EleFault + fault_class = (@soap_version == SOAPVersion1_2) ? SOAP12Fault : SOAPFault if parent.node.is_a?(SOAPBody) - o = SOAPFault.new + o = fault_class.new parent.node.fault = o elsif parent.node.is_a?(SOAPEnvelope) # live.com server returns SOAPFault as a direct child of SOAPEnvelope. # support it even if it's not spec compliant. warn("Fault must be a child of Body.") - body = SOAPBody.new + body = SOAPBody.new(nil, false, @soap_version) parent.node.body = body - o = SOAPFault.new + o = fault_class.new body.fault = o else return nil diff --git a/lib/soap/rpc/driver.rb b/lib/soap/rpc/driver.rb index dabd8c20..30e1b46f 100644 --- a/lib/soap/rpc/driver.rb +++ b/lib/soap/rpc/driver.rb @@ -30,6 +30,7 @@ class Driver attr_proxy :literal_mapping_registry, true attr_proxy :allow_unqualified_element, true attr_proxy :default_encodingstyle, true + attr_proxy :soap_version, true attr_proxy :generate_explicit_type, true attr_proxy :use_default_namespace, true attr_proxy :return_response_as_xml, true @@ -165,7 +166,7 @@ def set_wiredump_file_base(name) end def create_header(headers) - header = SOAPHeader.new() + header = SOAPHeader.new(@proxy.soap_version) headers.each do |content, mustunderstand, encodingstyle| header.add(SOAPHeaderItem.new(content, mustunderstand, encodingstyle)) end diff --git a/lib/soap/rpc/httpserver.rb b/lib/soap/rpc/httpserver.rb index b5123a38..9df38723 100644 --- a/lib/soap/rpc/httpserver.rb +++ b/lib/soap/rpc/httpserver.rb @@ -29,6 +29,7 @@ class HTTPServer < Logger::Application attr_proxy :literal_mapping_registry, true attr_proxy :generate_explicit_type, true attr_proxy :use_default_namespace, true + attr_proxy :soap_version, true def initialize(config) actor = config[:SOAPHTTPServerApplicationName] || self.class.name diff --git a/lib/soap/rpc/proxy.rb b/lib/soap/rpc/proxy.rb index 4d14cd2b..6a13d7d4 100644 --- a/lib/soap/rpc/proxy.rb +++ b/lib/soap/rpc/proxy.rb @@ -8,6 +8,7 @@ require 'soap/soap' +require 'soap/soapversion' require 'soap/processor' require 'soap/mapping' require 'soap/mapping/literalregistry' @@ -32,6 +33,7 @@ class Proxy attr_accessor :mandatorycharset attr_accessor :allow_unqualified_element attr_accessor :default_encodingstyle + attr_accessor :soap_version attr_accessor :generate_explicit_type attr_accessor :use_default_namespace attr_accessor :return_response_as_xml @@ -57,6 +59,7 @@ def initialize(endpoint_url, soapaction, options) # TODO: set to false by default or drop thie option in 1.6.0 @allow_unqualified_element = true @default_encodingstyle = nil + @soap_version = SOAPVersion1_1 @generate_explicit_type = nil @use_default_namespace = false @return_response_as_xml = false @@ -126,7 +129,8 @@ def call(name, *params) req_header = create_request_header req_body = SOAPBody.new( op_info.request_body(params, @mapping_registry, - @literal_mapping_registry, mapping_opt) + @literal_mapping_registry, mapping_opt), + false, @soap_version ) reqopt = create_encoding_opt( :soapaction => op_info.soapaction || @soapaction, @@ -167,14 +171,14 @@ def call(name, *params) end def route(req_header, req_body, reqopt, resopt) - req_env = ::SOAP::SOAPEnvelope.new(req_header, req_body) + req_env = ::SOAP::SOAPEnvelope.new(req_header, req_body, @soap_version) unless reqopt[:envelopenamespace].nil? set_envelopenamespace(req_env, reqopt[:envelopenamespace]) end reqopt[:external_content] = nil conn_data = marshal(req_env, reqopt) if ext = reqopt[:external_content] - mime = MIMEMessage.new + mime = MIMEMessage.new(@soap_version) ext.each do |k, v| mime.add_attachment(v.data) end @@ -182,8 +186,10 @@ def route(req_header, req_body, reqopt, resopt) mime.close conn_data.send_string = mime.content_str conn_data.send_contenttype = mime.headers['content-type'].str + conn_data.multipart = true end conn_data.soapaction = reqopt[:soapaction] + conn_data.soap_version = @soap_version conn_data = @streamhandler.send(@endpoint_url, conn_data) if conn_data.receive_string.empty? return nil @@ -243,7 +249,7 @@ def set_envelopenamespace(env, namespace) end def create_request_header - header = ::SOAP::SOAPHeader.new + header = ::SOAP::SOAPHeader.new(@soap_version) items = @headerhandler.on_outbound(header) items.each do |item| header.add(item.elename.name, item) @@ -304,6 +310,7 @@ def create_encoding_opt(hash = nil) opt[:default_encodingstyle] = @default_encodingstyle opt[:allow_unqualified_element] = @allow_unqualified_element opt[:generate_explicit_type] = @generate_explicit_type + opt[:soap_version] = @soap_version opt[:no_indent] = @options["soap.envelope.no_indent"] opt[:use_numeric_character_reference] = @options["soap.envelope.use_numeric_character_reference"] diff --git a/lib/soap/rpc/router.rb b/lib/soap/rpc/router.rb index 40447285..b310a831 100644 --- a/lib/soap/rpc/router.rb +++ b/lib/soap/rpc/router.rb @@ -8,6 +8,7 @@ require 'soap/soap' +require 'soap/soapversion' require 'soap/processor' require 'soap/mapping' require 'soap/mapping/literalregistry' @@ -33,6 +34,7 @@ class Router attr_accessor :generate_explicit_type attr_accessor :use_default_namespace attr_accessor :external_ces + attr_accessor :soap_version attr_reader :filterchain def initialize(actor) @@ -43,6 +45,7 @@ def initialize(actor) @generate_explicit_type = true @use_default_namespace = false @external_ces = nil + @soap_version = SOAPVersion1_1 @operation_by_soapaction = {} @operation_by_qname = {} @headerhandlerfactory = [] @@ -191,15 +194,16 @@ def route(conn_data) conn_data.is_nocontent = true conn_data else - body = SOAPBody.new(soap_response, conn_data.is_fault) - env = SOAPEnvelope.new(header, body) + body = SOAPBody.new(soap_response, conn_data.is_fault, @soap_version) + env = SOAPEnvelope.new(header, body, @soap_version) marshal(conn_data, env, default_encodingstyle) end end # Create fault response string. def create_fault_response(e) - env = SOAPEnvelope.new(SOAPHeader.new, SOAPBody.new(fault(e, nil), true)) + env = SOAPEnvelope.new(SOAPHeader.new(@soap_version), + SOAPBody.new(fault(e, nil), true, @soap_version), @soap_version) opt = {} opt[:external_content] = nil @filterchain.reverse_each do |filter| @@ -277,7 +281,7 @@ def lookup_operation(soapaction, body) end def call_headers(headerhandler) - header = ::SOAP::SOAPHeader.new + header = ::SOAP::SOAPHeader.new(@soap_version) items = headerhandler.on_outbound(header) items.each do |item| header.add(item.elename.name, item) @@ -292,6 +296,7 @@ def receive_headers(headerhandler, header) def unmarshal(conn_data) xml = nil opt = {} + opt[:soap_version] = @soap_version contenttype = conn_data.receive_contenttype if /#{MIMEMessage::MultipartContentType}/i =~ contenttype opt[:external_content] = {} @@ -316,7 +321,7 @@ def unmarshal(conn_data) end env = Processor.unmarshal(xml, opt) charset = opt[:charset] - conn_data.send_contenttype = "text/xml; charset=\"#{charset}\"" + conn_data.send_contenttype = @soap_version.build_content_type(charset) env end @@ -324,6 +329,7 @@ def marshal(conn_data, env, default_encodingstyle = nil) opt = {} opt[:external_content] = nil opt[:default_encodingstyle] = default_encodingstyle + opt[:soap_version] = @soap_version opt[:generate_explicit_type] = @generate_explicit_type opt[:use_default_namespace] = @use_default_namespace @filterchain.reverse_each do |filter| @@ -339,7 +345,7 @@ def marshal(conn_data, env, default_encodingstyle = nil) end def mimeize(conn_data, ext) - mime = MIMEMessage.new + mime = MIMEMessage.new(@soap_version) ext.each do |k, v| mime.add_attachment(v.data) end @@ -352,10 +358,18 @@ def mimeize(conn_data, ext) # Create fault response. def fault(e, wsdl_fault_details) - if e.is_a?(UnhandledMustUnderstandHeaderError) - faultcode = FaultCode::MustUnderstand + if @soap_version == SOAPVersion1_2 + if e.is_a?(UnhandledMustUnderstandHeaderError) + faultcode = FaultCode12::MustUnderstand + else + faultcode = FaultCode12::Receiver + end else - faultcode = FaultCode::Server + if e.is_a?(UnhandledMustUnderstandHeaderError) + faultcode = FaultCode::MustUnderstand + else + faultcode = FaultCode::Server + end end # If the exception represents a WSDL fault, the fault element should @@ -385,11 +399,15 @@ def fault(e, wsdl_fault_details) detail = SOAPString.new("failed to serialize detail object: #{$!}") end - SOAPFault.new( - SOAPElement.new(nil, faultcode), - SOAPString.new(e.to_s), - SOAPString.new(@actor), - detail) + if @soap_version == SOAPVersion1_2 + SOAP12Fault.new(faultcode, e.to_s, nil, @actor, detail) + else + SOAPFault.new( + SOAPElement.new(nil, faultcode), + SOAPString.new(e.to_s), + SOAPString.new(@actor), + detail) + end end def create_mapping_opt diff --git a/lib/soap/rpc/soaplet.rb b/lib/soap/rpc/soaplet.rb index 0a5deb21..a0ce6b06 100644 --- a/lib/soap/rpc/soaplet.rb +++ b/lib/soap/rpc/soaplet.rb @@ -141,6 +141,14 @@ def setup_req(conn_data, req) conn_data.receive_string = req.body conn_data.receive_contenttype = req['content-type'] conn_data.soapaction = parse_soapaction(req.meta_vars['HTTP_SOAPACTION']) + # SOAP 1.2 has no separate SOAPAction header -- the action travels as + # a Content-Type parameter instead. Only consulted when the header is + # absent, so this is a pure fallback: 1.1 traffic (which always sends + # the header) is completely unaffected. + if conn_data.soapaction.nil? + conn_data.soapaction = + StreamHandler.parse_action_from_content_type(conn_data.receive_contenttype) + end end def setup_res(conn_data, req, res) diff --git a/lib/soap/soap.rb b/lib/soap/soap.rb index 4bf3c7fc..5d63f81f 100644 --- a/lib/soap/soap.rb +++ b/lib/soap/soap.rb @@ -99,17 +99,34 @@ class FaultError < Error attr_reader :faultactor attr_accessor :detail + # fault is either a SOAPFault (1.1's flat faultcode/faultstring/ + # faultactor/detail) or a SOAP12Fault (1.2's nested Code/Reason/Role/ + # Detail) -- exposed through this same set of reader names either way, + # since callers already rely on this shape and the two protocols' + # underlying fault semantics don't need identical Ruby types to be + # usable, just a reasonable value for each. code_value/reason_text/role + # are already the natural Ruby values (QName-or-string/string/string); + # faultcode/faultactor for 1.1 stay exactly as before (whatever + # SOAPFault#faultcode/#faultactor already returned). def initialize(fault) - @faultcode = fault.faultcode - @faultstring = fault.faultstring - @faultactor = fault.faultactor + if fault.is_a?(SOAP12Fault) + @faultcode = fault.code_value + @faultstring = fault.reason_text + @faultactor = fault.role + else + @faultcode = fault.faultcode + @faultstring = fault.faultstring + @faultactor = fault.faultactor + end @detail = fault.detail super(self.to_s) end def to_s str = nil - if @faultstring and @faultstring.respond_to?('data') + if @faultstring.is_a?(String) + str = @faultstring + elsif @faultstring and @faultstring.respond_to?('data') str = @faultstring.data end str || '(No faultstring)' diff --git a/lib/soap/soapversion.rb b/lib/soap/soapversion.rb new file mode 100644 index 00000000..d6f03778 --- /dev/null +++ b/lib/soap/soapversion.rb @@ -0,0 +1,87 @@ +# encoding: UTF-8 +# SOAP4R - SOAP protocol version bundle (1.1 vs 1.2). +# Copyright (C) 2000-2007 NAKAMURA, Hiroshi . + +# This program is copyrighted free software by NAKAMURA, Hiroshi. You can +# redistribute it and/or modify it under the same terms of Ruby's license; +# either the dual license version in 2003, or any later version. + + +require 'soap/soap' + + +module SOAP + + +# A SOAPVersion bundles the constants that differ between SOAP 1.1 and 1.2 +# (envelope namespace, and -- as later phases land -- encoding namespace, +# media type, fault codes, header role attribute, etc). It's resolved once +# per call, the same place @default_encodingstyle already is (Proxy/Router's +# opt hashes), never inferred from parsed content: SOAP version is a +# whole-message, driver-configured-in-advance choice, not a per-element, +# reactively-discovered one like encodingStyle. +class SOAPVersion + attr_reader :envelope_namespace + # The attribute a header block uses to address its intended recipient: + # SOAP 1.1 calls this "actor", SOAP 1.2 renamed it "role" and gave it + # slightly different semantics (a URI identifying a SOAP role rather than + # a specific node) -- both are exposed through this one field name since + # from the header-encoding machinery's point of view they serve the same + # structural purpose. + attr_reader :role_attr_name + # SOAP 1.2-only; nil under 1.1, since 1.1 has no equivalent concept. + attr_reader :relay_attr_name + attr_reader :mustunderstand_attr_name + attr_reader :media_type + # SOAP 1.2's HTTP binding folds the SOAPAction value into a Content-Type + # "action" parameter and drops the separate SOAPAction header entirely; + # 1.1 keeps them as two separate things. false under 1.1, true under 1.2. + attr_reader :action_in_content_type + + # Plain positional args with defaults, not Ruby keyword args (`x:`/`x: 1`) + # -- this project supports Ruby back to 1.8.7/1.9.3, which predate + # keyword-argument syntax entirely. + def initialize(envelope_namespace, role_attr_local_name, relay_attr_local_name = nil, + media_type = MediaType, action_in_content_type = false) + @envelope_namespace = envelope_namespace + @role_attr_name = XSD::QName.new(envelope_namespace, role_attr_local_name) + @relay_attr_name = + if relay_attr_local_name + XSD::QName.new(envelope_namespace, relay_attr_local_name) + end + @mustunderstand_attr_name = XSD::QName.new(envelope_namespace, 'mustUnderstand') + @media_type = media_type + @action_in_content_type = action_in_content_type + end + + # soapaction is only ever embedded when action_in_content_type is true + # (1.2) AND a soapaction is actually given -- under 1.1 (or when no + # soapaction applies, e.g. a response) this is just "; + # charset=", identical to what StreamHandler.create_media_type + # already built before this existed. + def build_content_type(charset, soapaction = nil) + ct = "#{@media_type}; charset=#{charset}" + if @action_in_content_type && soapaction + ct += %Q{; action="#{soapaction}"} + end + ct + end +end + +SOAPVersion1_1 = SOAPVersion.new(EnvelopeNamespace, 'actor') +SOAPVersion1_2 = SOAPVersion.new('http://www.w3.org/2003/05/soap-envelope', 'role', 'relay', + 'application/soap+xml', true) + +# SOAP 1.2's env:Fault/env:Code/env:Value content is a QName identifying +# the fault code; 1.1's Client/Server (SOAP::FaultCode in soap.rb) are +# renamed Sender/Receiver here, same VersionMismatch/MustUnderstand +# concepts otherwise. +module FaultCode12 + VersionMismatch = XSD::QName.new(SOAPVersion1_2.envelope_namespace, 'VersionMismatch').freeze + MustUnderstand = XSD::QName.new(SOAPVersion1_2.envelope_namespace, 'MustUnderstand').freeze + Sender = XSD::QName.new(SOAPVersion1_2.envelope_namespace, 'Sender').freeze + Receiver = XSD::QName.new(SOAPVersion1_2.envelope_namespace, 'Receiver').freeze +end + + +end diff --git a/lib/soap/streamHandler.rb b/lib/soap/streamHandler.rb index 7684e3c8..92c7c942 100644 --- a/lib/soap/streamHandler.rb +++ b/lib/soap/streamHandler.rb @@ -8,6 +8,7 @@ require 'soap/soap' +require 'soap/soapversion' require 'soap/httpconfigloader' require 'soap/httpbackend' require 'soap/filter/filterchain' @@ -35,6 +36,18 @@ class ConnectionData attr_accessor :is_fault attr_accessor :is_nocontent attr_accessor :soapaction + attr_accessor :soap_version + # True when send_string/send_contenttype are a MIME multipart message + # (SOAP-with-Attachments) -- send_post uses this to still emit the + # legacy SOAPAction header even under SOAP 1.2, where it's otherwise + # folded into the Content-Type's action parameter instead. That + # folding isn't attempted for the multipart case at all (see + # MIMEMessage): action placement for attachments+1.2 combined isn't + # settled by any spec this was checked against, and real-world use of + # that combination is rare enough that falling back to the + # widely-supported legacy header is the pragmatic choice, not a gap + # left to fix later. + attr_accessor :multipart def initialize(send_string = nil) @send_string = send_string @@ -44,6 +57,8 @@ def initialize(send_string = nil) @is_fault = false @is_nocontent = false @soapaction = nil + @soap_version = nil + @multipart = false end end @@ -51,17 +66,36 @@ def initialize @filterchain = Filter::FilterChain.new end + # Recognizes both SOAP 1.1's media type (text/xml) and 1.2's + # (application/soap+xml). Unlike the original 1.1-only version, this + # isn't fully anchored to the end of the string: a 1.2 Content-Type can + # carry an additional action="..." parameter alongside charset, in + # either order, which parse_action_from_content_type below extracts + # separately. def self.parse_media_type(str) - if /^#{ MediaType }(?:\s*;\s*charset=([^"]+|"[^"]+"))?$/i !~ str - return nil + return nil unless str + return nil unless /^(?:#{Regexp.escape(MediaType)}|application\/soap\+xml)\b/i =~ str + if /charset=("[^"]*"|[^;\s]+)/i =~ str + charset = $1 + charset = charset.gsub(/"/, '') end - charset = $1 - charset.gsub!(/"/, '') if charset charset || 'utf-8' end - def self.create_media_type(charset) - "#{ MediaType }; charset=#{ charset }" + # SOAP 1.2 only: the Content-Type "action" parameter that replaces the + # 1.1 SOAPAction header. nil if absent (1.1 traffic, or a 1.2 request + # with no action). + def self.parse_action_from_content_type(str) + return nil unless str + if /action=("[^"]*"|[^;\s]+)/i =~ str + $1.gsub(/"/, '') + end + end + + def self.create_media_type(charset, media_type = MediaType, action = nil) + ct = "#{ media_type }; charset=#{ charset }" + ct += %Q{; action="#{ action }"} if action + ct end def send(url, conn_data, soapaction = nil, charset = nil) @@ -201,7 +235,10 @@ def set_cookie_store_file(value) end def send_post(url, conn_data, charset) - conn_data.send_contenttype ||= StreamHandler.create_media_type(charset) + soap_version = conn_data.soap_version || SOAPVersion1_1 + conn_data.send_contenttype ||= + StreamHandler.create_media_type(charset, soap_version.media_type, + soap_version.action_in_content_type ? conn_data.soapaction : nil) if @wiredump_file_base filename = @wiredump_file_base + '_request.xml' @@ -212,7 +249,11 @@ def send_post(url, conn_data, charset) extheader = {} extheader['Content-Type'] = conn_data.send_contenttype - extheader['SOAPAction'] = "\"#{ conn_data.soapaction }\"" + # See ConnectionData#multipart's comment -- attachments always get the + # legacy header too, regardless of version. + if !soap_version.action_in_content_type || conn_data.multipart + extheader['SOAPAction'] = "\"#{ conn_data.soapaction }\"" + end extheader['Accept-Encoding'] = 'gzip' if send_accept_encoding_gzip? send_string = conn_data.send_string @wiredump_dev << "Wire dump:\n\n" if @wiredump_dev diff --git a/lib/soap/wsdlDriver.rb b/lib/soap/wsdlDriver.rb index be91e857..2c21ed4f 100644 --- a/lib/soap/wsdlDriver.rb +++ b/lib/soap/wsdlDriver.rb @@ -113,6 +113,9 @@ def find_port(servicename = nil, portname = nil) end def init_driver(drv, binding) + if binding.soapbinding && binding.soapbinding.soap12 + drv.soap_version = ::SOAP::SOAPVersion1_2 + end wsdl_elements = @wsdl.collect_elements wsdl_types = @wsdl.collect_complextypes + @wsdl.collect_simpletypes rpc_decode_typemap = wsdl_types + diff --git a/lib/wsdl/binding.rb b/lib/wsdl/binding.rb index b502641f..8f2f94e3 100644 --- a/lib/wsdl/binding.rb +++ b/lib/wsdl/binding.rb @@ -38,8 +38,9 @@ def parse_element(element) o = OperationBinding.new @operations << o o - when SOAPBindingName + when SOAPBindingName, SOAP12BindingName o = WSDL::SOAP::Binding.new + o.soap12 = (element == SOAP12BindingName) @soapbinding = o o when DocumentationName diff --git a/lib/wsdl/data.rb b/lib/wsdl/data.rb index c7cc9eda..829010d1 100644 --- a/lib/wsdl/data.rb +++ b/lib/wsdl/data.rb @@ -51,6 +51,19 @@ module WSDL SOAPFaultName = XSD::QName.new(SOAPBindingNamespace, 'fault') SOAPOperationName = XSD::QName.new(SOAPBindingNamespace, 'operation') +# SOAP 1.2 WSDL binding extension elements -- same local names, different +# namespace. Routed to the exact same WSDL::SOAP::* classes as their 1.1 +# counterparts at every dispatch point (binding.rb, operationBinding.rb, +# param.rb, port.rb, service.rb, soap/header.rb): only the recognized +# input namespace differs, the object model doesn't need to know or care +# which SOAP version bound a given WSDL operation. +SOAP12AddressName = XSD::QName.new(SOAP12BindingNamespace, 'address') +SOAP12BindingName = XSD::QName.new(SOAP12BindingNamespace, 'binding') +SOAP12HeaderName = XSD::QName.new(SOAP12BindingNamespace, 'header') +SOAP12BodyName = XSD::QName.new(SOAP12BindingNamespace, 'body') +SOAP12FaultName = XSD::QName.new(SOAP12BindingNamespace, 'fault') +SOAP12OperationName = XSD::QName.new(SOAP12BindingNamespace, 'operation') + BindingAttrName = XSD::QName.new(nil, 'binding') ElementAttrName = XSD::QName.new(nil, 'element') LocationAttrName = XSD::QName.new(nil, 'location') diff --git a/lib/wsdl/operationBinding.rb b/lib/wsdl/operationBinding.rb index fe2df5f0..11da42ff 100644 --- a/lib/wsdl/operationBinding.rb +++ b/lib/wsdl/operationBinding.rb @@ -177,7 +177,7 @@ def parse_element(element) o = Param.new @fault << o o - when SOAPOperationName + when SOAPOperationName, SOAP12OperationName o = WSDL::SOAP::Operation.new @soapoperation = o o diff --git a/lib/wsdl/param.rb b/lib/wsdl/param.rb index 4736cd4f..a98208b9 100644 --- a/lib/wsdl/param.rb +++ b/lib/wsdl/param.rb @@ -55,15 +55,15 @@ def soapbody_encodingstyle def parse_element(element) case element - when SOAPBodyName + when SOAPBodyName, SOAP12BodyName o = WSDL::SOAP::Body.new @soapbody = o o - when SOAPHeaderName + when SOAPHeaderName, SOAP12HeaderName o = WSDL::SOAP::Header.new @soapheader << o o - when SOAPFaultName + when SOAPFaultName, SOAP12FaultName o = WSDL::SOAP::Fault.new @soapfault = o o diff --git a/lib/wsdl/port.rb b/lib/wsdl/port.rb index 79ca02c7..a6e2fe3e 100644 --- a/lib/wsdl/port.rb +++ b/lib/wsdl/port.rb @@ -39,7 +39,7 @@ def find_binding def parse_element(element) case element - when SOAPAddressName + when SOAPAddressName, SOAP12AddressName o = WSDL::SOAP::Address.new @soap_address = o o diff --git a/lib/wsdl/service.rb b/lib/wsdl/service.rb index d89026ef..044da964 100644 --- a/lib/wsdl/service.rb +++ b/lib/wsdl/service.rb @@ -36,7 +36,7 @@ def parse_element(element) o = Port.new @ports << o o - when SOAPAddressName + when SOAPAddressName, SOAP12AddressName o = WSDL::SOAP::Address.new @soap_address = o o diff --git a/lib/wsdl/soap/binding.rb b/lib/wsdl/soap/binding.rb index 1f679deb..250ba855 100644 --- a/lib/wsdl/soap/binding.rb +++ b/lib/wsdl/soap/binding.rb @@ -17,11 +17,18 @@ module SOAP class Binding < Info attr_reader :style attr_reader :transport + # Set by WSDL::Binding#parse_element (lib/wsdl/binding.rb) based on + # whether the or element matched -- + # this class itself is identical either way, only the WSDL author's + # chosen namespace differs, so that's the one place that actually knows + # which one it was. + attr_accessor :soap12 def initialize super @style = nil @transport = nil + @soap12 = false end def parse_element(element) diff --git a/lib/wsdl/soap/data.rb b/lib/wsdl/soap/data.rb index 9bcdedd0..b45e6b5d 100644 --- a/lib/wsdl/soap/data.rb +++ b/lib/wsdl/soap/data.rb @@ -25,6 +25,7 @@ module SOAP HeaderFaultName = XSD::QName.new(SOAPBindingNamespace, 'headerfault') +SOAP12HeaderFaultName = XSD::QName.new(SOAP12BindingNamespace, 'headerfault') LocationAttrName = XSD::QName.new(nil, 'location') StyleAttrName = XSD::QName.new(nil, 'style') diff --git a/lib/wsdl/soap/driverCreator.rb b/lib/wsdl/soap/driverCreator.rb index ca300bc4..16ecb60e 100644 --- a/lib/wsdl/soap/driverCreator.rb +++ b/lib/wsdl/soap/driverCreator.rb @@ -83,9 +83,12 @@ def dump_porttype(porttype) EOD wsdl_name = @definitions.name ? @definitions.name.name : 'default' mrname = safeconstname(wsdl_name + 'MappingRegistry') + soap_version_line = + binding.soapbinding.soap12 ? %Q[self.soap_version = ::SOAP::SOAPVersion1_2\n] : '' c.def_method("initialize", "endpoint_url = nil") do %Q[endpoint_url ||= DefaultEndpointUrl\n] + %Q[super(endpoint_url, nil)\n] + + soap_version_line + %Q[self.mapping_registry = #{mrname}::EncodedRegistry\n] + %Q[self.literal_mapping_registry = #{mrname}::LiteralRegistry\n] + %Q[init_methods] diff --git a/lib/wsdl/soap/header.rb b/lib/wsdl/soap/header.rb index 550dc12a..3bf9b89a 100644 --- a/lib/wsdl/soap/header.rb +++ b/lib/wsdl/soap/header.rb @@ -52,7 +52,7 @@ def find_part def parse_element(element) case element - when HeaderFaultName + when HeaderFaultName, SOAP12HeaderFaultName o = WSDL::SOAP::HeaderFault.new @headerfault = o o diff --git a/lib/wsdl/wsdl.rb b/lib/wsdl/wsdl.rb index 7e293751..2b32e0a4 100644 --- a/lib/wsdl/wsdl.rb +++ b/lib/wsdl/wsdl.rb @@ -18,6 +18,10 @@ module WSDL Namespace = 'http://schemas.xmlsoap.org/wsdl/' SOAPBindingNamespace ='http://schemas.xmlsoap.org/wsdl/soap/' +# From the "SOAP 1.2 Binding for WSDL 1.1" W3C submission, S2.1: the +# namespace implementations MUST use for soap12:binding/operation/body/ +# header/fault/address WSDL extension elements. +SOAP12BindingNamespace = 'http://schemas.xmlsoap.org/wsdl/soap12/' class Error < StandardError; include ::SOAP::NestedException; end diff --git a/test/soap/fault/test_soap12_fault_client.rb b/test/soap/fault/test_soap12_fault_client.rb new file mode 100644 index 00000000..394ae37d --- /dev/null +++ b/test/soap/fault/test_soap12_fault_client.rb @@ -0,0 +1,56 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/rpc/driver' +require 'soap/rpc/standaloneServer' + + +module SOAP +module Fault + + +# Phase 2 of SOAP 1.2 support, step 2 (continued): full client-side +# canned-envelope test mirroring test_fault.rb, but feeding a hand-built +# SOAP 1.2 fault envelope through a driver configured with +# soap_version = SOAPVersion1_2, confirming the raised FaultError carries +# the right message end to end (parser -> SOAP12Fault -> FaultError). +class TestSoap12FaultClient < Test::Unit::TestCase + def setup + @client = SOAP::RPC::Driver.new(nil, 'urn:fault') + @client.wiredump_dev = STDERR if $DEBUG + @client.soap_version = SOAP::SOAPVersion1_2 + @client.add_method("hello", "msg") + end + + def teardown + @client.reset_stream if @client + end + + def test_soap12_fault + @client.mapping_registry = SOAP::Mapping::EncodedRegistry.new + @client.test_loopback_response << <<__XML__ + + + + + env:Sender + + + DN cannot be empty + + + + +__XML__ + begin + @client.hello("world") + assert(false) + rescue ::SOAP::FaultError => e + assert_equal("DN cannot be empty", e.message) + assert_equal(SOAP::FaultCode12::Sender, e.faultcode) + end + end +end + + +end +end diff --git a/test/soap/fault/test_soap12_fault_router.rb b/test/soap/fault/test_soap12_fault_router.rb new file mode 100644 index 00000000..c2483ebc --- /dev/null +++ b/test/soap/fault/test_soap12_fault_router.rb @@ -0,0 +1,55 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/rpc/router' +require 'soap/mapping/mapping' +require 'soap/processor' + + +module SOAP +module Fault + + +# Phase 2 of SOAP 1.2 support, step 2: wire SOAP12Fault into +# Router#create_fault_response/#fault and FaultError. Direct port of +# test_soaparray.rb's pattern (router.create_fault_response($!) -> +# Processor.unmarshal -> FaultError.new -> Mapping.fault2exception), +# with router.soap_version = SOAPVersion1_2 -- asserting Sender/Receiver +# fault codes appear, not Client/Server, and that the whole chain still +# ends up re-raising the original exception correctly. +class TestSoap12FaultRouter < Test::Unit::TestCase + def test_parse_fault_soap12 + router = SOAP::RPC::Router.new('parse_soap12_error') + router.soap_version = SOAP::SOAPVersion1_2 + soap_fault = pump_stack rescue router.create_fault_response($!) + env = SOAP::Processor.unmarshal(soap_fault.send_string, :soap_version => SOAP::SOAPVersion1_2) + assert_kind_of(SOAP::SOAP12Fault, env.body.fault) + assert_equal(SOAP::FaultCode12::Receiver, env.body.fault.code_value) + + soap_fault = SOAP::FaultError.new(env.body.fault) + assert_raises(RuntimeError) do + registry = SOAP::Mapping::LiteralRegistry.new + SOAP::Mapping.fault2exception(soap_fault, registry) + end + end + + def test_soap11_fault_router_unaffected + router = SOAP::RPC::Router.new('parse_soap11_error') + soap_fault = pump_stack rescue router.create_fault_response($!) + env = SOAP::Processor.unmarshal(soap_fault.send_string) + assert_kind_of(SOAP::SOAPFault, env.body.fault) + # Same pre-existing decode limitation documented on SOAP12Fault#code_value + # applies here too (not new): the wire value is a raw prefixed string, + # not resolved back to a QName. Confirms 1.1's own fault path (and its + # existing Client/Server codes) is untouched by the 1.2 work. + assert_equal("Server", env.body.fault.faultcode.data.split(':').last) + end + + def pump_stack(max = 0) + raise ArgumentError if max > 10 + pump_stack(max+1) + end +end + + +end +end diff --git a/test/soap/fault/test_soap12_fault_shape.rb b/test/soap/fault/test_soap12_fault_shape.rb new file mode 100644 index 00000000..d44d3fe7 --- /dev/null +++ b/test/soap/fault/test_soap12_fault_shape.rb @@ -0,0 +1,92 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/processor' +require 'soap/rpc/element' + + +module SOAP +module Fault + + +# Phase 2 of SOAP 1.2 support, step 1: prove the nested/qualified/ +# repeatable SOAP12Fault shape marshals and unmarshals correctly in +# isolation, before wiring it into router/exception machinery (that's +# test_soap12_fault_router.rb / test_soap12_fault_client.rb). SOAPFault +# itself is completely untouched by this -- confirmed separately by the +# existing test_fault.rb/test_customfault.rb/test_soaparray.rb suite +# still passing unmodified. +class TestSoap12FaultShape < Test::Unit::TestCase + def build_envelope(fault) + # Matches how router.rb actually builds a fault response + # (SOAPBody.new(fault_obj, true, soap_version)) rather than the + # decode-side body.fault= setter -- the latter forces the struct key + # (and, via dup_name, the wire tag) to the literal lowercase string + # "fault" regardless of the fault object's own elename, which is only + # fine for values that get read back out via accessors, not + # re-marshaled as-is. + header = SOAP::SOAPHeader.new(SOAP::SOAPVersion1_2) + body = SOAP::SOAPBody.new(fault, true, SOAP::SOAPVersion1_2) + SOAP::SOAPEnvelope.new(header, body, SOAP::SOAPVersion1_2) + end + + def test_fault_with_subcode_and_role_round_trips + fault = SOAP::SOAP12Fault.new( + SOAP::FaultCode12::Sender, "Invalid request", XSD::QName.new("urn:myapp", "BadArgument"), + "http://example.com/myrole" + ) + xml = SOAP::Processor.marshal(build_envelope(fault), :soap_version => SOAP::SOAPVersion1_2) + + assert_match(/env:Code/, xml) + assert_match(/env:Value/, xml) + assert_match(/env:Subcode/, xml) + assert_match(/env:Reason/, xml) + assert_match(/env:Text xml:lang="en"/, xml) + assert_match(/env:Role/, xml) + + env = SOAP::Processor.unmarshal(xml, :soap_version => SOAP::SOAPVersion1_2) + decoded = env.body.fault + # Standard fault codes (Sender/Receiver/etc.) always live in the 1.2 + # envelope namespace per spec, so code_value resolves back to a real + # QName even post-decode. Custom Subcodes can live in arbitrary + # app namespaces, which the generic literal-style decode path has no + # automatic QName-content resolution for (a pre-existing gap, not + # specific to fault codes) -- so code_subcode is the raw wire string + # ("prefix:localname") after a real decode, not a resolved QName. + assert_equal(SOAP::FaultCode12::Sender, decoded.code_value) + assert_equal("BadArgument", decoded.code_subcode.split(':').last) + assert_equal("Invalid request", decoded.reason_text) + assert_equal("http://example.com/myrole", decoded.role) + end + + def test_fault_without_subcode_or_role + fault = SOAP::SOAP12Fault.new(SOAP::FaultCode12::Receiver, "Server broke") + xml = SOAP::Processor.marshal(build_envelope(fault), :soap_version => SOAP::SOAPVersion1_2) + refute_match(/env:Subcode/, xml) + refute_match(/env:Role/, xml) + + env = SOAP::Processor.unmarshal(xml, :soap_version => SOAP::SOAPVersion1_2) + decoded = env.body.fault + assert_equal(SOAP::FaultCode12::Receiver, decoded.code_value) + assert_nil(decoded.code_subcode) + assert_equal("Server broke", decoded.reason_text) + assert_nil(decoded.role) + end + + def test_fault_uses_sender_receiver_not_client_server + fault = SOAP::SOAP12Fault.new(SOAP::FaultCode12::Sender, "bad input") + xml = SOAP::Processor.marshal(build_envelope(fault), :soap_version => SOAP::SOAPVersion1_2) + assert_match(/Sender/, xml) + refute_match(/Client/, xml) + refute_match(/Server/, xml) + end + + private + + def refute_match(pattern, string) + assert_nil(pattern.match(string), "expected #{pattern.inspect} not to match #{string.inspect}") + end +end + + +end +end diff --git a/test/soap/swa/test_soap12_swa.rb b/test/soap/swa/test_soap12_swa.rb new file mode 100644 index 00000000..6b0be3b9 --- /dev/null +++ b/test/soap/swa/test_soap12_swa.rb @@ -0,0 +1,85 @@ +# encoding: UTF-8 +require 'helper' +require 'testutil' +require 'soap/rpc/driver' +require 'soap/rpc/standaloneServer' +require 'soap/attachment' +require 'stringio' + + +module SOAP +module SWA + + +# Confirms SOAP-with-Attachments (MIME multipart) honors soap_version -- a +# real gap found alongside SOAP 1.2 support: MIMEMessage previously +# hardcoded "text/xml" for both the root part and the outer multipart's +# own "type" parameter regardless of soap_version, clobbering whatever +# Content-Type the non-attachment path would have built (see +# lib/soap/mimemessage.rb). Also confirms the legacy SOAPAction header is +# still sent for attachment requests even under 1.2, where it's otherwise +# folded into the Content-Type's action parameter instead -- action +# placement for attachments+1.2 combined isn't settled by any spec this +# was checked against, so the widely-supported legacy header is kept as a +# pragmatic fallback rather than guessed at (see +# StreamHandler::ConnectionData#multipart's comment). +class TestSoap12Swa < Test::Unit::TestCase + Port = 17171 + + class SwAService + def put_file(name, file) + "File '#{name}' was received ok." + end + end + + def setup + @server = SOAP::RPC::StandaloneServer.new('SwAServer12', + 'http://www.acmetron.com/soap', '0.0.0.0', Port) + @server.add_servant(SwAService.new) + @server.level = Logger::Severity::ERROR + @server.soap_version = SOAP::SOAPVersion1_2 + @t = TestUtil.start_server_thread(@server) + @endpoint = "http://localhost:#{Port}/" + @client = SOAP::RPC::Driver.new(@endpoint, 'http://www.acmetron.com/soap') + @client.soap_version = SOAP::SOAPVersion1_2 + @client.add_method('put_file', 'name', 'file') + end + + def teardown + @server.shutdown if @server + if @t + unless @t.join(10) + @t.kill + @t.join + end + end + @client.reset_stream if @client + end + + def test_attachment_request_uses_soap12_media_type_and_keeps_soapaction_header + wire = StringIO.new + @client.wiredump_dev = wire + # A synthetic payload, not this test's own source -- attaching the + # latter would make refute_match's "text/xml" check below a false + # positive against this very file's own comments. + attachment = SOAP::Attachment.new(StringIO.new("attachment payload, unrelated to any media type string")) + result = @client.put_file('foo', attachment) + assert_equal("File 'foo' was received ok.", result) + + dump = wire.string + assert_match(/Content-Type:.*multipart\/related.*type="application\/soap\+xml"/i, dump) + assert_match(/SOAPAction:/i, dump) + assert_match(/Content-Type:\s*application\/soap\+xml;\s*charset/i, dump) + refute_match(/text\/xml/i, dump) + end + + private + + def refute_match(pattern, string) + assert_nil(pattern.match(string), "expected #{pattern.inspect} not to match #{string.inspect}") + end +end + + +end +end diff --git a/test/soap/version/test_soap12_envelope_roundtrip.rb b/test/soap/version/test_soap12_envelope_roundtrip.rb new file mode 100644 index 00000000..d605af09 --- /dev/null +++ b/test/soap/version/test_soap12_envelope_roundtrip.rb @@ -0,0 +1,59 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/processor' + + +module SOAP + + +# Phase 1 of SOAP 1.2 support: envelope/header/body namespace switching via +# SOAPVersion1_2, no fault shape yet (that's Phase 2) and no HTTP transport +# changes yet (Phase 3). Fault-less SOAP 1.2 messages should already +# round-trip correctly through Processor.marshal/unmarshal at this point. +class TestSoap12EnvelopeRoundtrip < Test::Unit::TestCase + def build_envelope + header = SOAP::SOAPHeader.new(SOAP::SOAPVersion1_2) + body = SOAP::SOAPBody.new( + SOAP::SOAPElement.new(XSD::QName.new("my:foo", "bodyitem"), 'bi'), + false, SOAP::SOAPVersion1_2 + ) + SOAP::SOAPEnvelope.new(header, body, SOAP::SOAPVersion1_2) + end + + def test_marshal_uses_soap12_namespace_only + xml = SOAP::Processor.marshal(build_envelope, :soap_version => SOAP::SOAPVersion1_2) + assert_match(/xmlns:env="http:\/\/www\.w3\.org\/2003\/05\/soap-envelope"/, xml) + refute_match(/#{Regexp.escape(SOAP::EnvelopeNamespace)}/, xml) + end + + def test_unmarshal_roundtrip + xml = SOAP::Processor.marshal(build_envelope, :soap_version => SOAP::SOAPVersion1_2) + env = SOAP::Processor.unmarshal(xml, :soap_version => SOAP::SOAPVersion1_2) + assert_equal('http://www.w3.org/2003/05/soap-envelope', env.elename.namespace) + assert_equal('http://www.w3.org/2003/05/soap-envelope', env.body.elename.namespace) + assert_equal('bi', env.body.root_node.data) + end + + # Documents the existing, pre-SOAP-1.2 contract: an envelope in a + # namespace the Parser wasn't configured to recognize was never a clean + # error (this predates soap_version entirely -- any unrecognized + # envelope namespace falls through to decode_tag's generic + # encoding-style handler instead of decode_soap_envelope). soap_version + # being "explicit opt-in only" rides on that existing behavior rather + # than needing anything new: a 1.2 envelope fed to a default (1.1) + # Parser is simply never recognized as an envelope at all. + def test_soap12_envelope_not_recognized_by_default_1_1_parser + xml = SOAP::Processor.marshal(build_envelope, :soap_version => SOAP::SOAPVersion1_2) + result = SOAP::Processor.unmarshal(xml, {}) + assert_not_equal(SOAP::SOAPEnvelope, result.class) + end + + private + + def refute_match(pattern, string) + assert_nil(pattern.match(string), "expected #{pattern.inspect} not to match") + end +end + + +end diff --git a/test/soap/version/test_soap12_http_headers.rb b/test/soap/version/test_soap12_http_headers.rb new file mode 100644 index 00000000..28e99f19 --- /dev/null +++ b/test/soap/version/test_soap12_http_headers.rb @@ -0,0 +1,63 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/streamHandler' + + +module SOAP + + +# Phase 3 of SOAP 1.2 support: HTTP binding. SOAP 1.2 replaces the +# separate SOAPAction header with an action="..." parameter folded into +# Content-Type (application/soap+xml instead of text/xml). Unit-level +# coverage for the building blocks; test_soap12_http_roundtrip.rb covers +# a real client -> WEBrick server -> back round trip. +class TestSoap12HttpHeaders < Test::Unit::TestCase + def test_soap11_content_type_never_embeds_action + ct = SOAP::SOAPVersion1_1.build_content_type("utf-8", "urn:foo:bar") + assert_equal("text/xml; charset=utf-8", ct) + end + + def test_soap12_content_type_embeds_action + ct = SOAP::SOAPVersion1_2.build_content_type("utf-8", "urn:foo:bar") + assert_equal('application/soap+xml; charset=utf-8; action="urn:foo:bar"', ct) + end + + def test_soap12_content_type_omits_action_when_absent + ct = SOAP::SOAPVersion1_2.build_content_type("utf-8") + assert_equal("application/soap+xml; charset=utf-8", ct) + end + + def test_parse_media_type_handles_both_versions + assert_equal("utf-8", SOAP::StreamHandler.parse_media_type("text/xml; charset=utf-8")) + assert_equal("utf-8", SOAP::StreamHandler.parse_media_type("application/soap+xml; charset=utf-8")) + # action can appear before or after charset + assert_equal("utf-8", SOAP::StreamHandler.parse_media_type( + 'application/soap+xml; action="urn:foo"; charset=utf-8')) + assert_equal("utf-8", SOAP::StreamHandler.parse_media_type( + 'application/soap+xml; charset=utf-8; action="urn:foo"')) + end + + def test_parse_media_type_defaults_charset_when_absent + assert_equal("utf-8", SOAP::StreamHandler.parse_media_type("text/xml")) + end + + def test_parse_media_type_rejects_unrelated_content_type + assert_nil(SOAP::StreamHandler.parse_media_type("application/json")) + end + + def test_parse_action_from_content_type + assert_equal("urn:foo:bar", SOAP::StreamHandler.parse_action_from_content_type( + 'application/soap+xml; charset=utf-8; action="urn:foo:bar"')) + assert_nil(SOAP::StreamHandler.parse_action_from_content_type("text/xml; charset=utf-8")) + assert_nil(SOAP::StreamHandler.parse_action_from_content_type(nil)) + end + + def test_create_media_type_backward_compatible_two_arg_form + # the pre-1.2 call site (charset only) must keep producing exactly + # what it always did. + assert_equal("text/xml; charset=utf-8", SOAP::StreamHandler.create_media_type("utf-8")) + end +end + + +end diff --git a/test/soap/version/test_soap12_http_roundtrip.rb b/test/soap/version/test_soap12_http_roundtrip.rb new file mode 100644 index 00000000..27982220 --- /dev/null +++ b/test/soap/version/test_soap12_http_roundtrip.rb @@ -0,0 +1,58 @@ +# encoding: UTF-8 +require 'helper' +require 'testutil' +require 'soap/rpc/driver' +require 'soap/rpc/standaloneServer' + + +module SOAP + + +# Phase 3 of SOAP 1.2 support: full live HTTP round trip (client driver +# -> real WEBrick standalone server -> back) under soap_version = +# SOAPVersion1_2 on both ends -- proves setup_req's Content-Type action +# fallback (soaplet.rb) actually dispatches correctly with a real HTTP +# stack involved, not just that the pieces compile. +class TestSoap12HttpRoundtrip < Test::Unit::TestCase + Port = 17171 + + class EchoServer < SOAP::RPC::StandaloneServer + def on_init + add_method(self, 'echo', 'msg') + end + + def echo(msg) + msg + end + end + + def setup + @server = EchoServer.new('soap12echo', 'urn:soap12echo', '0.0.0.0', Port) + @server.level = Logger::Severity::ERROR + @server.soap_version = SOAP::SOAPVersion1_2 + @t = TestUtil.start_server_thread(@server) + @endpoint = "http://localhost:#{Port}/" + @client = SOAP::RPC::Driver.new(@endpoint, 'urn:soap12echo') + @client.soap_version = SOAP::SOAPVersion1_2 + @client.wiredump_dev = STDERR if $DEBUG + @client.add_method("echo", "msg") + end + + def teardown + @server.shutdown if @server + if @t + unless @t.join(10) + @t.kill + @t.join + end + end + @client.reset_stream if @client + end + + def test_soap12_round_trip_dispatches_via_content_type_action + assert_equal("hello", @client.echo("hello")) + end +end + + +end diff --git a/test/soap/version/test_soap12_role_relay.rb b/test/soap/version/test_soap12_role_relay.rb new file mode 100644 index 00000000..80b6afc3 --- /dev/null +++ b/test/soap/version/test_soap12_role_relay.rb @@ -0,0 +1,52 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/processor' + + +module SOAP + + +class TestSoap12RoleRelay < Test::Unit::TestCase + ROLE = 'http://www.w3.org/2003/05/soap-envelope/role/next' + + def build_envelope_with_header_item + header = SOAP::SOAPHeader.new(SOAP::SOAPVersion1_2) + item_element = SOAP::SOAPElement.new(XSD::QName.new("my:foo", "auth"), 'secret') + header.add("auth", item_element) + header_item = header.instance_variable_get(:@data).last + header_item.actor = ROLE + header_item.relay = true + body = SOAP::SOAPBody.new(nil, false, SOAP::SOAPVersion1_2) + SOAP::SOAPEnvelope.new(header, body, SOAP::SOAPVersion1_2) + end + + def test_role_and_relay_round_trip + xml = SOAP::Processor.marshal(build_envelope_with_header_item, :soap_version => SOAP::SOAPVersion1_2) + assert_match(/env:role="#{Regexp.escape(ROLE)}"/, xml) + assert_match(/env:relay="true"/, xml) + assert_nil(/actor=/.match(xml)) + + env = SOAP::Processor.unmarshal(xml, :soap_version => SOAP::SOAPVersion1_2) + item = env.header.instance_variable_get(:@data).first + assert_equal(ROLE, item.actor) + assert_equal(true, item.relay) + end + + def test_soap11_header_item_never_emits_role_or_relay + header = SOAP::SOAPHeader.new + item_element = SOAP::SOAPElement.new(XSD::QName.new("my:foo", "auth"), 'secret') + header.add("auth", item_element) + header_item = header.instance_variable_get(:@data).last + header_item.actor = 'http://example.com/actor' + header_item.relay = true # should be silently ignored -- no such concept under 1.1 + body = SOAP::SOAPBody.new + env = SOAP::SOAPEnvelope.new(header, body) + + xml = SOAP::Processor.marshal(env, {}) + assert_match(/env:actor="http:\/\/example\.com\/actor"/, xml) + assert_nil(/relay=/.match(xml)) + end +end + + +end diff --git a/test/soap/version/test_soap_version_default.rb b/test/soap/version/test_soap_version_default.rb new file mode 100644 index 00000000..2de82ad7 --- /dev/null +++ b/test/soap/version/test_soap_version_default.rb @@ -0,0 +1,62 @@ +# encoding: UTF-8 +require 'helper' +require 'soap/processor' +require 'soap/rpc/driver' +require 'soap/rpc/router' + + +module SOAP + + +# Phase 0 of SOAP 1.2 support: SOAPVersion plumbing only, no visible +# behavior change yet. This is the regression pin for that guarantee -- +# with soap_version left untouched anywhere, marshal/unmarshal must behave +# exactly as it did before SOAPVersion existed, since SOAPVersion1_1's +# envelope_namespace is built from the very same EnvelopeNamespace constant +# Parser/Proxy/Router already defaulted to. +class TestSoapVersionDefault < Test::Unit::TestCase + def build_envelope + header = SOAP::SOAPHeader.new + body = SOAP::SOAPBody.new(SOAP::SOAPElement.new(XSD::QName.new("my:foo", "bodyitem"), 'bi')) + SOAP::SOAPEnvelope.new(header, body) + end + + def test_marshal_identical_with_or_without_explicit_soap_version_1_1 + implicit = SOAP::Processor.marshal(build_envelope, {}) + explicit = SOAP::Processor.marshal(build_envelope, :soap_version => SOAP::SOAPVersion1_1) + assert_equal(implicit, explicit) + assert_match(/xmlns:env="#{Regexp.escape(SOAP::EnvelopeNamespace)}"/, implicit) + end + + def test_unmarshal_identical_with_or_without_explicit_soap_version_1_1 + xml = SOAP::Processor.marshal(build_envelope, {}) + implicit = SOAP::Processor.unmarshal(xml, {}) + explicit = SOAP::Processor.unmarshal(xml, :soap_version => SOAP::SOAPVersion1_1) + assert_equal(SOAP::EnvelopeNamespace, implicit.elename.namespace) + assert_equal(SOAP::EnvelopeNamespace, explicit.elename.namespace) + end + + def test_parser_default_soap_version_is_1_1 + parser = SOAP::Parser.new + assert_equal(SOAP::SOAPVersion1_1, parser.soap_version) + assert_equal(SOAP::EnvelopeNamespace, parser.envelopenamespace) + end + + def test_driver_proxy_soap_version_accessor_defaults_and_is_settable + driver = SOAP::RPC::Driver.new("http://localhost:17171/") + assert_equal(SOAP::SOAPVersion1_1, driver.soap_version) + driver.soap_version = SOAP::SOAPVersion1_2 + assert_equal(SOAP::SOAPVersion1_2, driver.soap_version) + assert_equal(SOAP::SOAPVersion1_2, driver.proxy.soap_version) + end + + def test_router_soap_version_accessor_defaults_and_is_settable + router = SOAP::RPC::Router.new('test_router') + assert_equal(SOAP::SOAPVersion1_1, router.soap_version) + router.soap_version = SOAP::SOAPVersion1_2 + assert_equal(SOAP::SOAPVersion1_2, router.soap_version) + end +end + + +end diff --git a/test/wsdl/soap/soap12/soap12.wsdl b/test/wsdl/soap/soap12/soap12.wsdl new file mode 100644 index 00000000..82c74a65 --- /dev/null +++ b/test/wsdl/soap/soap12/soap12.wsdl @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/wsdl/soap/soap12/test_soap12_roundtrip.rb b/test/wsdl/soap/soap12/test_soap12_roundtrip.rb new file mode 100644 index 00000000..404c3fd0 --- /dev/null +++ b/test/wsdl/soap/soap12/test_soap12_roundtrip.rb @@ -0,0 +1,69 @@ +# encoding: UTF-8 +require 'helper' +require 'testutil' +require 'wsdl/parser' +require 'soap/rpc/standaloneServer' +require 'soap/wsdlDriver' + + +module WSDL; module SOAP; module T_Soap12Roundtrip + + +# Phase 4 of SOAP 1.2 support, the strongest test in the whole plan: +# following test_soapenc.rb's template, proves a SOAP 1.2-bound WSDL +# doesn't just parse (test_soap12_binding_parse.rb already covers that) +# but produces a driver that completes a real, wire-correct SOAP 1.2 RPC +# call end to end -- WSDLDriverFactory#create_rpc_driver must actually +# detect binding.soapbinding.soap12 and configure the resulting driver's +# soap_version accordingly (lib/soap/wsdlDriver.rb#init_driver), or this +# fails despite the WSDL parsing perfectly fine. +class TestSoap12Roundtrip < Test::Unit::TestCase + class Server < ::SOAP::RPC::StandaloneServer + def on_init + add_rpc_method(self, 'echo', 'msg') + self.soap_version = ::SOAP::SOAPVersion1_2 + end + + def echo(msg) + msg + end + end + + DIR = File.dirname(File.expand_path(__FILE__)) + Port = 17171 + + def setup + @server = Server.new('Test', 'urn:soap12echo', '0.0.0.0', Port) + @server.level = Logger::Severity::ERROR + @server_thread = TestUtil.start_server_thread(@server) + @client = nil + end + + def teardown + @server.shutdown if @server + unless @server_thread.join(10) + @server_thread.kill + @server_thread.join + end + @client.reset_stream if @client + end + + def test_soap12_bound_wsdl_drives_a_real_call + wsdl = File.join(DIR, 'soap12.wsdl') + factory = ::SOAP::WSDLDriverFactory.new(wsdl) + @client = factory.create_rpc_driver + assert_equal(::SOAP::SOAPVersion1_2, @client.soap_version) + @client.endpoint_url = "http://localhost:#{Port}/" + @client.wiredump_dev = STDOUT if $DEBUG + ret = @client.echo("hello") + # RPC-style single-part response wrapping is an existing, orthogonal + # WSDL/RPC message-mapping behavior, not a SOAP 1.2 concern -- the + # actual point of this test is everything upstream: the request made + # it over the wire framed as SOAP 1.2 and the server correctly + # dispatched and responded. + assert_equal(["hello"], ret) + end +end + + +end; end; end diff --git a/test/wsdl/soap/test_soap12_binding_parse.rb b/test/wsdl/soap/test_soap12_binding_parse.rb new file mode 100644 index 00000000..40779d9b --- /dev/null +++ b/test/wsdl/soap/test_soap12_binding_parse.rb @@ -0,0 +1,38 @@ +# encoding: UTF-8 +require 'helper' +require 'wsdl/importer' + + +module WSDL +module SOAP + + +# Phase 4 of SOAP 1.2 support: WSDL binding-namespace recognition. Proves +# the six case/when dispatch points (binding.rb, operationBinding.rb, +# param.rb, port.rb, service.rb, soap/header.rb) actually recognize +# etc -- the fixture (soap12/soap12.wsdl) uses the SOAP +# 1.2 WSDL binding namespace exclusively, no at all, so +# this only passes if every one of those six arms works. +class TestSoap12BindingParse < Test::Unit::TestCase + DIR = File.dirname(File.expand_path(__FILE__)) + + def test_soap12_binding_recognized + wsdl = WSDL::Importer.import(File.join(DIR, 'soap12', 'soap12.wsdl')) + binding = wsdl.bindings.find { |b| b.name.name == 'soap12echo_binding' } + assert_not_nil(binding) + assert_not_nil(binding.soapbinding) + assert_equal(true, binding.soapbinding.soap12) + assert_equal(:rpc, binding.soapbinding.style) + + operation = binding.operations.find { |op| op.name == 'echo' } + assert_not_nil(operation.soapoperation) + + port = wsdl.services[0].ports[0] + assert_not_nil(port.soap_address) + assert_equal('http://localhost:17171/', port.soap_address.location) + end +end + + +end +end