source: fedd/fedd_util.py @ c922f23

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

Start the move to unified service calling routines

  • Property mode set to 100644
File size: 16.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, 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
370def make_soap_handler(typecode, method, constructor, body_name):
371    """
372    Generate 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 basically a stub compiler for
377    calling a fedd service trhough a soap interface.  The parameters are the
378    typecode of the request parameters, the method to call (usually a bound
379    instance of a method on a fedd service providing class), the constructor of
380    a response packet and the name of the body element of that packet.  The
381    handler takes a ParsedSoap object (the request) and returns an instance of
382    the class created by constructor containing the response.  Failures of the
383    constructor or badly created constructors will result in None being
384    returned.
385    """
386    def handler(ps, fid):
387        req = ps.Parse(typecode)
388
389        msg = method(fedids_to_obj(unpack_soap(req)), fid)
390
391        resp = constructor()
392        set_element = getattr(resp, "set_element_%s" % body_name, None)
393        if set_element and callable(set_element):
394            try:
395                set_element(pack_soap(resp, body_name, msg))
396                return resp
397            except (NameError, TypeError):
398                return None
399        else:
400            return None
401
402    return handler
403
404def make_xmlrpc_handler(method, body_name):
405    """
406    Generate the handler code to unpack and pack SOAP requests and responses
407    and call the given method.
408
409    The code to marshall and unmarshall XMLRPC parameters to and from a fedd
410    service is largely the same.  This helper creates such a handler.  The
411    parameters are the method name, and the name of the body struct that
412    contains the response.  A handler is created that takes the params response
413    from an xmlrpclib.loads on the incoming rpc and a fedid and responds with
414    a hash representing the struct ro be returned to the other side.  On error
415    None is returned.  Fedid fields are decapsulated from binary and converted
416    to fedid objects on input and encapsulated as Binaries on output.
417    """
418    def handler(params, fid):
419        decap_fedids = (('fedid', lambda x: fedid(bits=x.data)),)
420
421        p = apply_to_tags(params[0], decap_fedids)
422        msg = method(p, fid)
423
424        if msg != None:
425            return make_unicode(encapsulate_binaries({ body_name: msg }, ('fedid',)))
426        else:
427            return None
428
429    return handler
430
431def make_service_callers(service_name, port_name, request_message, 
432        request_body_name):
433
434    def call_xmlrpc_service(self, url, req, cert_file=None, cert_pwd=None, 
435            trusted_certs=None):
436        """Send an XMLRPC request.  """
437        decap_fedids = (('fedid', lambda x: fedid(bits=x.data)),)
438
439        # No retry loop here.  Proxy servers must correctly authenticate
440        # themselves without help
441        try:
442            ctx = fedd_ssl_context(cert_file, trusted_certs, password=cert_pwd)
443        except SSL.SSLError:
444            raise service_error(service_error.server_config,
445                    "Server certificates misconfigured")
446
447        # Of all the dumbass things.  The XMLRPC library in use here won't
448        # properly encode unicode strings, so we make a copy of req with the
449        # unicode objects converted.  We also convert the url to a basic string
450        # if it isn't one already.
451        r = strip_unicode(copy.deepcopy(req))
452        url = str(url)
453       
454        transport = SSL_Transport(ctx)
455        port = ServerProxy(url, transport=transport)
456        try:
457            remote_method = getattr(port, service_name, None)
458            resp = remote_method(encapsulate_binaries({ request_body_name: r},
459                ('fedid',)))
460        except Fault, f:
461            raise service_error(None, f.faultString, f.faultCode)
462        except Error, e:
463            raise service_error(service_error.proxy, 
464                    "Remote XMLRPC Fault: %s" % e)
465
466        return apply_to_tags(resp, decap_fedids) 
467
468    def call_soap_service(self, url, req, loc_const, cert_file=None, cert_pwd=None, 
469            trusted_certs=None):
470        """
471        Send req on to the real destination in dt and return the response
472
473        Req is just the requestType object.  This function re-wraps it.  It
474        also rethrows any faults.
475        """
476        # No retry loop here.  Proxy servers must correctly authenticate
477        # themselves without help
478        try:
479            ctx = fedd_ssl_context(cert_file, trusted_certs, password=cert_pwd)
480        except SSL.SSLError:
481            raise service_error(service_error.server_config, 
482                    "Server certificates misconfigured")
483
484        loc = loc_const()
485        get_port = getattr(loc, port_name, None)
486        if not get_port:
487            raise service_error(service_error.internal, 
488                    "Cannot get port %s from locator" % port_name)
489        port = get_port(url,
490                transport=M2Crypto.httpslib.HTTPSConnection, 
491                transdict={ 'ssl_context' : ctx })
492        remote_method = getattr(port, service_name, None)
493        if not remote_method:
494            raise service_error(service_error.internal,
495                    "Cannot get service from SOAP port")
496
497        # Reconstruct the full request message
498        msg = request_message()
499        set_element = getattr(msg, "set_element_%s" % request_body_name, None)
500        if not set_element:
501            raise service_error(service_error.internal,
502                    "Cannot get element setting method for %s" % \
503                            request_body_name)
504        set_element(pack_soap(msg, request_body_name, req))
505        try:
506            resp = remote_method(msg)
507        except ZSI.ParseException, e:
508            raise service_error(service_error.proxy,
509                    "Bad format message (XMLRPC??): %s" %
510                    str(e))
511        r = unpack_soap(resp)
512        return r
513
514    return (call_soap_service, call_xmlrpc_service)
515
516def set_log_level(config, sect, log):
517    """ Set the logging level to the value passed in sect of config."""
518    # The getattr sleight of hand finds the logging level constant
519    # corrersponding to the string.  We're a little paranoid to avoid user
520    # mayhem.
521    if config.has_option(sect, "log_level"):
522        level_str = config.get(sect, "log_level")
523        try:
524            level = int(getattr(logging, level_str.upper(), -1))
525
526            if  logging.DEBUG <= level <= logging.CRITICAL:
527                log.setLevel(level)
528            else:
529                log.error("Bad experiment_log value: %s" % level_str)
530
531        except ValueError:
532            log.error("Bad experiment_log value: %s" % level_str)
533
Note: See TracBrowser for help on using the repository browser.