source: fedd/federation/remote_service.py @ 2ee4226

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

More attempts to make the SSL more reliable on users. Not completely
successful.

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