source: OpenRLabs-Git/deploy/rlabs-docker/web2py-rlabs/gluon/contrib/pysimplesoap/server.py

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100755
File size: 24.7 KB
Line 
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU Lesser General Public License as published by the
5# Free Software Foundation; either version 3, or (at your option) any later
6# version.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11# for more details.
12
13"""Pythonic simple SOAP Server implementation"""
14
15
16from __future__ import unicode_literals
17import sys
18
19if sys.version_info[0] < 3:
20    is_py2 = True
21else:
22    is_py2 = False
23    unicode = str
24
25
26import datetime
27import sys
28import logging
29import warnings
30import re
31import traceback
32try:
33    from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
34except ImportError:
35    from http.server import BaseHTTPRequestHandler, HTTPServer
36
37from . import __author__, __copyright__, __license__, __version__
38from .simplexml import SimpleXMLElement, TYPE_MAP, Date, Decimal
39
40log = logging.getLogger(__name__)
41
42# Deprecated?
43NS_RX = re.compile(r'xmlns:(\w+)="(.+?)"')
44
45
46class SoapFault(Exception):
47    def __init__(self, faultcode=None, faultstring=None, detail=None):
48        self.faultcode = faultcode or self.__class__.__name__
49        self.faultstring = faultstring or ''
50        self.detail = detail
51
52
53class SoapDispatcher(object):
54    """Simple Dispatcher for SOAP Server"""
55
56    def __init__(self, name, documentation='', action='', location='',
57                 namespace=None, prefix=False,
58                 soap_uri="http://schemas.xmlsoap.org/soap/envelope/",
59                 soap_ns='soap',
60                 namespaces={},
61                 pretty=False,
62                 debug=False,
63                 **kwargs):
64        """
65        :param namespace: Target namespace; xmlns=targetNamespace
66        :param prefix: Prefix for target namespace; xmlns:prefix=targetNamespace
67        :param namespaces: Specify additional namespaces; example: {'external': 'http://external.mt.moboperator'}
68        :param pretty: Prettifies generated xmls
69        :param debug: Use to add tracebacks in generated xmls.
70
71        Multiple namespaces
72        ===================
73
74        It is possible to support multiple namespaces.
75        You need to specify additional namespaces by passing `namespace` parameter.
76
77        >>> dispatcher = SoapDispatcher(
78        ...    name = "MTClientWS",
79        ...    location = "http://localhost:8008/ws/MTClientWS",
80        ...    action = 'http://localhost:8008/ws/MTClientWS', # SOAPAction
81        ...    namespace = "http://external.mt.moboperator", prefix="external",
82        ...    documentation = 'moboperator MTClientWS',
83        ...    namespaces = {
84        ...        'external': 'http://external.mt.moboperator',
85        ...        'model': 'http://model.common.mt.moboperator'
86        ...    },
87        ...    ns = True)
88
89        Now the registered method must return node names with namespaces' prefixes.
90
91        >>> def _multi_ns_func(self, serviceMsisdn):
92        ...    ret = {
93        ...        'external:activateSubscriptionsReturn': [
94        ...            {'model:code': '0'},
95        ...            {'model:description': 'desc'},
96        ...        ]}
97        ...    return ret
98
99        Our prefixes will be changed to those used by the client.
100        """
101        self.methods = {}
102        self.name = name
103        self.documentation = documentation
104        self.action = action  # base SoapAction
105        self.location = location
106        self.namespace = namespace  # targetNamespace
107        self.prefix = prefix
108        self.soap_ns = soap_ns
109        self.soap_uri = soap_uri
110        self.namespaces = namespaces
111        self.pretty = pretty
112        self.debug = debug
113
114    @staticmethod
115    def _extra_namespaces(xml, ns):
116        """Extends xml with extra namespaces.
117        :param ns: dict with namespaceUrl:prefix pairs
118        :param xml: XML node to modify
119        """
120        if ns:
121            _tpl = 'xmlns:%s="%s"'
122            _ns_str = " ".join([_tpl % (prefix, uri) for uri, prefix in ns.items() if uri not in xml])
123            xml = xml.replace('/>', ' ' + _ns_str + '/>')
124        return xml
125
126    def register_function(self, name, fn, returns=None, args=None, doc=None, response_element_name=None):
127        self.methods[name] = fn, returns, args, doc or getattr(fn, "__doc__", ""), response_element_name or '%sResponse' % name
128
129    def response_element_name(self, method):
130        return self.methods[method][4]
131
132    def dispatch(self, xml, action=None, fault=None):
133        """Receive and process SOAP call, returns the xml"""
134        # a dict can be sent in fault to expose it to the caller
135        # default values:
136        prefix = self.prefix
137        ret = None
138        if fault is None:
139            fault = {}
140        soap_ns, soap_uri = self.soap_ns, self.soap_uri
141        soap_fault_code = 'VersionMismatch'
142        name = None
143
144        # namespaces = [('model', 'http://model.common.mt.moboperator'), ('external', 'http://external.mt.moboperator')]
145        _ns_reversed = dict(((v, k) for k, v in self.namespaces.items()))  # Switch keys-values
146        # _ns_reversed = {'http://external.mt.moboperator': 'external', 'http://model.common.mt.moboperator': 'model'}
147
148        try:
149            request = SimpleXMLElement(xml, namespace=self.namespace)
150
151            # detect soap prefix and uri (xmlns attributes of Envelope)
152            for k, v in request[:]:
153                if v in ("http://schemas.xmlsoap.org/soap/envelope/",
154                         "http://www.w3.org/2003/05/soap-env",
155                         "http://www.w3.org/2003/05/soap-envelope",):
156                    soap_ns = request.attributes()[k].localName
157                    soap_uri = request.attributes()[k].value
158
159                # If the value from attributes on Envelope is in additional namespaces
160                elif v in self.namespaces.values():
161                    _ns = request.attributes()[k].localName
162                    _uri = request.attributes()[k].value
163                    _ns_reversed[_uri] = _ns  # update with received alias
164                    # Now we change 'external' and 'model' to the received forms i.e. 'ext' and 'mod'
165                # After that we know how the client has prefixed additional namespaces
166
167            decoded_xml = xml if is_py2 else xml.decode('utf8')
168            ns = NS_RX.findall(decoded_xml)
169            for k, v in ns:
170                if v in self.namespaces.values():
171                    _ns_reversed[v] = k
172
173            soap_fault_code = 'Client'
174
175            # parse request message and get local method
176            method = request('Body', ns=soap_uri).children()(0)
177            if action:
178                # method name = action
179                name = action[len(self.action)+1:-1]
180                prefix = self.prefix
181            if not action or not name:
182                # method name = input message name
183                name = method.get_local_name()
184                prefix = method.get_prefix()
185
186            log.debug('dispatch method: %s', name)
187            function, returns_types, args_types, doc, response_element_name = self.methods[name]
188            log.debug('returns_types %s', returns_types)
189
190            # de-serialize parameters (if type definitions given)
191            if args_types:
192                args = method.children().unmarshall(args_types)
193            elif args_types is None:
194                args = {'request': method}  # send raw request
195            else:
196                args = {}  # no parameters
197
198            soap_fault_code = 'Server'
199            # execute function
200            ret = function(**args)
201            log.debug('dispathed method returns: %s', ret)
202
203        except SoapFault as e:
204            fault.update({
205                'faultcode': "%s.%s" % (soap_fault_code, e.faultcode),
206                'faultstring': e.faultstring,
207                'detail': e.detail
208            })
209
210        except Exception:  # This shouldn't be one huge try/except
211            import sys
212            etype, evalue, etb = sys.exc_info()
213            log.error(traceback.format_exc())
214            if self.debug:
215                detail = u''.join(traceback.format_exception(etype, evalue, etb))
216                detail += u'\n\nXML REQUEST\n\n' + xml.decode('UTF-8')
217            else:
218                detail = None
219            fault.update({'faultcode': "%s.%s" % (soap_fault_code, etype.__name__),
220                     'faultstring': evalue,
221                     'detail': detail})
222
223        # build response message
224        if not prefix:
225            xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s"/>"""
226        else:
227            xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s"
228                       xmlns:%(prefix)s="%(namespace)s"/>"""
229
230        xml %= {    # a %= {} is a shortcut for a = a % {}
231            'namespace': self.namespace,
232            'prefix': prefix,
233            'soap_ns': soap_ns,
234            'soap_uri': soap_uri
235        }
236
237        # Now we add extra namespaces
238        xml = SoapDispatcher._extra_namespaces(xml, _ns_reversed)
239
240        # Change our namespace alias to that given by the client.
241        # We put [('model', 'http://model.common.mt.moboperator'), ('external', 'http://external.mt.moboperator')]
242        # mix it with {'http://external.mt.moboperator': 'ext', 'http://model.common.mt.moboperator': 'mod'}
243        mapping = dict(((k, _ns_reversed[v]) for k, v in self.namespaces.items()))  # Switch keys-values and change value
244        # and get {'model': u'mod', 'external': u'ext'}
245
246        response = SimpleXMLElement(xml,
247                                    namespace=self.namespace,
248                                    namespaces_map=mapping,
249                                    prefix=prefix)
250
251        response['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
252        response['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema"
253
254        body = response.add_child("%s:Body" % soap_ns, ns=False)
255
256        if fault:
257            # generate a Soap Fault (with the python exception)
258            body.marshall("%s:Fault" % soap_ns, fault, ns=False)
259        else:
260            # return normal value
261            res = body.add_child(self.response_element_name(name), ns=self.namespace)
262            if not prefix:
263                res['xmlns'] = self.namespace  # add target namespace
264
265            # serialize returned values (response) if type definition available
266            if returns_types:
267                # TODO: full sanity check of type structure (recursive)
268                complex_type = isinstance(ret, dict)
269                if complex_type:
270                    # check if type mapping correlates with return value
271                    types_ok = all([k in returns_types for k in ret.keys()])
272                    if not types_ok:
273                        warnings.warn("Return value doesn't match type structure: "
274                                     "%s vs %s" % (str(returns_types), str(ret)))
275                if not complex_type or not types_ok:
276                    # backward compatibility for scalar and simple types
277                    res.marshall(list(returns_types.keys())[0], ret, )
278                else:
279                    # new style for complex classes
280                    for k, v in ret.items():
281                        res.marshall(k, v)
282            elif returns_types is None:
283                # merge xmlelement returned
284                res.import_node(ret)
285            elif returns_types == {}:
286                log.warning('Given returns_types is an empty dict.')
287
288        return response.as_xml(pretty=self.pretty)
289
290    # Introspection functions:
291
292    def list_methods(self):
293        """Return a list of aregistered operations"""
294        return [(method, doc) for method, (function, returns, args, doc, response_element_name) in self.methods.items()]
295
296    def help(self, method=None):
297        """Generate sample request and response messages"""
298        (function, returns, args, doc, response_element_name) = self.methods[method]
299        xml = """
300<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
301<soap:Body><%(method)s xmlns="%(namespace)s"/></soap:Body>
302</soap:Envelope>""" % {'method': method, 'namespace': self.namespace}
303        request = SimpleXMLElement(xml, namespace=self.namespace, prefix=self.prefix)
304        if args:
305            items = args.items()
306        elif args is None:
307            items = [('value', None)]
308        else:
309            items = []
310        for k, v in items:
311            request(method).marshall(k, v, add_comments=True, ns=False)
312
313        xml = """
314<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
315<soap:Body><%(response_element_name)s xmlns="%(namespace)s"/></soap:Body>
316</soap:Envelope>""" % {'response_element_name': response_element_name, 'namespace': self.namespace}
317        response = SimpleXMLElement(xml, namespace=self.namespace, prefix=self.prefix)
318        if returns:
319            items = returns.items()
320        elif args is None:
321            items = [('value', None)]
322        else:
323            items = []
324        for k, v in items:
325            response(response_element_name).marshall(k, v, add_comments=True, ns=False)
326
327        return request.as_xml(pretty=True), response.as_xml(pretty=True), doc
328
329    def wsdl(self):
330        """Generate Web Service Description v1.1"""
331        xml = """<?xml version="1.0"?>
332<wsdl:definitions name="%(name)s"
333          targetNamespace="%(namespace)s"
334          xmlns:tns="%(namespace)s"
335          xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
336          xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
337          xmlns:xsd="http://www.w3.org/2001/XMLSchema">
338    <wsdl:documentation xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">%(documentation)s</wsdl:documentation>
339
340    <wsdl:types>
341       <xsd:schema targetNamespace="%(namespace)s"
342              elementFormDefault="qualified"
343              xmlns:xsd="http://www.w3.org/2001/XMLSchema">
344       </xsd:schema>
345    </wsdl:types>
346
347</wsdl:definitions>
348""" % {'namespace': self.namespace, 'name': self.name, 'documentation': self.documentation}
349        wsdl = SimpleXMLElement(xml)
350
351        for method, (function, returns, args, doc, response_element_name) in self.methods.items():
352            # create elements:
353
354            def parse_element(name, values, array=False, complex=False):
355                if not complex:
356                    element = wsdl('wsdl:types')('xsd:schema').add_child('xsd:element')
357                    complex = element.add_child("xsd:complexType")
358                else:
359                    complex = wsdl('wsdl:types')('xsd:schema').add_child('xsd:complexType')
360                    element = complex
361                element['name'] = name
362                if values:
363                    items = values
364                elif values is None:
365                    items = [('value', None)]
366                else:
367                    items = []
368                if not array and items:
369                    all = complex.add_child("xsd:all")
370                elif items:
371                    all = complex.add_child("xsd:sequence")
372                for k, v in items:
373                    e = all.add_child("xsd:element")
374                    e['name'] = k
375                    if array:
376                        e[:] = {'minOccurs': "0", 'maxOccurs': "unbounded"}
377                    if v is None:
378                        t = 'xsd:anyType'
379                    elif type(v) == list:
380                        n = "ArrayOf%s%s" % (name, k)
381                        l = []
382                        for d in v:
383                            l.extend(d.items())
384                        parse_element(n, l, array=True, complex=True)
385                        t = "tns:%s" % n
386                    elif type(v) == dict:
387                        n = "%s%s" % (name, k)
388                        parse_element(n, v.items(), complex=True)
389                        t = "tns:%s" % n
390                    elif v in TYPE_MAP:
391                        t = 'xsd:%s' % TYPE_MAP[v]
392                    else:
393                        raise TypeError("unknown type %s for marshalling" % str(v))
394                    e.add_attribute('type', t)
395
396            parse_element("%s" % method, args and args.items())
397            parse_element(response_element_name, returns and returns.items())
398
399            # create messages:
400            for m, e in ('Input', method), ('Output', response_element_name):
401                message = wsdl.add_child('wsdl:message')
402                message['name'] = "%s%s" % (method, m)
403                part = message.add_child("wsdl:part")
404                part[:] = {'name': 'parameters',
405                           'element': 'tns:%s' % e}
406
407        # create ports
408        portType = wsdl.add_child('wsdl:portType')
409        portType['name'] = "%sPortType" % self.name
410        for method, (function, returns, args, doc, response_element_name) in self.methods.items():
411            op = portType.add_child('wsdl:operation')
412            op['name'] = method
413            if doc:
414                op.add_child("wsdl:documentation", doc)
415            input = op.add_child("wsdl:input")
416            input['message'] = "tns:%sInput" % method
417            output = op.add_child("wsdl:output")
418            output['message'] = "tns:%sOutput" % method
419
420        # create bindings
421        binding = wsdl.add_child('wsdl:binding')
422        binding['name'] = "%sBinding" % self.name
423        binding['type'] = "tns:%sPortType" % self.name
424        soapbinding = binding.add_child('soap:binding')
425        soapbinding['style'] = "document"
426        soapbinding['transport'] = "http://schemas.xmlsoap.org/soap/http"
427        for method in self.methods.keys():
428            op = binding.add_child('wsdl:operation')
429            op['name'] = method
430            soapop = op.add_child('soap:operation')
431            soapop['soapAction'] = self.action + method
432            soapop['style'] = 'document'
433            input = op.add_child("wsdl:input")
434            ##input.add_attribute('name', "%sInput" % method)
435            soapbody = input.add_child("soap:body")
436            soapbody["use"] = "literal"
437            output = op.add_child("wsdl:output")
438            ##output.add_attribute('name', "%sOutput" % method)
439            soapbody = output.add_child("soap:body")
440            soapbody["use"] = "literal"
441
442        service = wsdl.add_child('wsdl:service')
443        service["name"] = "%sService" % self.name
444        service.add_child('wsdl:documentation', text=self.documentation)
445        port = service.add_child('wsdl:port')
446        port["name"] = "%s" % self.name
447        port["binding"] = "tns:%sBinding" % self.name
448        soapaddress = port.add_child('soap:address')
449        soapaddress["location"] = self.location
450        return wsdl.as_xml(pretty=True)
451
452
453class SOAPHandler(BaseHTTPRequestHandler):
454
455    def do_GET(self):
456        """User viewable help information and wsdl"""
457        args = self.path[1:].split("?")
458        if self.path != "/" and args[0] not in self.server.dispatcher.methods.keys():
459            self.send_error(404, "Method not found: %s" % args[0])
460        else:
461            if self.path == "/":
462                # return wsdl if no method supplied
463                response = self.server.dispatcher.wsdl()
464            else:
465                # return supplied method help (?request or ?response messages)
466                req, res, doc = self.server.dispatcher.help(args[0])
467                if len(args) == 1 or args[1] == "request":
468                    response = req
469                else:
470                    response = res
471            self.send_response(200)
472            self.send_header("Content-type", "text/xml")
473            self.end_headers()
474            self.wfile.write(response)
475
476    def do_POST(self):
477        """SOAP POST gateway"""
478        request = self.rfile.read(int(self.headers.get('content-length')))
479        # convert xml request to unicode (according to request headers)
480        if sys.version < '3':
481            encoding = self.headers.getparam("charset")
482        else:
483            encoding = self.headers.get_param("charset")
484        request = request.decode(encoding)
485        fault = {}
486        # execute the method
487        response = self.server.dispatcher.dispatch(request, fault=fault)
488        # check if fault dict was completed (faultcode, faultstring, detail)
489        if fault:
490            self.send_response(500)
491        else:
492            self.send_response(200)
493        self.send_header("Content-type", "text/xml")
494        self.end_headers()
495        self.wfile.write(response)
496
497
498class WSGISOAPHandler(object):
499
500    def __init__(self, dispatcher):
501        self.dispatcher = dispatcher
502
503    def __call__(self, environ, start_response):
504        return self.handler(environ, start_response)
505
506    def handler(self, environ, start_response):
507        if environ['REQUEST_METHOD'] == 'GET':
508            return self.do_get(environ, start_response)
509        elif environ['REQUEST_METHOD'] == 'POST':
510            return self.do_post(environ, start_response)
511        else:
512            start_response('405 Method not allowed', [('Content-Type', 'text/plain')])
513            return ['Method not allowed']
514
515    def do_get(self, environ, start_response):
516        path = environ.get('PATH_INFO').lstrip('/')
517        query = environ.get('QUERY_STRING')
518        if path != "" and path not in self.dispatcher.methods.keys():
519            start_response('404 Not Found', [('Content-Type', 'text/plain')])
520            return ["Method not found: %s" % path]
521        elif path == "":
522            # return wsdl if no method supplied
523            response = self.dispatcher.wsdl()
524        else:
525            # return supplied method help (?request or ?response messages)
526            req, res, doc = self.dispatcher.help(path)
527            if len(query) == 0 or query == "request":
528                response = req
529            else:
530                response = res
531        start_response('200 OK', [('Content-Type', 'text/xml'), ('Content-Length', str(len(response)))])
532        return [response]
533
534    def do_post(self, environ, start_response):
535        length = int(environ['CONTENT_LENGTH'])
536        request = environ['wsgi.input'].read(length)
537        response = self.dispatcher.dispatch(request)
538        start_response('200 OK', [('Content-Type', 'text/xml'), ('Content-Length', str(len(response)))])
539        return [response]
540
541
542if __name__ == "__main__":
543
544    dispatcher = SoapDispatcher(
545        name="PySimpleSoapSample",
546        location="http://localhost:8008/",
547        action='http://localhost:8008/',  # SOAPAction
548        namespace="http://example.com/pysimplesoapsamle/", prefix="ns0",
549        documentation='Example soap service using PySimpleSoap',
550        trace=True, debug=True,
551        ns=True)
552
553    def adder(p, c, dt=None):
554        """Add several values"""
555        dt = dt + datetime.timedelta(365)
556        return {'ab': p['a'] + p['b'], 'dd': c[0]['d'] + c[1]['d'], 'dt': dt}
557
558    def dummy(in0):
559        """Just return input"""
560        return in0
561
562    def echo(request):
563        """Copy request->response (generic, any type)"""
564        return request.value
565
566    dispatcher.register_function(
567        'Adder', adder,
568        returns={'AddResult': {'ab': int, 'dd': unicode, 'dt': datetime.date}},
569        args={'p': {'a': int, 'b': int}, 'dt': Date, 'c': [{'d': Decimal}]}
570    )
571
572    dispatcher.register_function(
573        'Dummy', dummy,
574        returns={'out0': str},
575        args={'in0': str}
576    )
577
578    dispatcher.register_function('Echo', echo)
579
580    if '--local' in sys.argv:
581
582        wsdl = dispatcher.wsdl()
583
584        for method, doc in dispatcher.list_methods():
585            request, response, doc = dispatcher.help(method)
586
587    if '--serve' in sys.argv:
588        log.info("Starting server...")
589        httpd = HTTPServer(("", 8008), SOAPHandler)
590        httpd.dispatcher = dispatcher
591        httpd.serve_forever()
592
593    if '--wsgi-serve' in sys.argv:
594        log.info("Starting wsgi server...")
595        from wsgiref.simple_server import make_server
596        application = WSGISOAPHandler(dispatcher)
597        wsgid = make_server('', 8008, application)
598        wsgid.serve_forever()
599
600    if '--consume' in sys.argv:
601        from .client import SoapClient
602        client = SoapClient(
603            location="http://localhost:8008/",
604            action='http://localhost:8008/',  # SOAPAction
605            namespace="http://example.com/sample.wsdl",
606            soap_ns='soap',
607            trace=True,
608            ns="ns0",
609        )
610        p = {'a': 1, 'b': 2}
611        c = [{'d': '1.20'}, {'d': '2.01'}]
612        response = client.Adder(p=p, dt='2010-07-24', c=c)
613        result = response.AddResult
614        log.info(int(result.ab))
615        log.info(str(result.dd))
616       
617    if '--consume-wsdl' in sys.argv:
618        from .client import SoapClient
619        client = SoapClient(
620            wsdl="http://localhost:8008/",
621        )
622        p = {'a': 1, 'b': 2}
623        c = [{'d': '1.20'}, {'d': '2.01'}]
624        dt = datetime.date.today()
625        response = client.Adder(p=p, dt=dt, c=c)
626        result = response['AddResult']
627        log.info(int(result['ab']))
628        log.info(str(result['dd']))
629
Note: See TracBrowser for help on using the repository browser.