debian-zeep/src/zeep/wsdl/bindings/soap.py

445 lines
16 KiB
Python

import logging
from lxml import etree
from requests_toolbelt.multipart.decoder import MultipartDecoder
from zeep import ns, plugins, wsa
from zeep.exceptions import Fault, TransportError, XMLSyntaxError
from zeep.loader import parse_xml
from zeep.utils import as_qname, get_media_type, qname_attr
from zeep.wsdl.attachments import MessagePack
from zeep.wsdl.definitions import Binding, Operation
from zeep.wsdl.messages import DocumentMessage, RpcMessage
from zeep.wsdl.messages.xop import process_xop
from zeep.wsdl.utils import etree_to_string, url_http_to_https
logger = logging.getLogger(__name__)
class SoapBinding(Binding):
"""Soap 1.1/1.2 binding"""
def __init__(self, wsdl, name, port_name, transport, default_style):
"""The SoapBinding is the base class for the Soap11Binding and
Soap12Binding.
:param wsdl:
:type wsdl:
:param name:
:type name: string
:param port_name:
:type port_name: string
:param transport:
:type transport: zeep.transports.Transport
:param default_style:
"""
super(SoapBinding, self).__init__(wsdl, name, port_name)
self.transport = transport
self.default_style = default_style
@classmethod
def match(cls, node):
"""Check if this binding instance should be used to parse the given
node.
:param node: The node to match against
:type node: lxml.etree._Element
"""
soap_node = node.find('soap:binding', namespaces=cls.nsmap)
return soap_node is not None
def create_message(self, operation, *args, **kwargs):
envelope, http_headers = self._create(operation, args, kwargs)
return envelope
def _create(self, operation, args, kwargs, client=None, options=None):
"""Create the XML document to send to the server.
Note that this generates the soap envelope without the wsse applied.
"""
operation_obj = self.get(operation)
if not operation_obj:
raise ValueError("Operation %r not found" % operation)
# Create the SOAP envelope
serialized = operation_obj.create(*args, **kwargs)
self._set_http_headers(serialized, operation_obj)
envelope = serialized.content
http_headers = serialized.headers
# Apply ws-addressing
if client:
if not options:
options = client.service._binding_options
if operation_obj.abstract.input_message.wsa_action:
envelope, http_headers = wsa.WsAddressingPlugin().egress(
envelope, http_headers, operation_obj, options)
# Apply plugins
envelope, http_headers = plugins.apply_egress(
client, envelope, http_headers, operation_obj, options)
# Apply WSSE
if client.wsse:
envelope, http_headers = client.wsse.apply(envelope, http_headers)
return envelope, http_headers
def send(self, client, options, operation, args, kwargs):
"""Called from the service
:param client: The client with which the operation was called
:type client: zeep.client.Client
:param options: The binding options
:type options: dict
:param operation: The operation object from which this is a reply
:type operation: zeep.wsdl.definitions.Operation
:param args: The args to pass to the operation
:type args: tuple
:param kwargs: The kwargs to pass to the operation
:type kwargs: dict
"""
envelope, http_headers = self._create(
operation, args, kwargs,
client=client,
options=options)
response = client.transport.post_xml(
options['address'], envelope, http_headers)
operation_obj = self.get(operation)
# If the client wants to return the raw data then let's do that.
if client.raw_response:
return response
return self.process_reply(client, operation_obj, response)
def process_reply(self, client, operation, response):
"""Process the XML reply from the server.
:param client: The client with which the operation was called
:type client: zeep.client.Client
:param operation: The operation object from which this is a reply
:type operation: zeep.wsdl.definitions.Operation
:param response: The response object returned by the remote server
:type response: requests.Response
"""
if response.status_code != 200 and not response.content:
raise TransportError(
u'Server returned HTTP status %d (no content available)'
% response.status_code,
status_code=response.status_code)
content_type = response.headers.get('Content-Type', 'text/xml')
media_type = get_media_type(content_type)
message_pack = None
# If the reply is a multipart/related then we need to retrieve all the
# parts
if media_type == 'multipart/related':
decoder = MultipartDecoder(
response.content, content_type, response.encoding or 'utf-8')
content = decoder.parts[0].content
if len(decoder.parts) > 1:
message_pack = MessagePack(parts=decoder.parts[1:])
else:
content = response.content
try:
doc = parse_xml(
content, self.transport,
strict=client.wsdl.strict,
xml_huge_tree=client.xml_huge_tree)
except XMLSyntaxError:
raise TransportError(
'Server returned HTTP status %d (%s)'
% (response.status_code, response.content),
status_code=response.status_code,
content=response.content)
# Check if this is an XOP message which we need to decode first
if message_pack:
if process_xop(doc, message_pack):
message_pack = None
if client.wsse:
client.wsse.verify(doc)
doc, http_headers = plugins.apply_ingress(
client, doc, response.headers, operation)
# If the response code is not 200 or if there is a Fault node available
# then assume that an error occured.
fault_node = doc.find(
'soap-env:Body/soap-env:Fault', namespaces=self.nsmap)
if response.status_code != 200 or fault_node is not None:
return self.process_error(doc, operation)
result = operation.process_reply(doc)
if message_pack:
message_pack._set_root(result)
return message_pack
return result
def process_error(self, doc, operation):
raise NotImplementedError
def process_service_port(self, xmlelement, force_https=False):
address_node = xmlelement.find('soap:address', namespaces=self.nsmap)
if address_node is None:
logger.debug("No valid soap:address found for service")
return
# Force the usage of HTTPS when the force_https boolean is true
location = address_node.get('location')
if force_https and location:
location = url_http_to_https(location)
if location != address_node.get('location'):
logger.warning("Forcing soap:address location to HTTPS")
return {
'address': location
}
@classmethod
def parse(cls, definitions, xmlelement):
"""
Definition::
<wsdl:binding name="nmtoken" type="qname"> *
<-- extensibility element (1) --> *
<wsdl:operation name="nmtoken"> *
<-- extensibility element (2) --> *
<wsdl:input name="nmtoken"? > ?
<-- extensibility element (3) -->
</wsdl:input>
<wsdl:output name="nmtoken"? > ?
<-- extensibility element (4) --> *
</wsdl:output>
<wsdl:fault name="nmtoken"> *
<-- extensibility element (5) --> *
</wsdl:fault>
</wsdl:operation>
</wsdl:binding>
"""
name = qname_attr(xmlelement, 'name', definitions.target_namespace)
port_name = qname_attr(xmlelement, 'type', definitions.target_namespace)
# The soap:binding element contains the transport method and
# default style attribute for the operations.
soap_node = xmlelement.find('soap:binding', namespaces=cls.nsmap)
transport = soap_node.get('transport')
supported_transports = [
'http://schemas.xmlsoap.org/soap/http',
'http://www.w3.org/2003/05/soap/bindings/HTTP/',
]
if transport not in supported_transports:
raise NotImplementedError(
"The binding transport %s is not supported (only soap/http)" % (
transport))
default_style = soap_node.get('style', 'document')
obj = cls(definitions.wsdl, name, port_name, transport, default_style)
for node in xmlelement.findall('wsdl:operation', namespaces=cls.nsmap):
operation = SoapOperation.parse(definitions, node, obj, nsmap=cls.nsmap)
obj._operation_add(operation)
return obj
class Soap11Binding(SoapBinding):
nsmap = {
'soap': ns.SOAP_11,
'soap-env': ns.SOAP_ENV_11,
'wsdl': ns.WSDL,
'xsd': ns.XSD,
}
def process_error(self, doc, operation):
fault_node = doc.find(
'soap-env:Body/soap-env:Fault', namespaces=self.nsmap)
if fault_node is None:
raise Fault(
message='Unknown fault occured',
code=None,
actor=None,
detail=etree_to_string(doc))
def get_text(name):
child = fault_node.find(name)
if child is not None:
return child.text
raise Fault(
message=get_text('faultstring'),
code=get_text('faultcode'),
actor=get_text('faultactor'),
detail=fault_node.find('detail'))
def _set_http_headers(self, serialized, operation):
serialized.headers['Content-Type'] = 'text/xml; charset=utf-8'
class Soap12Binding(SoapBinding):
nsmap = {
'soap': ns.SOAP_12,
'soap-env': ns.SOAP_ENV_12,
'wsdl': ns.WSDL,
'xsd': ns.XSD,
}
def process_error(self, doc, operation):
fault_node = doc.find(
'soap-env:Body/soap-env:Fault', namespaces=self.nsmap)
if fault_node is None:
raise Fault(
message='Unknown fault occured',
code=None,
actor=None,
detail=etree_to_string(doc))
def get_text(name):
child = fault_node.find(name)
if child is not None:
return child.text
message = fault_node.findtext('soap-env:Reason/soap-env:Text', namespaces=self.nsmap)
code = fault_node.findtext('soap-env:Code/soap-env:Value', namespaces=self.nsmap)
# Extract the fault subcodes. These can be nested, as in subcodes can
# also contain other subcodes.
subcodes = []
subcode_element = fault_node.find('soap-env:Code/soap-env:Subcode', namespaces=self.nsmap)
while subcode_element is not None:
subcode_value_element = subcode_element.find('soap-env:Value', namespaces=self.nsmap)
subcode_qname = as_qname(subcode_value_element.text, subcode_value_element.nsmap, None)
subcodes.append(subcode_qname)
subcode_element = subcode_element.find('soap-env:Subcode', namespaces=self.nsmap)
# TODO: We should use the fault message as defined in the wsdl.
detail_node = fault_node.find('soap-env:Detail', namespaces=self.nsmap)
raise Fault(
message=message,
code=code,
actor=None,
detail=detail_node,
subcodes=subcodes)
def _set_http_headers(self, serialized, operation):
serialized.headers['Content-Type'] = '; '.join([
'application/soap+xml',
'charset=utf-8',
'action="%s"' % operation.soapaction
])
class SoapOperation(Operation):
"""Represent's an operation within a specific binding."""
def __init__(self, name, binding, nsmap, soapaction, style):
super(SoapOperation, self).__init__(name, binding)
self.nsmap = nsmap
self.soapaction = soapaction
self.style = style
def process_reply(self, envelope):
envelope_qname = etree.QName(self.nsmap['soap-env'], 'Envelope')
if envelope.tag != envelope_qname:
raise XMLSyntaxError((
"The XML returned by the server does not contain a valid " +
"{%s}Envelope root element. The root element found is %s "
) % (envelope_qname.namespace, envelope.tag))
if self.output:
return self.output.deserialize(envelope)
@classmethod
def parse(cls, definitions, xmlelement, binding, nsmap):
"""
Definition::
<wsdl:operation name="nmtoken"> *
<soap:operation soapAction="uri"? style="rpc|document"?>?
<wsdl:input name="nmtoken"? > ?
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output name="nmtoken"? > ?
<-- extensibility element (4) --> *
</wsdl:output>
<wsdl:fault name="nmtoken"> *
<-- extensibility element (5) --> *
</wsdl:fault>
</wsdl:operation>
Example::
<wsdl:operation name="GetLastTradePrice">
<soap:operation soapAction="http://example.com/GetLastTradePrice"/>
<wsdl:input>
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output>
</wsdl:output>
<wsdl:fault name="dataFault">
<soap:fault name="dataFault" use="literal"/>
</wsdl:fault>
</operation>
"""
name = xmlelement.get('name')
# The soap:operation element is required for soap/http bindings
# and may be omitted for other bindings.
soap_node = xmlelement.find('soap:operation', namespaces=binding.nsmap)
action = None
if soap_node is not None:
action = soap_node.get('soapAction')
style = soap_node.get('style', binding.default_style)
else:
style = binding.default_style
obj = cls(name, binding, nsmap, action, style)
if style == 'rpc':
message_class = RpcMessage
else:
message_class = DocumentMessage
for node in xmlelement.getchildren():
tag_name = etree.QName(node.tag).localname
if tag_name not in ('input', 'output', 'fault'):
continue
msg = message_class.parse(
definitions=definitions, xmlelement=node,
operation=obj, nsmap=nsmap, type=tag_name)
if tag_name == 'fault':
obj.faults[msg.name] = msg
else:
setattr(obj, tag_name, msg)
return obj
def resolve(self, definitions):
super(SoapOperation, self).resolve(definitions)
for name, fault in self.faults.items():
if name in self.abstract.fault_messages:
fault.resolve(definitions, self.abstract.fault_messages[name])
if self.output:
self.output.resolve(definitions, self.abstract.output_message)
if self.input:
self.input.resolve(definitions, self.abstract.input_message)