source: fedd/federation/remote_service.py @ dc803a7

axis_examplecompt_changesinfo-opsversion-3.01version-3.02
Last change on this file since dc803a7 was c2c153b, checked in by Ted Faber <faber@…>, 15 years ago

another place SSL/HTTP can bedevil us

  • Property mode set to 100644
File size: 16.4 KB
Line 
1#!/usr/local/bin/python
2
3import copy
4
5from socket import error as socket_error
6
7import M2Crypto.httpslib
8from M2Crypto import SSL
9from M2Crypto.m2xmlrpclib import SSL_Transport
10from M2Crypto.SSL import SSLError
11from ZSI import ParseException, FaultException, SoapWriter
12
13# Underlying SOAP comms use this and we need to catch their exceptions
14import httplib
15
16from service_error import service_error
17from xmlrpclib import ServerProxy, dumps, loads, Fault, Error, Binary
18
19import fedd_services
20import fedd_internal_services
21from util import fedd_ssl_context
22from fedid import fedid
23import parse_detail
24
25# Turn off the matching of hostname to certificate ID
26SSL.Connection.clientPostConnectionCheck = None
27
28# Used by the remote_service_base class.
29def to_binary(o):
30    """
31    A function that converts an object into an xmlrpclib.Binary using
32    either its internal packing method, or the standard Binary constructor.
33    """
34    pack = getattr(o, 'pack_xmlrpc', None)
35    if callable(pack): return Binary(pack())
36    else: return Binary(o)
37
38# Classes that encapsulate the process of making and dealing with requests to
39# WSDL-generated and XMLRPC remote accesses. 
40
41class remote_service_base:
42    """
43    This invisible base class encapsulates the functions used to massage the
44    dictionaries used to pass parameters into and out of the RPC formats.  It's
45    mostly a container for the static methods to do that work, but defines some
46    maps sued by sub classes on apply_to_tags
47    """
48    # A map used to convert fedid fields to fedid objects (when the field is
49    # already a string)
50    fedid_to_object = ( ('fedid', lambda x: fedid(bits=x)),)
51    # A map used by apply_to_tags to convert fedids from xmlrpclib.Binary
52    # objects to fedid objects in one sweep.
53    decap_fedids = (('fedid', lambda x: fedid(bits=x.data)),)
54    # A map used to encapsulate fedids into xmlrpclib.Binary objects
55    encap_fedids = (('fedid', to_binary),)
56
57    @staticmethod
58    def pack_soap(container, name, contents):
59        """
60        Convert the dictionary in contents into a tree of ZSI classes.
61
62        The holder classes are constructed from factories in container and
63        assigned to either the element or attribute name.  This is used to
64        recursively create the SOAP message.
65        """
66        if getattr(contents, "__iter__", None) != None:
67            attr =getattr(container, "new_%s" % name, None)
68            if attr: obj = attr()
69            else:
70                raise TypeError("%s does not have a new_%s attribute" % \
71                        (container, name))
72            for e, v in contents.iteritems():
73                assign = getattr(obj, "set_element_%s" % e, None) or \
74                        getattr(obj, "set_attribute_%s" % e, None)
75                if isinstance(v, type(dict())):
76                    assign(remote_service_base.pack_soap(obj, e, v))
77                elif getattr(v, "__iter__", None) != None:
78                    assign([ remote_service_base.pack_soap(obj, e, val ) \
79                            for val in v])
80                elif getattr(v, "pack_soap", None) != None:
81                    assign(v.pack_soap())
82                else:
83                    assign(v)
84            return obj
85        else: return contents
86
87    @staticmethod
88    def unpack_soap(element):
89        """
90        Convert a tree of ZSI SOAP classes intro a hash.  The inverse of
91        pack_soap
92
93        Elements or elements that are empty are ignored.
94        """
95        methods = [ m for m in dir(element) \
96                if m.startswith("get_element") or m.startswith("get_attribute")]
97        if len(methods) > 0:
98            rv = { }
99            for m in methods:
100                if m.startswith("get_element_"):
101                    n = m.replace("get_element_","",1)
102                else:
103                    n = m.replace("get_attribute_", "", 1)
104                sub = getattr(element, m)()
105                if sub != None:
106                    if isinstance(sub, basestring):
107                        rv[n] = sub
108                    elif getattr(sub, "__iter__", None) != None:
109                        if len(sub) > 0: rv[n] = \
110                                [remote_service_base.unpack_soap(e) \
111                                    for e in sub]
112                    else:
113                        rv[n] = remote_service_base.unpack_soap(sub)
114            return rv
115        else: 
116            return element
117
118    @staticmethod
119    def apply_to_tags(e, map):
120        """
121        Map is an iterable of ordered pairs (tuples) that map a key to a
122        function.
123        This function walks the given message and replaces any object with a
124        key in the map with the result of applying that function to the object.
125        """
126        if isinstance(e, dict):
127            for k in e.keys():
128                for tag, fcn in map:
129                    if k == tag:
130                        if isinstance(e[k], list):
131                            e[k] = [ fcn(b) for b in e[k]]
132                        else:
133                            e[k] = fcn(e[k])
134                    elif isinstance(e[k], dict):
135                        remote_service_base.apply_to_tags(e[k], map)
136                    elif isinstance(e[k], list):
137                        for ee in e[k]:
138                            remote_service_base.apply_to_tags(ee, map)
139        # Other types end the recursion - they should be leaves
140        return e
141
142    @staticmethod
143    def strip_unicode(obj):
144        """Walk through a message and convert all strings to non-unicode
145        strings"""
146        if isinstance(obj, dict):
147            for k in obj.keys():
148                obj[k] = remote_service_base.strip_unicode(obj[k])
149            return obj
150        elif isinstance(obj, basestring) and not isinstance(obj, str):
151            return str(obj)
152        elif getattr(obj, "__iter__", None):
153            return [ remote_service_base.strip_unicode(x) for x in obj]
154        else:
155            return obj
156
157    @staticmethod
158    def make_unicode(obj):
159        """Walk through a message and convert all strings to unicode"""
160        if isinstance(obj, dict):
161            for k in obj.keys():
162                obj[k] = remote_service_base.make_unicode(obj[k])
163            return obj
164        elif isinstance(obj, basestring) and not isinstance(obj, unicode):
165            return unicode(obj)
166        elif getattr(obj, "__iter__", None):
167            return [ remote_service_base.make_unicode(x) for x in obj]
168        else:
169            return obj
170
171
172
173class soap_handler(remote_service_base):
174    """
175    Encapsulate the handler code to unpack and pack SOAP requests and responses
176    and call the given method.
177
178    The code to decapsulate and encapsulate parameters encoded in SOAP is the
179    same modulo a few parameters.  This is a functor that calls a fedd service
180    trhough a soap interface.  The parameters are the typecode of the request
181    parameters, the method to call (usually a bound instance of a method on a
182    fedd service providing class), the constructor of a response packet and the
183    name of the body element of that packet.  The handler takes a ParsedSoap
184    object (the request) and returns an instance of the class created by
185    constructor containing the response.  Failures of the constructor or badly
186    created constructors will result in None being returned.
187    """
188    def __init__(self, service_name, method, typecode=None,
189            constructor=None, body_name=None):
190        self.method = method
191
192        response_class_name = "%sResponseMessage" % service_name
193        request_class_name = "%sRequestMessage" % service_name
194
195        if body_name: self.body_name = body_name
196        else: self.body_name = "%sResponseBody" % service_name
197
198        if constructor: self.constructor = constructor
199        else:
200            self.constructor = self.get_class(response_class_name)
201            if not self.constructor:
202                raise service_error(service_error.internal,
203                        "Cannot find class for %s" % response_class_name)
204
205        if typecode: self.typecode = typecode
206        else: 
207            req = self.get_class(request_class_name)
208            if req:
209                self.typecode = req.typecode
210            else:
211                raise service_error(service_error.internal,
212                        "Cannot find class for %s" % request_class_name)
213
214            if not self.typecode:
215                raise service_error(service_error.internal,
216                        "Cannot get typecode for %s" % class_name)
217
218    def get_class(self, class_name):
219        return getattr(fedd_services, class_name, None) or \
220                getattr(fedd_internal_services, class_name, None)
221
222    def __call__(self, ps, fid):
223        req = ps.Parse(self.typecode)
224        # Convert the message to a dict with the fedid strings converted to
225        # fedid objects
226        req = self.apply_to_tags(self.unpack_soap(req), self.fedid_to_object)
227
228        msg = self.method(req, fid)
229
230        resp = self.constructor()
231        set_element = getattr(resp, "set_element_%s" % self.body_name, None)
232        if set_element and callable(set_element):
233            try:
234                set_element(self.pack_soap(resp, self.body_name, msg))
235                return resp
236            except (NameError, TypeError):
237                return None
238        else:
239            return None
240
241class xmlrpc_handler(remote_service_base):
242    """
243    Generate the handler code to unpack and pack XMLRPC requests and responses
244    and call the given method.
245
246    The code to marshall and unmarshall XMLRPC parameters to and from a fedd
247    service is largely the same.  This helper creates such a handler.  The
248    parameters are the method name, and the name of the body struct that
249    contains the response.  A handler is created that takes the params response
250    from an xmlrpclib.loads on the incoming rpc and a fedid and responds with
251    a hash representing the struct ro be returned to the other side.  On error
252    None is returned.  Fedid fields are decapsulated from binary and converted
253    to fedid objects on input and encapsulated as Binaries on output.
254    """
255    def __init__(self, service_name, method):
256        self.method = method
257        self.body_name = "%sResponseBody" % service_name
258
259    def __call__(self, params, fid):
260        msg = None
261
262        p = self.apply_to_tags(params[0], self.decap_fedids)
263        try:
264            msg = self.method(p, fid)
265        except service_error, e:
266            raise Fault(e.code, "%s: %s" % (e.code_string(), e.desc))
267        if msg != None:
268            return self.make_unicode(self.apply_to_tags(\
269                    { self.body_name: msg }, self.encap_fedids))
270        else:
271            return None
272
273class service_caller(remote_service_base):
274    def __init__(self, service_name, request_message=None, 
275            request_body_name=None, tracefile=None, strict=True):
276        self.service_name = service_name
277
278        if getattr(fedd_services.feddBindingSOAP, service_name, None):
279            self.locator = fedd_services.feddServiceLocator
280            self.port_name = 'getfeddPortType'
281        elif getattr(fedd_internal_services.feddInternalBindingSOAP, 
282                service_name, None):
283            self.locator = fedd_internal_services.feddInternalServiceLocator
284            self.port_name = 'getfeddInternalPortType'
285
286        if request_message: self.request_message = request_message
287        else:
288            request_message_name = "%sRequestMessage" % service_name
289            self.request_message = \
290                    getattr(fedd_services, request_message_name, None) or \
291                    getattr(fedd_internal_services, request_message_name, None)
292            if not self.request_message and strict:
293                raise service_error(service_error.internal,
294                        "Cannot find class for %s" % request_message_name)
295
296        if request_body_name is not None:
297            self.request_body_name = request_body_name
298        else: 
299            self.request_body_name = "%sRequestBody" % service_name
300
301        self.tracefile = tracefile
302        self.__call__ = self.call_service
303        self.max_retries = 5
304
305    def serialize_soap(self, req):
306        """
307        Return a string containing the message that would be sent to call this
308        service with the given request.
309        """
310        msg = self.request_message()
311        set_element = getattr(msg, "set_element_%s" % self.request_body_name,
312                None)
313        if not set_element:
314            raise service_error(service_error.internal,
315                    "Cannot get element setting method for %s" % \
316                            self.request_body_name)
317        set_element(self.pack_soap(msg, self.request_body_name, req))
318        sw = SoapWriter()
319        sw.serialize(msg)
320        return unicode(sw)
321
322    def call_xmlrpc_service(self, url, req, cert_file=None, cert_pwd=None, 
323            trusted_certs=None, context=None, tracefile=None):
324        """Send an XMLRPC request.  """
325
326
327        # If a context is given, use it.  Otherwise construct one from
328        # components.  The construction shouldn't call out for passwords.
329        if context:
330            ctx = context
331        else:
332            try:
333                ctx = fedd_ssl_context(cert_file, trusted_certs, 
334                        password=cert_pwd)
335            except SSL.SSLError:
336                raise service_error(service_error.server_config,
337                        "Certificates misconfigured")
338
339        # Of all the dumbass things.  The XMLRPC library in use here won't
340        # properly encode unicode strings, so we make a copy of req with
341        # the unicode objects converted.  We also convert the url to a
342        # basic string if it isn't one already.
343        r = self.strip_unicode(copy.deepcopy(req))
344        url = str(url)
345       
346        transport = SSL_Transport(ctx)
347        port = ServerProxy(url, transport=transport)
348        # Make the call, and convert faults back to service_errors
349        if self.request_body_name:
350            r  = self.apply_to_tags(\
351                    { self.request_body_name: r}, self.encap_fedids)
352        else:
353            r = self.apply_to_tags(r, self.encap_fedids)
354
355        ok = False
356        retries = 0
357
358        while not ok and retries < self.max_retries:
359            try:
360                remote_method = getattr(port, self.service_name, None)
361                resp = remote_method(r)
362                ok = True
363            except socket_error, e:
364                raise service_error(service_error.connect, 
365                        "Cannot connect to %s: %s" % (url, e[1]))
366            except SSLError, e:
367                print "SSL error contacting %s: %s" % (url, e.message)
368                retries += 1
369            except httplib.HTTPException, e:
370                print "HTTP error contacting %s: %s" % (url, e.message)
371                retries +=1
372            except Fault, f:
373                raise service_error(f.faultCode, f.faultString)
374            except Error, e:
375                raise service_error(service_error.protocol, 
376                        "Remote XMLRPC Fault: %s" % e)
377
378        if retries > self.max_retries :
379            raise service_error(req.connect, "Too many SSL failures")
380
381        return self.apply_to_tags(resp, self.decap_fedids) 
382
383    def call_soap_service(self, url, req, cert_file=None, cert_pwd=None,
384            trusted_certs=None, context=None, tracefile=None):
385        """
386        Send req on to the real destination in dt and return the response
387
388        Req is just the requestType object.  This function re-wraps it.  It
389        also rethrows any faults.
390        """
391
392        tf = tracefile or self.tracefile or None
393
394        if not self.request_body_name:
395            raise service_error(service_error.internal, 
396                    "Call to soap service without a configured request body");
397
398        # If a context is given, use it.  Otherwise construct one from
399        # components.  The construction shouldn't call out for passwords.
400        if context:
401            ctx = context
402        else:
403            try:
404                ctx = fedd_ssl_context(cert_file, trusted_certs, 
405                        password=cert_pwd)
406            except SSL.SSLError:
407                raise service_error(service_error.server_config,
408                        "Certificates misconfigured")
409        loc = self.locator()
410        get_port = getattr(loc, self.port_name, None)
411        if not get_port:
412            raise service_error(service_error.internal, 
413                    "Cannot get port %s from locator" % self.port_name)
414        port = get_port(url,
415                transport=M2Crypto.httpslib.HTTPSConnection, 
416                transdict={ 'ssl_context' : ctx },
417                tracefile=tf)
418        remote_method = getattr(port, self.service_name, None)
419        if not remote_method:
420            raise service_error(service_error.internal,
421                    "Cannot get service from SOAP port")
422
423        # Reconstruct the full request message
424        msg = self.request_message()
425        set_element = getattr(msg, "set_element_%s" % self.request_body_name,
426                None)
427        if not set_element:
428            raise service_error(service_error.internal,
429                    "Cannot get element setting method for %s" % \
430                            self.request_body_name)
431        set_element(self.pack_soap(msg, self.request_body_name, req))
432        ok = False
433        retries = 0
434        while not ok and retries < self.max_retries:
435            try:
436                resp = remote_method(msg)
437                ok = True
438            except socket_error, e:
439                raise service_error(service_error.connect, 
440                        "Cannot connect to %s: %s" % (url, e[1]))
441            except SSLError, e:
442                print "SSL error contacting %s: %s" % (url, e.message)
443                retries += 1
444            except httplib.HTTPException, e:
445                print "HTTP error contacting %s: %s" % (url, e.message)
446                retries +=1
447            except ParseException, e:
448                raise service_error(service_error.protocol,
449                        "Bad format message (XMLRPC??): %s" % e)
450            except FaultException, e:
451                ee = self.unpack_soap(e.fault.detail[0]).get('FeddFaultBody',
452                        { })
453                if ee:
454                    raise service_error(ee['code'], ee['desc'])
455                else:
456                    raise service_error(service_error.internal,
457                            "Unexpected fault body")
458
459        if retries > self.max_retries :
460            raise service_error(req.connect, "Too many SSL failures")
461
462        # Unpack and convert fedids to objects
463        r = self.apply_to_tags(self.unpack_soap(resp), self.fedid_to_object)
464
465        #  Make sure all strings are unicode
466        r = self.make_unicode(r)
467        return r
468
469    def call_service(self, url, req, cert_file=None, cert_pwd=None, 
470        trusted_certs=None, context=None, tracefile=None):
471        p_fault = None  # Any SOAP failure (sent unless XMLRPC works)
472        resp = None
473        try:
474            # Try the SOAP request
475            resp = self.call_soap_service(url, req, 
476                    cert_file, cert_pwd, trusted_certs, context, tracefile)
477            return resp
478        except service_error, e:
479            if e.code == service_error.protocol: p_fault = None
480            else: raise
481        except FaultException, f:
482            p_fault = f.fault.detail[0]
483               
484
485        # If we could not get a valid SOAP response to the request above,
486        # try the same address using XMLRPC and let any faults flow back
487        # out.
488        if p_fault == None:
489            resp = self.call_xmlrpc_service(url, req, cert_file,
490                    cert_pwd, trusted_certs, context, tracefile)
491            return resp
492        else:
493            # Build the fault
494            ee = unpack_soap(p_fault).get('FeddFaultBody', { })
495            if ee:
496                raise service_error(ee['code'], ee['desc'])
497            else:
498                raise service_error(service_error.internal,
499                        "Unexpected fault body")
Note: See TracBrowser for help on using the repository browser.