source: fedd/fedd_util.py @ 058f58e

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

Unify the code for calling SOAP and XMLRPC services into a couple classes.
Before there were slightly different semantics everywhere.

Also make the handlers classes rather than the output of stub compiling
functions.

  • Property mode set to 100644
File size: 18.4 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, type(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    if id.has_key("fedid"): return fedid(id["fedid"])
183    else:
184        for k in ("localname", "uri", "kerberosUsername"):
185            if id.has_key(k): return id[k]
186    return None
187
188def pack_soap(container, name, contents):
189    """
190    Convert the dictionary in contents into a tree of ZSI classes.
191
192    The holder classes are constructed from factories in container and assigned
193    to either the element or attribute name.  This is used to recursively
194    create the SOAP message.
195    """
196    if getattr(contents, "__iter__", None) != None:
197        attr =getattr(container, "new_%s" % name, None)
198        if attr: obj = attr()
199        else:
200            raise TypeError("%s does not have a new_%s attribute" % \
201                    (container, name))
202        for e, v in contents.iteritems():
203            assign = getattr(obj, "set_element_%s" % e, None) or \
204                    getattr(obj, "set_attribute_%s" % e, None)
205            if isinstance(v, type(dict())):
206                assign(pack_soap(obj, e, v))
207            elif getattr(v, "__iter__", None) != None:
208                assign([ pack_soap(obj, e, val ) for val in v])
209            elif getattr(v, "pack_soap", None) != None:
210                assign(v.pack_soap())
211            else:
212                assign(v)
213        return obj
214    else: return contents
215
216def unpack_soap(element):
217    """
218    Convert a tree of ZSI SOAP classes intro a hash.  The inverse of pack_soap
219
220    Elements or elements that are empty are ignored.
221    """
222    methods = [ m for m in dir(element) \
223            if m.startswith("get_element") or m.startswith("get_attribute")]
224    if len(methods) > 0:
225        rv = { }
226        for m in methods:
227            if m.startswith("get_element_"): n = m.replace("get_element_","",1)
228            else: n = m.replace("get_attribute_", "", 1)
229            sub = getattr(element, m)()
230            if sub != None:
231                if isinstance(sub, basestring):
232                    rv[n] = sub
233                elif getattr(sub, "__iter__", None) != None:
234                    if len(sub) > 0: rv[n] = [unpack_soap(e) for e in sub]
235                else:
236                    rv[n] = unpack_soap(sub)
237        return rv
238    else: 
239        return element
240def apply_to_tags(e, map):
241    """
242    Map is an iterable of ordered pairs (tuples) that map a key to a function.
243    This function walks the given message and replaces any object with a key in
244    the map with the result of applying that function to the object.
245    """
246    dict_type = type(dict())
247    list_type = type(list())
248    str_type = type(str())
249
250    if isinstance(e, dict_type):
251        for k in e.keys():
252            for tag, fcn in map:
253                if k == tag:
254                    if isinstance(e[k], list_type):
255                        e[k] = [ fcn(b) for b in e[k]]
256                    else:
257                        e[k] = fcn(e[k])
258                elif isinstance(e[k], dict_type):
259                    apply_to_tags(e[k], map)
260                elif isinstance(e[k], list_type):
261                    for ee in e[k]:
262                        apply_to_tags(ee, map)
263    # Other types end the recursion - they should be leaves
264    return e
265
266# These are all just specializations of apply_to_tags
267def fedids_to_obj(e, tags=('fedid',)):
268    """
269    Turn the fedids in a message that are encoded as bitstrings into fedid
270    objects.
271    """
272    map = [ (t, lambda x: fedid(bits=x)) for t in tags]
273    return apply_to_tags(e, map)
274
275def encapsulate_binaries(e, tags):
276    """Walk through a message and encapsulate any dictionary entries in
277    tags into a binary object."""
278
279    def to_binary(o):
280        pack = getattr(o, 'pack_xmlrpc', None)
281        if callable(pack): return Binary(pack())
282        else: return Binary(o)
283
284    map = [ (t, to_binary) for t in tags]
285    return apply_to_tags(e, map)
286
287def decapsulate_binaries(e, tags):
288    """Walk through a message and encapsulate any dictionary entries in
289    tags into a binary object."""
290
291    map = [ (t, lambda x: x.data) for t in tags]
292    return apply_to_tags(e, map)
293#end of tag specializations
294
295def strip_unicode(obj):
296    """Walk through a message and convert all strings to non-unicode strings"""
297    if isinstance(obj, dict):
298        for k in obj.keys():
299            obj[k] = strip_unicode(obj[k])
300        return obj
301    elif isinstance(obj, basestring):
302        return str(obj)
303    elif getattr(obj, "__iter__", None):
304        return [ strip_unicode(x) for x in obj]
305    else:
306        return obj
307
308def make_unicode(obj):
309    """Walk through a message and convert all strings to unicode"""
310    if isinstance(obj, dict):
311        for k in obj.keys():
312            obj[k] = make_unicode(obj[k])
313        return obj
314    elif isinstance(obj, basestring):
315        return unicode(obj)
316    elif getattr(obj, "__iter__", None):
317        return [ make_unicode(x) for x in obj]
318    else:
319        return obj
320
321
322def generate_fedid(subj, bits=2048, log=None, dir=None, trace=sys.stderr):
323    """
324    Create a new certificate and derive a fedid from it.
325
326    The fedid and the certificate are returned as a tuple.
327    """
328
329    keypath = None
330    certpath = None
331    try:
332        try:
333            kd, keypath = tempfile.mkstemp(dir=dir, prefix="key",
334                    suffix=".pem")
335            cd, certpath = tempfile.mkstemp(dir=dir, prefix="cert",
336                    suffix=".pem")
337
338            cmd = ["/usr/bin/openssl", "req", "-text", "-newkey", 
339                    "rsa:%d" % bits, "-keyout", keypath,  "-nodes", 
340                    "-subj", "/CN=%s" % subj, "-x509", "-days", "30", 
341                    "-out", certpath]
342
343            if log:
344                log.debug("[generate_fedid] %s" % " ".join(cmd))
345
346            if trace: call_out = trace
347            else: 
348                call_out = open("/dev/null", "w")
349
350            rv = subprocess.call(cmd, stdout=call_out, stderr=call_out)
351            log.debug("rv = %d" % rv)
352            if rv == 0:
353                cert = ""
354                for p in (certpath, keypath):
355                    f = open(p)
356                    for line in f:
357                        cert += line
358               
359                fid = fedid(file=certpath)
360                return (fid, cert)
361            else:
362                return (None, None)
363        except IOError, e:
364            raise e
365    finally:
366        if keypath: os.remove(keypath)
367        if certpath: os.remove(certpath)
368
369
370class soap_handler:
371    """
372    Encapsulate the handler code to unpack and pack SOAP requests and responses
373    and call the given method.
374
375    The code to decapsulate and encapsulate parameters encoded in SOAP is the
376    same modulo a few parameters.  This is a functor that calls a fedd service
377    trhough a soap interface.  The parameters are the typecode of the request
378    parameters, the method to call (usually a bound instance of a method on a
379    fedd service providing class), the constructor of a response packet and the
380    name of the body element of that packet.  The handler takes a ParsedSoap
381    object (the request) and returns an instance of the class created by
382    constructor containing the response.  Failures of the constructor or badly
383    created constructors will result in None being returned.
384    """
385    def __init__(self, typecode, method, constructor, body_name):
386        self.typecode = typecode
387        self.method = method
388        self.constructor = constructor
389        self.body_name = body_name
390
391    def __call__(self, ps, fid):
392        req = ps.Parse(self.typecode)
393
394        msg = self.method(fedids_to_obj(unpack_soap(req)), fid)
395
396        resp = self.constructor()
397        set_element = getattr(resp, "set_element_%s" % self.body_name, None)
398        if set_element and callable(set_element):
399            try:
400                set_element(pack_soap(resp, self.body_name, msg))
401                return resp
402            except (NameError, TypeError):
403                return None
404        else:
405            return None
406
407class xmlrpc_handler:
408    """
409    Generate the handler code to unpack and pack XMLRPC requests and responses
410    and call the given method.
411
412    The code to marshall and unmarshall XMLRPC parameters to and from a fedd
413    service is largely the same.  This helper creates such a handler.  The
414    parameters are the method name, and the name of the body struct that
415    contains the response.  A handler is created that takes the params response
416    from an xmlrpclib.loads on the incoming rpc and a fedid and responds with
417    a hash representing the struct ro be returned to the other side.  On error
418    None is returned.  Fedid fields are decapsulated from binary and converted
419    to fedid objects on input and encapsulated as Binaries on output.
420    """
421    def __init__(self, method, body_name):
422        self.method = method
423        self.body_name = body_name
424        # A map used by apply_to_tags to convert fedids from xmlrpclib.Binary
425        # objects to fedid objects in one sweep.
426        self.decap_fedids = (('fedid', lambda x: fedid(bits=x.data)),)
427
428    def __call__(self, params, fid):
429        msg = None
430
431        p = apply_to_tags(params[0], self.decap_fedids)
432        try:
433            msg = self.method(p, fid)
434        except service_error, e:
435            raise Fault(e.code_string(), e.desc)
436        if msg != None:
437            return make_unicode(encapsulate_binaries(\
438                    { self.body_name: msg }, ('fedid',)))
439        else:
440            return None
441
442class service_caller:
443    def __init__(self, service_name, port_name, locator, request_message, 
444            request_body_name, tracefile=None):
445        self.service_name = service_name
446        self.port_name = port_name
447        self.locator = locator
448        self.request_message = request_message
449        self.request_body_name = request_body_name
450        self.tracefile = tracefile
451        self.__call__ = self.call_service
452
453    def call_xmlrpc_service(self, url, req, cert_file=None, cert_pwd=None, 
454            trusted_certs=None, context=None, tracefile=None):
455        """Send an XMLRPC request.  """
456        decap_fedids = (('fedid', lambda x: fedid(bits=x.data)),)
457
458
459        # If a context is given, use it.  Otherwise construct one from
460        # components.  The construction shouldn't call out for passwords.
461        if context:
462            ctx = context
463        else:
464            try:
465                ctx = fedd_ssl_context(cert_file, trusted_certs, 
466                        password=cert_pwd)
467            except SSL.SSLError:
468                raise service_error(service_error.server_config,
469                        "Certificates misconfigured")
470
471        # Of all the dumbass things.  The XMLRPC library in use here won't
472        # properly encode unicode strings, so we make a copy of req with
473        # the unicode objects converted.  We also convert the url to a
474        # basic string if it isn't one already.
475        r = strip_unicode(copy.deepcopy(req))
476        url = str(url)
477       
478        transport = SSL_Transport(ctx)
479        port = ServerProxy(url, transport=transport)
480        # Make the call, and convert faults back to service_errors
481        try:
482            remote_method = getattr(port, self.service_name, None)
483            resp = remote_method(encapsulate_binaries(\
484                    { self.request_body_name: r}, ('fedid',)))
485        except Fault, f:
486            raise service_error(None, f.faultString, f.faultCode)
487        except Error, e:
488            raise service_error(service_error.protocol, 
489                    "Remote XMLRPC Fault: %s" % e)
490
491        return apply_to_tags(resp, decap_fedids) 
492
493    def call_soap_service(self, url, req, cert_file=None, cert_pwd=None,
494            trusted_certs=None, context=None, tracefile=None):
495        """
496        Send req on to the real destination in dt and return the response
497
498        Req is just the requestType object.  This function re-wraps it.  It
499        also rethrows any faults.
500        """
501
502        tf = tracefile or self.tracefile or None
503
504        # If a context is given, use it.  Otherwise construct one from
505        # components.  The construction shouldn't call out for passwords.
506        if context:
507            ctx = context
508        else:
509            try:
510                ctx = fedd_ssl_context(cert_file, trusted_certs, 
511                        password=cert_pwd)
512            except SSL.SSLError:
513                raise service_error(service_error.server_config,
514                        "Certificates misconfigured")
515        loc = self.locator()
516        get_port = getattr(loc, self.port_name, None)
517        if not get_port:
518            raise service_error(service_error.internal, 
519                    "Cannot get port %s from locator" % self.port_name)
520        port = get_port(url,
521                transport=M2Crypto.httpslib.HTTPSConnection, 
522                transdict={ 'ssl_context' : ctx },
523                tracefile=tf)
524        remote_method = getattr(port, self.service_name, None)
525        if not remote_method:
526            raise service_error(service_error.internal,
527                    "Cannot get service from SOAP port")
528
529        # Reconstruct the full request message
530        msg = self.request_message()
531        set_element = getattr(msg, "set_element_%s" % self.request_body_name,
532                None)
533        if not set_element:
534            raise service_error(service_error.internal,
535                    "Cannot get element setting method for %s" % \
536                            self.request_body_name)
537        set_element(pack_soap(msg, self.request_body_name, req))
538        try:
539            resp = remote_method(msg)
540        except ZSI.ParseException, e:
541            raise service_error(service_error.protocol,
542                    "Bad format message (XMLRPC??): %s" % e)
543        except ZSI.FaultException, e:
544            ee = unpack_soap(e.fault.detail[0]).get('FeddFaultBody', { })
545            if ee:
546                raise service_error(ee['code'], ee['desc'])
547            else:
548                raise service_error(service_error.internal,
549                        "Unexpected fault body")
550        r = make_unicode(fedids_to_obj(unpack_soap(resp)))
551        return r
552
553    def call_service(self, url, req, cert_file=None, cert_pwd=None, 
554        trusted_certs=None, context=None, tracefile=None):
555        p_fault = None  # Any SOAP failure (sent unless XMLRPC works)
556        resp = None
557        try:
558            # Try the SOAP request
559            resp = self.call_soap_service(url, req, 
560                    cert_file, cert_pwd, trusted_certs, context, tracefile)
561            return resp
562        except service_error, e:
563            if e.code == service_error.protocol: p_fault = None
564            else: raise
565        except ZSI.FaultException, f:
566            p_fault = f.fault.detail[0]
567               
568
569        # If we could not get a valid SOAP response to the request above,
570        # try the same address using XMLRPC and let any faults flow back
571        # out.
572        if p_fault == None:
573            resp = self.call_xmlrpc_service(url, req, cert_file,
574                    cert_pwd, trusted_certs, context, tracefile)
575            return resp
576        else:
577            # Build the fault
578            ee = unpack_soap(p_fault).get('FeddFaultBody', { })
579            if ee:
580                raise service_error(ee['code'], ee['desc'])
581            else:
582                raise service_error(service_error.internal,
583                        "Unexpected fault body")
584
585
586def set_log_level(config, sect, log):
587    """ Set the logging level to the value passed in sect of config."""
588    # The getattr sleight of hand finds the logging level constant
589    # corrersponding to the string.  We're a little paranoid to avoid user
590    # mayhem.
591    if config.has_option(sect, "log_level"):
592        level_str = config.get(sect, "log_level")
593        try:
594            level = int(getattr(logging, level_str.upper(), -1))
595
596            if  logging.DEBUG <= level <= logging.CRITICAL:
597                log.setLevel(level)
598            else:
599                log.error("Bad experiment_log value: %s" % level_str)
600
601        except ValueError:
602            log.error("Bad experiment_log value: %s" % level_str)
603
Note: See TracBrowser for help on using the repository browser.