source: fedd/fedd_util.py @ f8b118e

axis_examplecompt_changesinfo-opsversion-1.30version-2.00version-3.01version-3.02
Last change on this file since f8b118e was f8b118e, checked in by Ted Faber <faber@…>, 16 years ago

clean up service classes a bit

  • Property mode set to 100644
File size: 19.1 KB
Line 
1#!/usr/local/bin/python
2
3import os, sys
4import subprocess
5import tempfile
6import logging
7import copy
8
9from M2Crypto import SSL, X509, EVP
10from M2Crypto.m2xmlrpclib import SSL_Transport
11import M2Crypto.httpslib
12from pyasn1.codec.der import decoder
13
14from fedd_services import *
15from fedd_internal_services import *
16from service_error import *
17from xmlrpclib import ServerProxy, dumps, loads, Fault, Error, Binary
18
19
20# The version of M2Crypto on users is pretty old and doesn't have all the
21# features that are useful.  The legacy code is somewhat more brittle than the
22# main line, but will work.
23if "as_der" not in dir(EVP.PKey):
24    from asn1_raw import get_key_bits_from_file, get_key_bits_from_cert
25    legacy = True
26else:
27    legacy = False
28
29class fedd_ssl_context(SSL.Context):
30    """
31    Simple wrapper around an M2Crypto.SSL.Context to initialize it for fedd.
32    """
33    def __init__(self, my_cert, trusted_certs=None, password=None):
34        """
35        construct a fedd_ssl_context
36
37        @param my_cert: PEM file with my certificate in it
38        @param trusted_certs: PEM file with trusted certs in it (optional)
39        """
40        SSL.Context.__init__(self)
41
42        # load_cert takes a callback to get a password, not a password, so if
43        # the caller provided a password, this creates a nonce callback using a
44        # lambda form.
45        if password != None and not callable(password):
46            # This is cute.  password = lambda *args: password produces a
47            # function object that returns itself rather than one that returns
48            # the object itself.  This is because password is an object
49            # reference and after the assignment it's a lambda.  So we assign
50            # to a temp.
51            pwd = password
52            password =lambda *args: pwd
53
54        if password != None:
55            self.load_cert(my_cert, callback=password)
56        else:
57            self.load_cert(my_cert)
58        if trusted_certs != None: self.load_verify_locations(trusted_certs)
59        self.set_verify(SSL.verify_peer, 10)
60
61class fedid:
62    """
63    Wrapper around the federated ID from an X509 certificate.
64    """
65    HASHSIZE=20
66    def __init__(self, bits=None, hexstr=None, cert=None, file=None):
67        if bits != None:
68            self.set_bits(bits)
69        elif hexstr != None:
70            self.set_hexstr(hexstr)
71        elif cert != None:
72            self.set_cert(cert)
73        elif file != None:
74            self.set_file(file)
75        else:
76            self.buf = None
77
78    def __hash__(self):
79        return hash(self.buf)
80
81    def __eq__(self, other):
82        if isinstance(other, type(self)):
83            return self.buf == other.buf
84        elif isinstance(other, type(str())):
85            return self.buf == other;
86        else:
87            return False
88
89    def __ne__(self, other): return not self.__eq__(other)
90
91    def __str__(self):
92        if self.buf != None:
93            return str().join([ "%02x" % ord(x) for x in self.buf])
94        else: return ""
95
96    def __repr__(self):
97        return "fedid(hexstr='%s')" % self.__str__()
98
99    def pack_soap(self):
100        return self.buf
101
102    def pack_xmlrpc(self):
103        return self.buf
104
105    def digest_bits(self, bits):
106        """Internal function.  Compute the fedid from bits and store in buf"""
107        d = EVP.MessageDigest('sha1')
108        d.update(bits)
109        self.buf = d.final()
110
111
112    def set_hexstr(self, hexstr):
113        h = hexstr.replace(':','')
114        self.buf= str().join([chr(int(h[i:i+2],16)) \
115                for i in range(0,2*fedid.HASHSIZE,2)])
116
117    def get_hexstr(self):
118        """Return the hexstring representation of the fedid"""
119        return __str__(self)
120
121    def set_bits(self, bits):
122        """Set the fedid to bits(a 160 bit buffer)"""
123        self.buf = bits
124
125    def get_bits(self):
126        """Get the 160 bit buffer from the fedid"""
127        return self.buf
128
129    def set_file(self, file):
130        """Get the fedid from a certificate file
131
132        Calculate the SHA1 hash over the bit string of the public key as
133        defined in RFC3280.
134        """
135        self.buf = None
136        if legacy: self.digest_bits(get_key_bits_from_file(file))
137        else: self.set_cert(X509.load_cert(file))
138
139    def set_cert(self, cert):
140        """Get the fedid from a certificate.
141
142        Calculate the SHA1 hash over the bit string of the public key as
143        defined in RFC3280.
144        """
145
146        self.buf = None
147        if (cert != None):
148            if legacy:
149                self.digest_bits(get_key_bits_from_cert(cert))
150            else:
151                b = []
152                k = cert.get_pubkey()
153
154                # Getting the key was easy, but getting the bit string of the
155                # key requires a side trip through ASN.1
156                dec = decoder.decode(k.as_der())
157
158                # kv is a tuple of the bits in the key.  The loop below
159                # recombines these into bytes and then into a buffer for the
160                # SSL digest function.
161                kv =  dec[0].getComponentByPosition(1)
162                for i in range(0, len(kv), 8):
163                    v = 0
164                    for j in range(0, 8):
165                        v = (v << 1) + kv[i+j]
166                    b.append(v)
167                # The comprehension turns b from a list of bytes into a buffer
168                # (string) of bytes
169                self.digest_bits(str().join([chr(x) for x in b]))
170
171def pack_id(id):
172    """
173    Return a dictionary with the field name set by the id type.  Handy for
174    creating dictionaries to be converted to messages.
175    """
176    if isinstance(id, fedid): return { 'fedid': id }
177    elif id.startswith("http:") or id.startswith("https:"): return { 'uri': id }
178    else: return { 'localname': id}
179
180def unpack_id(id):
181    """return id as a type determined by the key"""
182    for k in ("localname", "fedid", "uri", "kerberosUsername"):
183        if id.has_key(k): return id[k]
184    return None
185
186def generate_fedid(subj, bits=2048, log=None, dir=None, trace=sys.stderr,
187        ssl_prog="/usr/bin/openssl"):
188    """
189    Create a new certificate and derive a fedid from it.
190
191    The fedid and the certificate are returned as a tuple.
192    """
193
194    keypath = None
195    certpath = None
196    try:
197        try:
198            kd, keypath = tempfile.mkstemp(dir=dir, prefix="key",
199                    suffix=".pem")
200            cd, certpath = tempfile.mkstemp(dir=dir, prefix="cert",
201                    suffix=".pem")
202
203            cmd = [ssl_prog, "req", "-text", "-newkey", 
204                    "rsa:%d" % bits, "-keyout", keypath,  "-nodes", 
205                    "-subj", "/CN=%s" % subj, "-x509", "-days", "30", 
206                    "-out", certpath]
207
208            if log:
209                log.debug("[generate_fedid] %s" % " ".join(cmd))
210
211            if trace: call_out = trace
212            else: 
213                call_out = open("/dev/null", "w")
214
215            rv = subprocess.call(cmd, stdout=call_out, stderr=call_out)
216            log.debug("rv = %d" % rv)
217            if rv == 0:
218                cert = ""
219                for p in (certpath, keypath):
220                    f = open(p)
221                    for line in f:
222                        cert += line
223               
224                fid = fedid(file=certpath)
225                return (fid, cert)
226            else:
227                return (None, None)
228        except IOError, e:
229            raise e
230    finally:
231        if keypath: os.remove(keypath)
232        if certpath: os.remove(certpath)
233
234# Used by the remote_service_base class.
235def to_binary(o):
236    """
237    A function that converts an object into an xmlrpclib.Binary using
238    either its internal packing method, or the standard Binary constructor.
239    """
240    pack = getattr(o, 'pack_xmlrpc', None)
241    if callable(pack): return Binary(pack())
242    else: return Binary(o)
243
244# Classes that encapsulate the process of making and dealing with requests to
245# WSDL-generated and XMLRPC remote accesses. 
246
247class remote_service_base:
248    """
249    This invisible base class encapsulates the functions used to massage the
250    dictionaries used to pass parameters into and out of the RPC formats.  It's
251    mostly a container for the static methods to do that work, but defines some
252    maps sued by sub classes on apply_to_tags
253    """
254    # A map used to convert fedid fields to fedid objects (when the field is
255    # already a string)
256    fedid_to_object = ( ('fedid', lambda x: fedid(bits=x)),)
257    # A map used by apply_to_tags to convert fedids from xmlrpclib.Binary
258    # objects to fedid objects in one sweep.
259    decap_fedids = (('fedid', lambda x: fedid(bits=x.data)),)
260    # A map used to encapsulate fedids into xmlrpclib.Binary objects
261    encap_fedids = (('fedid', to_binary),)
262
263    @staticmethod
264    def pack_soap(container, name, contents):
265        """
266        Convert the dictionary in contents into a tree of ZSI classes.
267
268        The holder classes are constructed from factories in container and
269        assigned to either the element or attribute name.  This is used to
270        recursively create the SOAP message.
271        """
272        if getattr(contents, "__iter__", None) != None:
273            attr =getattr(container, "new_%s" % name, None)
274            if attr: obj = attr()
275            else:
276                raise TypeError("%s does not have a new_%s attribute" % \
277                        (container, name))
278            for e, v in contents.iteritems():
279                assign = getattr(obj, "set_element_%s" % e, None) or \
280                        getattr(obj, "set_attribute_%s" % e, None)
281                if isinstance(v, type(dict())):
282                    assign(remote_service_base.pack_soap(obj, e, v))
283                elif getattr(v, "__iter__", None) != None:
284                    assign([ remote_service_base.pack_soap(obj, e, val ) \
285                            for val in v])
286                elif getattr(v, "pack_soap", None) != None:
287                    assign(v.pack_soap())
288                else:
289                    assign(v)
290            return obj
291        else: return contents
292
293    @staticmethod
294    def unpack_soap(element):
295        """
296        Convert a tree of ZSI SOAP classes intro a hash.  The inverse of
297        pack_soap
298
299        Elements or elements that are empty are ignored.
300        """
301        methods = [ m for m in dir(element) \
302                if m.startswith("get_element") or m.startswith("get_attribute")]
303        if len(methods) > 0:
304            rv = { }
305            for m in methods:
306                if m.startswith("get_element_"):
307                    n = m.replace("get_element_","",1)
308                else:
309                    n = m.replace("get_attribute_", "", 1)
310                sub = getattr(element, m)()
311                if sub != None:
312                    if isinstance(sub, basestring):
313                        rv[n] = sub
314                    elif getattr(sub, "__iter__", None) != None:
315                        if len(sub) > 0: rv[n] = \
316                                [remote_service_base.unpack_soap(e) \
317                                    for e in sub]
318                    else:
319                        rv[n] = remote_service_base.unpack_soap(sub)
320            return rv
321        else: 
322            return element
323
324    @staticmethod
325    def apply_to_tags(e, map):
326        """
327        Map is an iterable of ordered pairs (tuples) that map a key to a
328        function.
329        This function walks the given message and replaces any object with a
330        key in the map with the result of applying that function to the object.
331        """
332        if isinstance(e, dict):
333            for k in e.keys():
334                for tag, fcn in map:
335                    if k == tag:
336                        if isinstance(e[k], list):
337                            e[k] = [ fcn(b) for b in e[k]]
338                        else:
339                            e[k] = fcn(e[k])
340                    elif isinstance(e[k], dict):
341                        remote_service_base.apply_to_tags(e[k], map)
342                    elif isinstance(e[k], list):
343                        for ee in e[k]:
344                            remote_service_base.apply_to_tags(ee, map)
345        # Other types end the recursion - they should be leaves
346        return e
347
348    @staticmethod
349    def strip_unicode(obj):
350        """Walk through a message and convert all strings to non-unicode
351        strings"""
352        if isinstance(obj, dict):
353            for k in obj.keys():
354                obj[k] = remote_service_base.strip_unicode(obj[k])
355            return obj
356        elif isinstance(obj, basestring) and not isinstance(obj, str):
357            return str(obj)
358        elif getattr(obj, "__iter__", None):
359            return [ remote_service_base.strip_unicode(x) for x in obj]
360        else:
361            return obj
362
363    @staticmethod
364    def make_unicode(obj):
365        """Walk through a message and convert all strings to unicode"""
366        if isinstance(obj, dict):
367            for k in obj.keys():
368                obj[k] = remote_service_base.make_unicode(obj[k])
369            return obj
370        elif isinstance(obj, basestring) and not isinstance(obj, unicode):
371            return unicode(obj)
372        elif getattr(obj, "__iter__", None):
373            return [ remote_service_base.make_unicode(x) for x in obj]
374        else:
375            return obj
376
377
378
379class soap_handler(remote_service_base):
380    """
381    Encapsulate the handler code to unpack and pack SOAP requests and responses
382    and call the given method.
383
384    The code to decapsulate and encapsulate parameters encoded in SOAP is the
385    same modulo a few parameters.  This is a functor that calls a fedd service
386    trhough a soap interface.  The parameters are the typecode of the request
387    parameters, the method to call (usually a bound instance of a method on a
388    fedd service providing class), the constructor of a response packet and the
389    name of the body element of that packet.  The handler takes a ParsedSoap
390    object (the request) and returns an instance of the class created by
391    constructor containing the response.  Failures of the constructor or badly
392    created constructors will result in None being returned.
393    """
394    def __init__(self, typecode, method, constructor, body_name):
395        self.typecode = typecode
396        self.method = method
397        self.constructor = constructor
398        self.body_name = body_name
399
400    def __call__(self, ps, fid):
401        req = ps.Parse(self.typecode)
402        # Convert the message to a dict with the fedid strings converted to
403        # fedid objects
404        req = self.apply_to_tags(self.unpack_soap(req), self.fedid_to_object)
405
406        msg = self.method(req, fid)
407
408        resp = self.constructor()
409        set_element = getattr(resp, "set_element_%s" % self.body_name, None)
410        if set_element and callable(set_element):
411            try:
412                set_element(self.pack_soap(resp, self.body_name, msg))
413                return resp
414            except (NameError, TypeError):
415                return None
416        else:
417            return None
418
419class xmlrpc_handler(remote_service_base):
420    """
421    Generate the handler code to unpack and pack XMLRPC requests and responses
422    and call the given method.
423
424    The code to marshall and unmarshall XMLRPC parameters to and from a fedd
425    service is largely the same.  This helper creates such a handler.  The
426    parameters are the method name, and the name of the body struct that
427    contains the response.  A handler is created that takes the params response
428    from an xmlrpclib.loads on the incoming rpc and a fedid and responds with
429    a hash representing the struct ro be returned to the other side.  On error
430    None is returned.  Fedid fields are decapsulated from binary and converted
431    to fedid objects on input and encapsulated as Binaries on output.
432    """
433    def __init__(self, method, body_name):
434        self.method = method
435        self.body_name = body_name
436
437    def __call__(self, params, fid):
438        msg = None
439
440        p = self.apply_to_tags(params[0], self.decap_fedids)
441        try:
442            msg = self.method(p, fid)
443        except service_error, e:
444            raise Fault(e.code_string(), e.desc)
445        if msg != None:
446            return self.make_unicode(self.apply_to_tags(\
447                    { self.body_name: msg }, self.encap_fedids))
448        else:
449            return None
450
451class service_caller(remote_service_base):
452    def __init__(self, service_name, port_name, locator, request_message, 
453            request_body_name, tracefile=None):
454        self.service_name = service_name
455        self.port_name = port_name
456        self.locator = locator
457        self.request_message = request_message
458        self.request_body_name = request_body_name
459        self.tracefile = tracefile
460        self.__call__ = self.call_service
461
462    def call_xmlrpc_service(self, url, req, cert_file=None, cert_pwd=None, 
463            trusted_certs=None, context=None, tracefile=None):
464        """Send an XMLRPC request.  """
465
466
467        # If a context is given, use it.  Otherwise construct one from
468        # components.  The construction shouldn't call out for passwords.
469        if context:
470            ctx = context
471        else:
472            try:
473                ctx = fedd_ssl_context(cert_file, trusted_certs, 
474                        password=cert_pwd)
475            except SSL.SSLError:
476                raise service_error(service_error.server_config,
477                        "Certificates misconfigured")
478
479        # Of all the dumbass things.  The XMLRPC library in use here won't
480        # properly encode unicode strings, so we make a copy of req with
481        # the unicode objects converted.  We also convert the url to a
482        # basic string if it isn't one already.
483        r = self.strip_unicode(copy.deepcopy(req))
484        url = str(url)
485       
486        transport = SSL_Transport(ctx)
487        port = ServerProxy(url, transport=transport)
488        # Make the call, and convert faults back to service_errors
489        try:
490            remote_method = getattr(port, self.service_name, None)
491            resp = remote_method(self.apply_to_tags(\
492                    { self.request_body_name: r}, self.encap_fedids))
493        except Fault, f:
494            raise service_error(None, f.faultString, f.faultCode)
495        except Error, e:
496            raise service_error(service_error.protocol, 
497                    "Remote XMLRPC Fault: %s" % e)
498
499        return self.apply_to_tags(resp, self.decap_fedids) 
500
501    def call_soap_service(self, url, req, cert_file=None, cert_pwd=None,
502            trusted_certs=None, context=None, tracefile=None):
503        """
504        Send req on to the real destination in dt and return the response
505
506        Req is just the requestType object.  This function re-wraps it.  It
507        also rethrows any faults.
508        """
509
510        tf = tracefile or self.tracefile or None
511
512        # If a context is given, use it.  Otherwise construct one from
513        # components.  The construction shouldn't call out for passwords.
514        if context:
515            ctx = context
516        else:
517            try:
518                ctx = fedd_ssl_context(cert_file, trusted_certs, 
519                        password=cert_pwd)
520            except SSL.SSLError:
521                raise service_error(service_error.server_config,
522                        "Certificates misconfigured")
523        loc = self.locator()
524        get_port = getattr(loc, self.port_name, None)
525        if not get_port:
526            raise service_error(service_error.internal, 
527                    "Cannot get port %s from locator" % self.port_name)
528        port = get_port(url,
529                transport=M2Crypto.httpslib.HTTPSConnection, 
530                transdict={ 'ssl_context' : ctx },
531                tracefile=tf)
532        remote_method = getattr(port, self.service_name, None)
533        if not remote_method:
534            raise service_error(service_error.internal,
535                    "Cannot get service from SOAP port")
536
537        # Reconstruct the full request message
538        msg = self.request_message()
539        set_element = getattr(msg, "set_element_%s" % self.request_body_name,
540                None)
541        if not set_element:
542            raise service_error(service_error.internal,
543                    "Cannot get element setting method for %s" % \
544                            self.request_body_name)
545        set_element(self.pack_soap(msg, self.request_body_name, req))
546        try:
547            resp = remote_method(msg)
548        except ZSI.ParseException, e:
549            raise service_error(service_error.protocol,
550                    "Bad format message (XMLRPC??): %s" % e)
551        except ZSI.FaultException, e:
552            ee = self.unpack_soap(e.fault.detail[0]).get('FeddFaultBody', { })
553            if ee:
554                raise service_error(ee['code'], ee['desc'])
555            else:
556                raise service_error(service_error.internal,
557                        "Unexpected fault body")
558        # Unpack and convert fedids to objects
559        r = self.apply_to_tags(self.unpack_soap(resp), self.fedid_to_object)
560        #  Make sure all strings are unicode
561        r = self.make_unicode(r)
562        return r
563
564    def call_service(self, url, req, cert_file=None, cert_pwd=None, 
565        trusted_certs=None, context=None, tracefile=None):
566        p_fault = None  # Any SOAP failure (sent unless XMLRPC works)
567        resp = None
568        try:
569            # Try the SOAP request
570            resp = self.call_soap_service(url, req, 
571                    cert_file, cert_pwd, trusted_certs, context, tracefile)
572            return resp
573        except service_error, e:
574            if e.code == service_error.protocol: p_fault = None
575            else: raise
576        except ZSI.FaultException, f:
577            p_fault = f.fault.detail[0]
578               
579
580        # If we could not get a valid SOAP response to the request above,
581        # try the same address using XMLRPC and let any faults flow back
582        # out.
583        if p_fault == None:
584            resp = self.call_xmlrpc_service(url, req, cert_file,
585                    cert_pwd, trusted_certs, context, tracefile)
586            return resp
587        else:
588            # Build the fault
589            ee = unpack_soap(p_fault).get('FeddFaultBody', { })
590            if ee:
591                raise service_error(ee['code'], ee['desc'])
592            else:
593                raise service_error(service_error.internal,
594                        "Unexpected fault body")
595
596
597def set_log_level(config, sect, log):
598    """ Set the logging level to the value passed in sect of config."""
599    # The getattr sleight of hand finds the logging level constant
600    # corrersponding to the string.  We're a little paranoid to avoid user
601    # mayhem.
602    if config.has_option(sect, "log_level"):
603        level_str = config.get(sect, "log_level")
604        try:
605            level = int(getattr(logging, level_str.upper(), -1))
606
607            if  logging.DEBUG <= level <= logging.CRITICAL:
608                log.setLevel(level)
609            else:
610                log.error("Bad experiment_log value: %s" % level_str)
611
612        except ValueError:
613            log.error("Bad experiment_log value: %s" % level_str)
614
Note: See TracBrowser for help on using the repository browser.