wsme/wsmeext/soap/protocol.py

476 lines
15 KiB
Python

"""
A SOAP implementation for wsme.
Parts of the code were taken from the tgwebservices soap implmentation.
"""
from __future__ import absolute_import
import pkg_resources
import datetime
import decimal
import base64
import logging
import six
from wsmeext.soap.simplegeneric import generic
from wsmeext.soap.wsdl import WSDLGenerator
try:
from lxml import etree as ET
use_lxml = True
except ImportError:
from xml.etree import cElementTree as ET # noqa
use_lxml = False
from wsme.protocol import CallContext, Protocol, expose
import wsme.types
import wsme.runtime
from wsme import exc
from wsme.utils import parse_isodate, parse_isotime, parse_isodatetime
log = logging.getLogger(__name__)
xsd_ns = 'http://www.w3.org/2001/XMLSchema'
xsi_ns = 'http://www.w3.org/2001/XMLSchema-instance'
soapenv_ns = 'http://schemas.xmlsoap.org/soap/envelope/'
if not use_lxml:
ET.register_namespace('soap', soapenv_ns)
type_qn = '{%s}type' % xsi_ns
nil_qn = '{%s}nil' % xsi_ns
Envelope_qn = '{%s}Envelope' % soapenv_ns
Body_qn = '{%s}Body' % soapenv_ns
Fault_qn = '{%s}Fault' % soapenv_ns
faultcode_qn = '{%s}faultcode' % soapenv_ns
faultstring_qn = '{%s}faultstring' % soapenv_ns
detail_qn = '{%s}detail' % soapenv_ns
type_registry = {
wsme.types.bytes: 'xs:string',
wsme.types.text: 'xs:string',
int: 'xs:int',
float: "xs:float",
bool: "xs:boolean",
#unsigned: "xs:unsignedInt",
datetime.datetime: "xs:dateTime",
datetime.date: "xs:date",
datetime.time: "xs:time",
decimal.Decimal: "xs:decimal",
wsme.types.binary: "xs:base64Binary",
}
if not six.PY3:
type_registry[long] = "xs:long"
array_registry = {
wsme.types.text: "String_Array",
wsme.types.bytes: "String_Array",
int: "Int_Array",
float: "Float_Array",
bool: "Boolean_Array",
}
if not six.PY3:
array_registry[long] = "Long_Array"
def soap_array(datatype, ns):
if datatype.item_type in array_registry:
name = array_registry[datatype.item_type]
else:
name = soap_type(datatype.item_type, False) + '_Array'
if ns:
name = 'types:' + name
return name
def soap_type(datatype, ns):
name = None
if wsme.types.isarray(datatype):
return soap_array(datatype, ns)
if wsme.types.isdict(datatype):
return None
if datatype in type_registry:
stype = type_registry[datatype]
if not ns:
stype = stype[3:]
return stype
if wsme.types.iscomplex(datatype):
name = datatype.__name__
if name and ns:
name = 'types:' + name
return name
if wsme.types.isusertype(datatype):
return soap_type(datatype.basetype, ns)
def soap_fname(path, funcdef):
return "".join([path[0]] + [i.capitalize() for i in path[1:]])
class SoapEncoder(object):
def __init__(self, types_ns):
self.types_ns = types_ns
def make_soap_element(self, datatype, tag, value, xsitype=None):
el = ET.Element(tag)
if value is None:
el.set(nil_qn, 'true')
elif xsitype is not None:
el.set(type_qn, xsitype)
el.text = value
elif wsme.types.isusertype(datatype):
return self.tosoap(datatype.basetype, tag,
datatype.tobasetype(value))
elif wsme.types.iscomplex(datatype):
el.set(type_qn, 'types:%s' % (datatype.__name__))
for attrdef in wsme.types.list_attributes(datatype):
attrvalue = getattr(value, attrdef.key)
if attrvalue is not wsme.types.Unset:
el.append(self.tosoap(
attrdef.datatype,
'{%s}%s' % (self.types_ns, attrdef.name),
attrvalue
))
else:
el.set(type_qn, type_registry.get(datatype))
if not isinstance(value, wsme.types.text):
value = wsme.types.text(value)
el.text = value
return el
@generic
def tosoap(self, datatype, tag, value):
"""Converts a value into xml Element objects for inclusion in the SOAP
response output (after adding the type to the type_registry).
If a non-complex user specific type is to be used in the api,
a specific toxml should be added::
from wsme.protocol.soap import tosoap, make_soap_element, \
type_registry
class MySpecialType(object):
pass
type_registry[MySpecialType] = 'xs:MySpecialType'
@tosoap.when_object(MySpecialType)
def myspecialtype_tosoap(datatype, tag, value):
return make_soap_element(datatype, tag, str(value))
"""
return self.make_soap_element(datatype, tag, value)
@tosoap.when_type(wsme.types.ArrayType)
def array_tosoap(self, datatype, tag, value):
el = ET.Element(tag)
el.set(type_qn, soap_array(datatype, self.types_ns))
if value is None:
el.set(nil_qn, 'true')
elif len(value) == 0:
el.append(ET.Element('item'))
else:
for item in value:
el.append(self.tosoap(datatype.item_type, 'item', item))
return el
@tosoap.when_object(bool)
def bool_tosoap(self, datatype, tag, value):
return self.make_soap_element(
datatype,
tag,
'true' if value is True else 'false' if value is False else None
)
@tosoap.when_object(wsme.types.bytes)
def bytes_tosoap(self, datatype, tag, value):
print('bytes_tosoap', datatype, tag, value, type(value))
if isinstance(value, wsme.types.bytes):
value = value.decode('ascii')
return self.make_soap_element(datatype, tag, value)
@tosoap.when_object(datetime.datetime)
def datetime_tosoap(self, datatype, tag, value):
return self.make_soap_element(
datatype,
tag,
value is not None and value.isoformat() or None
)
@tosoap.when_object(wsme.types.binary)
def binary_tosoap(self, datatype, tag, value):
print(datatype, tag, value)
value = base64.encodestring(value) if value is not None else None
if six.PY3:
value = value.decode('ascii')
return self.make_soap_element(
datatype.basetype, tag, value, 'xs:base64Binary'
)
@tosoap.when_object(None)
def None_tosoap(self, datatype, tag, value):
return self.make_soap_element(datatype, tag, None)
@generic
def fromsoap(datatype, el, ns):
"""
A generic converter from soap elements to python datatype.
If a non-complex user specific type is to be used in the api,
a specific fromsoap should be added.
"""
if el.get(nil_qn) == 'true':
return None
if datatype in type_registry:
value = datatype(el.text)
elif wsme.types.isusertype(datatype):
value = datatype.frombasetype(
fromsoap(datatype.basetype, el, ns))
else:
value = datatype()
for attr in wsme.types.list_attributes(datatype):
child = el.find('{%s}%s' % (ns['type'], attr.name))
if child is not None:
setattr(value, attr.key, fromsoap(attr.datatype, child, ns))
return value
@fromsoap.when_type(wsme.types.ArrayType)
def array_fromsoap(datatype, el, ns):
if len(el) == 1:
if datatype.item_type \
not in wsme.types.pod_types + wsme.types.dt_types \
and len(el[0]) == 0:
return []
return [fromsoap(datatype.item_type, child, ns) for child in el]
@fromsoap.when_object(wsme.types.bytes)
def bytes_fromsoap(datatype, el, ns):
if el.get(nil_qn) == 'true':
return None
if el.get(type_qn) not in (None, 'xs:string'):
raise exc.InvalidInput(el.tag, ET.tostring(el))
return el.text.encode('ascii') if el.text else six.b('')
@fromsoap.when_object(wsme.types.text)
def text_fromsoap(datatype, el, ns):
if el.get(nil_qn) == 'true':
return None
if el.get(type_qn) not in (None, 'xs:string'):
raise exc.InvalidInput(el.tag, ET.tostring(el))
return datatype(el.text if el.text else '')
@fromsoap.when_object(bool)
def bool_fromsoap(datatype, el, ns):
if el.get(nil_qn) == 'true':
return None
if el.get(type_qn) not in (None, 'xs:boolean'):
raise exc.InvalidInput(el.tag, ET.tostring(el))
return el.text.lower() != 'false'
@fromsoap.when_object(datetime.date)
def date_fromsoap(datatype, el, ns):
if el.get(nil_qn) == 'true':
return None
if el.get(type_qn) not in (None, 'xs:date'):
raise exc.InvalidInput(el.tag, ET.tostring(el))
return parse_isodate(el.text)
@fromsoap.when_object(datetime.time)
def time_fromsoap(datatype, el, ns):
if el.get(nil_qn) == 'true':
return None
if el.get(type_qn) not in (None, 'xs:time'):
raise exc.InvalidInput(el.tag, ET.tostring(el))
return parse_isotime(el.text)
@fromsoap.when_object(datetime.datetime)
def datetime_fromsoap(datatype, el, ns):
if el.get(nil_qn) == 'true':
return None
if el.get(type_qn) not in (None, 'xs:dateTime'):
raise exc.InvalidInput(el.tag, ET.tostring(el))
return parse_isodatetime(el.text)
@fromsoap.when_object(wsme.types.binary)
def binary_fromsoap(datatype, el, ns):
if el.get(nil_qn) == 'true':
return None
if el.get(type_qn) not in (None, 'xs:base64Binary'):
raise exc.InvalidInput(el.tag, ET.tostring(el))
return base64.decodestring(el.text.encode('ascii'))
class SoapProtocol(Protocol):
"""
SOAP protocol.
.. autoattribute:: name
.. autoattribute:: content_types
"""
name = 'soap'
displayname = 'SOAP'
content_types = ['application/soap+xml']
ns = {
"soap": "http://www.w3.org/2001/12/soap-envelope",
"soapenv": "http://schemas.xmlsoap.org/soap/envelope/",
"soapenc": "http://schemas.xmlsoap.org/soap/encoding/",
}
def __init__(self, tns=None, typenamespace=None, baseURL=None,
servicename='MyApp'):
self.tns = tns
self.typenamespace = typenamespace
self.servicename = servicename
self.baseURL = baseURL
self._name_mapping = {}
self.encoder = SoapEncoder(typenamespace)
def get_name_mapping(self, service=None):
if service not in self._name_mapping:
self._name_mapping[service] = dict(
(soap_fname(path, f), path)
for path, f in self.root.getapi()
if service is None or (path and path[0] == service)
)
return self._name_mapping[service]
def accept(self, req):
for ct in self.content_types:
if req.headers['Content-Type'].startswith(ct):
return True
if req.headers.get("Soapaction"):
return True
return False
def iter_calls(self, request):
yield CallContext(request)
def extract_path(self, context):
request = context.request
el = ET.fromstring(request.body)
body = el.find('{%(soapenv)s}Body' % self.ns)
# Extract the service name from the tns
message = list(body)[0]
fname = message.tag
if fname.startswith('{%s}' % self.typenamespace):
fname = fname[len(self.typenamespace) + 2:]
mapping = self.get_name_mapping()
if fname not in mapping:
raise exc.UnknownFunction(fname)
path = mapping[fname]
context.soap_message = message
return path
return None
def read_arguments(self, context):
kw = {}
if not hasattr(context, 'soap_message'):
return kw
msg = context.soap_message
for param in msg:
name = param.tag[len(self.typenamespace) + 2:]
arg = context.funcdef.get_arg(name)
value = fromsoap(arg.datatype, param, {
'type': self.typenamespace,
})
kw[name] = value
wsme.runtime.check_arguments(context.funcdef, (), kw)
return kw
def soap_response(self, path, funcdef, result):
r = ET.Element('{%s}%sResponse' % (
self.typenamespace, soap_fname(path, funcdef)
))
print('soap_response', funcdef.return_type, result)
r.append(self.encoder.tosoap(
funcdef.return_type, '{%s}result' % self.typenamespace, result
))
return r
def encode_result(self, context, result):
print('encode_result', result)
if use_lxml:
envelope = ET.Element(
Envelope_qn,
nsmap={'xs': xsd_ns, 'types': self.typenamespace}
)
else:
envelope = ET.Element(Envelope_qn, {
'xmlns:xs': xsd_ns,
'xmlns:types': self.typenamespace
})
body = ET.SubElement(envelope, Body_qn)
body.append(self.soap_response(context.path, context.funcdef, result))
s = ET.tostring(envelope)
return s
def get_template(self, name):
return pkg_resources.resource_string(
__name__, '%s.html' % name)
def encode_error(self, context, infos):
envelope = ET.Element(Envelope_qn)
body = ET.SubElement(envelope, Body_qn)
fault = ET.SubElement(body, Fault_qn)
ET.SubElement(fault, faultcode_qn).text = infos['faultcode']
ET.SubElement(fault, faultstring_qn).text = infos['faultstring']
if 'debuginfo' in infos:
ET.SubElement(fault, detail_qn).text = infos['debuginfo']
s = ET.tostring(envelope)
return s
@expose('/api.wsdl', 'text/xml')
def api_wsdl(self, service=None):
if service is None:
servicename = self.servicename
else:
servicename = self.servicename + service.capitalize()
return WSDLGenerator(
tns=self.tns,
types_ns=self.typenamespace,
soapenc=self.ns['soapenc'],
service_name=servicename,
complex_types=self.root.__registry__.complex_types,
funclist=self.root.getapi(),
arrays=self.root.__registry__.array_types,
baseURL=self.baseURL,
soap_array=soap_array,
soap_type=soap_type,
soap_fname=soap_fname,
).generate(True)
def encode_sample_value(self, datatype, value, format=False):
r = self.encoder.make_soap_element(datatype, 'value', value)
if format:
xml_indent(r)
return ('xml', unicode(r))
def xml_indent(elem, level=0):
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
for e in elem:
xml_indent(e, level + 1)
if not e.tail or not e.tail.strip():
e.tail = i
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i