source: fedd/fedd_util.py @ f8582c9

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

Resource allocation and deallocation really working
Access handler selects allocation ID
Fedid allocation IDs work
Revamp of util code for maodifying messages (e.g. binaries)
Handlers now see fedids as objects in messages
Fedid bug in handlers in fedd_util

This should have been multiple commits

  • Property mode set to 100644
File size: 13.1 KB
Line 
1#!/usr/local/bin/python
2
3import os, sys
4import subprocess
5import tempfile
6import logging
7
8from M2Crypto import SSL, X509, EVP
9from pyasn1.codec.der import decoder
10
11from xmlrpclib import Binary
12
13
14# The version of M2Crypto on users is pretty old and doesn't have all the
15# features that are useful.  The legacy code is somewhat more brittle than the
16# main line, but will work.
17if "as_der" not in dir(EVP.PKey):
18    from asn1_raw import get_key_bits_from_file, get_key_bits_from_cert
19    legacy = True
20else:
21    legacy = False
22
23class fedd_ssl_context(SSL.Context):
24    """
25    Simple wrapper around an M2Crypto.SSL.Context to initialize it for fedd.
26    """
27    def __init__(self, my_cert, trusted_certs=None, password=None):
28        """
29        construct a fedd_ssl_context
30
31        @param my_cert: PEM file with my certificate in it
32        @param trusted_certs: PEM file with trusted certs in it (optional)
33        """
34        SSL.Context.__init__(self)
35
36        # load_cert takes a callback to get a password, not a password, so if
37        # the caller provided a password, this creates a nonce callback using a
38        # lambda form.
39        if password != None and not callable(password):
40            # This is cute.  password = lambda *args: password produces a
41            # function object that returns itself rather than one that returns
42            # the object itself.  This is because password is an object
43            # reference and after the assignment it's a lambda.  So we assign
44            # to a temp.
45            pwd = password
46            password =lambda *args: pwd
47
48        if password != None:
49            self.load_cert(my_cert, callback=password)
50        else:
51            self.load_cert(my_cert)
52        if trusted_certs != None: self.load_verify_locations(trusted_certs)
53        self.set_verify(SSL.verify_peer, 10)
54
55class fedid:
56    """
57    Wrapper around the federated ID from an X509 certificate.
58    """
59    HASHSIZE=20
60    def __init__(self, bits=None, hexstr=None, cert=None, file=None):
61        if bits != None:
62            self.set_bits(bits)
63        elif hexstr != None:
64            self.set_hexstr(hexstr)
65        elif cert != None:
66            self.set_cert(cert)
67        elif file != None:
68            self.set_file(file)
69        else:
70            self.buf = None
71
72    def __hash__(self):
73        return hash(self.buf)
74
75    def __eq__(self, other):
76        if isinstance(other, type(self)):
77            return self.buf == other.buf
78        elif isinstance(other, type(str())):
79            return self.buf == other;
80        else:
81            return False
82
83    def __ne__(self, other): return not self.__eq__(other)
84
85    def __str__(self):
86        if self.buf != None:
87            return str().join([ "%02x" % ord(x) for x in self.buf])
88        else: return ""
89
90    def __repr__(self):
91        return "fedid(hexstr='%s')" % self.__str__()
92
93    def pack_soap(self):
94        return self.buf
95
96    def pack_xmlrpc(self):
97        return self.buf
98
99    def digest_bits(self, bits):
100        """Internal function.  Compute the fedid from bits and store in buf"""
101        d = EVP.MessageDigest('sha1')
102        d.update(bits)
103        self.buf = d.final()
104
105
106    def set_hexstr(self, hexstr):
107        h = hexstr.replace(':','')
108        self.buf= str().join([chr(int(h[i:i+2],16)) \
109                for i in range(0,2*fedid.HASHSIZE,2)])
110
111    def get_hexstr(self):
112        """Return the hexstring representation of the fedid"""
113        return __str__(self)
114
115    def set_bits(self, bits):
116        """Set the fedid to bits(a 160 bit buffer)"""
117        self.buf = bits
118
119    def get_bits(self):
120        """Get the 160 bit buffer from the fedid"""
121        return self.buf
122
123    def set_file(self, file):
124        """Get the fedid from a certificate file
125
126        Calculate the SHA1 hash over the bit string of the public key as
127        defined in RFC3280.
128        """
129        self.buf = None
130        if legacy: self.digest_bits(get_key_bits_from_file(file))
131        else: self.set_cert(X509.load_cert(file))
132
133    def set_cert(self, cert):
134        """Get the fedid from a certificate.
135
136        Calculate the SHA1 hash over the bit string of the public key as
137        defined in RFC3280.
138        """
139
140        self.buf = None
141        if (cert != None):
142            if legacy:
143                self.digest_bits(get_key_bits_from_cert(cert))
144            else:
145                b = []
146                k = cert.get_pubkey()
147
148                # Getting the key was easy, but getting the bit string of the
149                # key requires a side trip through ASN.1
150                dec = decoder.decode(k.as_der())
151
152                # kv is a tuple of the bits in the key.  The loop below
153                # recombines these into bytes and then into a buffer for the
154                # SSL digest function.
155                kv =  dec[0].getComponentByPosition(1)
156                for i in range(0, len(kv), 8):
157                    v = 0
158                    for j in range(0, 8):
159                        v = (v << 1) + kv[i+j]
160                    b.append(v)
161                # The comprehension turns b from a list of bytes into a buffer
162                # (string) of bytes
163                self.digest_bits(str().join([chr(x) for x in b]))
164
165def pack_id(id):
166    """
167    Return a dictionary with the field name set by the id type.  Handy for
168    creating dictionaries to be converted to messages.
169    """
170    if isinstance(id, type(fedid())): return { 'fedid': id }
171    elif id.startswith("http:") or id.startswith("https:"): return { 'uri': id }
172    else: return { 'localname': id}
173
174def unpack_id(id):
175    """return id as a type determined by the key"""
176    if id.has_key("fedid"): return fedid(id["fedid"])
177    else:
178        for k in ("localname", "uri", "kerberosUsername"):
179            if id.has_key(k): return id[k]
180    return None
181
182def pack_soap(container, name, contents):
183    """
184    Convert the dictionary in contents into a tree of ZSI classes.
185
186    The holder classes are constructed from factories in container and assigned
187    to either the element or attribute name.  This is used to recursively
188    create the SOAP message.
189    """
190    if getattr(contents, "__iter__", None) != None:
191        attr =getattr(container, "new_%s" % name, None)
192        if attr: obj = attr()
193        else:
194            raise TypeError("%s does not have a new_%s attribute" % \
195                    (container, name))
196        for e, v in contents.iteritems():
197            assign = getattr(obj, "set_element_%s" % e, None) or \
198                    getattr(obj, "set_attribute_%s" % e, None)
199            if isinstance(v, type(dict())):
200                assign(pack_soap(obj, e, v))
201            elif getattr(v, "__iter__", None) != None:
202                assign([ pack_soap(obj, e, val ) for val in v])
203            elif getattr(v, "pack_soap", None) != None:
204                assign(v.pack_soap())
205            else:
206                assign(v)
207        return obj
208    else: return contents
209
210def unpack_soap(element):
211    """
212    Convert a tree of ZSI SOAP classes intro a hash.  The inverse of pack_soap
213
214    Elements or elements that are empty are ignored.
215    """
216    methods = [ m for m in dir(element) \
217            if m.startswith("get_element") or m.startswith("get_attribute")]
218    if len(methods) > 0:
219        rv = { }
220        for m in methods:
221            if m.startswith("get_element_"): n = m.replace("get_element_","",1)
222            else: n = m.replace("get_attribute_", "", 1)
223            sub = getattr(element, m)()
224            if sub != None:
225                if isinstance(sub, basestring):
226                    rv[n] = sub
227                elif getattr(sub, "__iter__", None) != None:
228                    if len(sub) > 0: rv[n] = [unpack_soap(e) for e in sub]
229                else:
230                    rv[n] = unpack_soap(sub)
231        return rv
232    else: 
233        return element
234def apply_to_tags(e, map):
235    """
236    Map is an iterable of ordered pairs (tuples) that map a key to a function.
237    This function walks the given message and replaces any object with a key in
238    the map with the result of applying that function to the object.
239    """
240    dict_type = type(dict())
241    list_type = type(list())
242    str_type = type(str())
243
244    if isinstance(e, dict_type):
245        for k in e.keys():
246            for tag, fcn in map:
247                if k == tag:
248                    if isinstance(e[k], list_type):
249                        e[k] = [ fcn(b) for b in e[k]]
250                    else:
251                        e[k] = fcn(e[k])
252                elif isinstance(e[k], dict_type):
253                    apply_to_tags(e[k], map)
254                elif isinstance(e[k], list_type):
255                    for ee in e[k]:
256                        apply_to_tags(ee, map)
257    # Other types end the recursion - they should be leaves
258    return e
259
260# These are all just specializations of apply_to_tags
261def fedids_to_obj(e, tags=('fedid',)):
262    """
263    Turn the fedids in a message that are encoded as bitstrings into fedid
264    objects.
265    """
266    map = [ (t, lambda x: fedid(bits=x)) for t in tags]
267    return apply_to_tags(e, map)
268
269def encapsulate_binaries(e, tags):
270    """Walk through a message and encapsulate any dictionary entries in
271    tags into a binary object."""
272
273    def to_binary(o):
274        pack = getattr(o, 'pack_xmlrpc', None)
275        if callable(pack): return Binary(pack())
276        else: return Binary(o)
277
278    map = [ (t, to_binary) for t in tags]
279    return apply_to_tags(e, map)
280
281def decapsulate_binaries(e, tags):
282    """Walk through a message and encapsulate any dictionary entries in
283    tags into a binary object."""
284
285    map = [ (t, lambda x: x.data) for t in tags]
286    return apply_to_tags(e, map)
287#end of tag specializations
288
289def strip_unicode(obj):
290    """Walk through a message and convert all strings to non-unicode strings"""
291    if isinstance(obj, dict):
292        for k in obj.keys():
293            obj[k] = strip_unicode(obj[k])
294        return obj
295    elif isinstance(obj, basestring):
296        return str(obj)
297    elif getattr(obj, "__iter__", None):
298        return [ strip_unicode(x) for x in obj]
299    else:
300        return obj
301
302def make_unicode(obj):
303    """Walk through a message and convert all strings to unicode"""
304    if isinstance(obj, dict):
305        for k in obj.keys():
306            obj[k] = make_unicode(obj[k])
307        return obj
308    elif isinstance(obj, basestring):
309        return unicode(obj)
310    elif getattr(obj, "__iter__", None):
311        return [ make_unicode(x) for x in obj]
312    else:
313        return obj
314
315
316def generate_fedid(subj, bits=2048, log=None, dir=None, trace=sys.stderr):
317    """
318    Create a new certificate and derive a fedid from it.
319
320    The fedid and the certificate are returned as a tuple.
321    """
322
323    keypath = None
324    certpath = None
325    try:
326        try:
327            kd, keypath = tempfile.mkstemp(dir=dir, prefix="key",
328                    suffix=".pem")
329            cd, certpath = tempfile.mkstemp(dir=dir, prefix="cert",
330                    suffix=".pem")
331
332            cmd = ["/usr/bin/openssl", "req", "-text", "-newkey", 
333                    "rsa:%d" % bits, "-keyout", keypath,  "-nodes", 
334                    "-subj", "/CN=%s" % subj, "-x509", "-days", "30", 
335                    "-out", certpath]
336
337            if log:
338                log.debug("[generate_fedid] %s" % " ".join(cmd))
339
340            if trace: call_out = trace
341            else: 
342                call_out = open("/dev/null", "w")
343
344            rv = subprocess.call(cmd, stdout=call_out, stderr=call_out)
345            log.debug("rv = %d" % rv)
346            if rv == 0:
347                cert = ""
348                for p in (certpath, keypath):
349                    f = open(p)
350                    for line in f:
351                        cert += line
352               
353                fid = fedid(file=certpath)
354                return (fid, cert)
355            else:
356                return (None, None)
357        except IOError, e:
358            raise e
359    finally:
360        if keypath: os.remove(keypath)
361        if certpath: os.remove(certpath)
362
363
364def make_soap_handler(typecode, method, constructor, body_name):
365    """
366    Generate the handler code to unpack and pack SOAP requests and responses
367    and call the given method.
368
369    The code to decapsulate and encapsulate parameters encoded in SOAP is the
370    same modulo a few parameters.  This is basically a stub compiler for
371    calling a fedd service trhough a soap interface.  The parameters are the
372    typecode of the request parameters, the method to call (usually a bound
373    instance of a method on a fedd service providing class), the constructor of
374    a response packet and the name of the body element of that packet.  The
375    handler takes a ParsedSoap object (the request) and returns an instance of
376    the class created by constructor containing the response.  Failures of the
377    constructor or badly created constructors will result in None being
378    returned.
379    """
380    def handler(ps, fid):
381        req = ps.Parse(typecode)
382
383        msg = method(fedids_to_obj(unpack_soap(req)), fid)
384
385        resp = constructor()
386        set_element = getattr(resp, "set_element_%s" % body_name, None)
387        if set_element and callable(set_element):
388            try:
389                set_element(pack_soap(resp, body_name, msg))
390                return resp
391            except (NameError, TypeError):
392                return None
393        else:
394            return None
395
396    return handler
397
398def make_xmlrpc_handler(method, body_name):
399    """
400    Generate the handler code to unpack and pack SOAP requests and responses
401    and call the given method.
402
403    The code to marshall and unmarshall XMLRPC parameters to and from a fedd
404    service is largely the same.  This helper creates such a handler.  The
405    parameters are the method name, and the name of the body struct that
406    contains the response.  A handler is created that takes the params response
407    from an xmlrpclib.loads on the incoming rpc and a fedid and responds with
408    a hash representing the struct ro be returned to the other side.  On error
409    None is returned.  Fedid fields are decapsulated from binary and converted
410    to fedid objects on input and encapsulated as Binaries on output.
411    """
412    def handler(params, fid):
413        decap_fedids = (('fedid', lambda x: fedid(bits=x.data)),)
414
415        #p = decapsulate_binaries(params[0], input_binaries)
416        p = apply_to_tags(params[0], decap_fedids)
417        msg = method(p, fid)
418
419        if msg != None:
420            return encapsulate_binaries({ body_name: msg }, ('fedid',))
421        else:
422            return None
423
424    return handler
425
426
427def set_log_level(config, sect, log):
428    """ Set the logging level to the value passed in sect of config."""
429    # The getattr sleight of hand finds the logging level constant
430    # corrersponding to the string.  We're a little paranoid to avoid user
431    # mayhem.
432    if config.has_option(sect, "log_level"):
433        level_str = config.get(sect, "log_level")
434        try:
435            level = int(getattr(logging, level_str.upper(), -1))
436
437            if  logging.DEBUG <= level <= logging.CRITICAL:
438                log.setLevel(level)
439            else:
440                log.error("Bad experiment_log value: %s" % level_str)
441
442        except ValueError:
443            log.error("Bad experiment_log value: %s" % level_str)
444
Note: See TracBrowser for help on using the repository browser.