source: fedd/federation/util.py @ ab847bc

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

New syntax for testbeds that includes a /instance rider. This allows users to
request multiple segments from a single testbed.

  • Property mode set to 100644
File size: 7.6 KB
Line 
1#!/usr/local/bin/python
2
3import re
4import string
5import logging
6
7import httplib
8
9from M2Crypto import SSL
10from M2Crypto.SSL import SSLError
11from fedid import fedid
12from service_error import service_error
13from urlparse import urlparse
14
15
16# If this is an old enough version of M2Crypto.SSL that has an
17# ssl_verify_callback that doesn't allow 0-length signed certs, create a
18# version of that callback that does.  This is edited from the original in
19# M2Crypto.SSL.cb.  This version also elides the printing to stderr.
20if not getattr(SSL.cb, 'ssl_verify_callback_allow_unknown_ca', None):
21    from M2Crypto.SSL.Context import map
22    from M2Crypto import m2
23
24    def ssl_verify_callback(ssl_ctx_ptr, x509_ptr, errnum, errdepth, ok):
25        unknown_issuer = [
26            m2.X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY,
27            m2.X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE,
28            m2.X509_V_ERR_CERT_UNTRUSTED,
29            m2.X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT
30            ]
31        ssl_ctx = map()[ssl_ctx_ptr]
32
33        if errnum in unknown_issuer: 
34            if ssl_ctx.get_allow_unknown_ca():
35                ok = 1
36        # CRL checking goes here...
37        if ok:
38            if ssl_ctx.get_verify_depth() >= errdepth:
39                ok = 1
40            else:
41                ok = 0
42        return ok
43else:
44    def ssl_verify_callback(ssl_ctx_ptr, x509_ptr, errnum, errdepth, ok):
45        raise ValueError("This should never be called")
46
47class fedd_ssl_context(SSL.Context):
48    """
49    Simple wrapper around an M2Crypto.SSL.Context to initialize it for fedd.
50    """
51    def __init__(self, my_cert, trusted_certs=None, password=None):
52        """
53        construct a fedd_ssl_context
54
55        @param my_cert: PEM file with my certificate in it
56        @param trusted_certs: PEM file with trusted certs in it (optional)
57        """
58        SSL.Context.__init__(self)
59
60        # load_cert takes a callback to get a password, not a password, so if
61        # the caller provided a password, this creates a nonce callback using a
62        # lambda form.
63        if password != None and not callable(password):
64            # This is cute.  password = lambda *args: password produces a
65            # function object that returns itself rather than one that returns
66            # the object itself.  This is because password is an object
67            # reference and after the assignment it's a lambda.  So we assign
68            # to a temp.
69            pwd = str(password)
70            password =lambda *args: pwd
71
72        # The calls to str below (and above) are because the underlying SSL
73        # stuff is intolerant of unicode.
74        if password != None:
75            self.load_cert(str(my_cert), callback=password)
76        else:
77            self.load_cert(str(my_cert))
78
79        # If no trusted certificates are specified, allow unknown CAs.
80        if trusted_certs: 
81            self.load_verify_locations(trusted_certs)
82            self.set_verify(SSL.verify_peer, 10)
83        else:
84            # More legacy code.  Recent versions of M2Crypto express the
85            # allow_unknown_ca option through a callback turned to allow it.
86            # Older versions use a standard callback that respects the
87            # attribute.  This should work under both regines.
88            callb = getattr(SSL.cb, 'ssl_verify_callback_allow_unknown_ca', 
89                    ssl_verify_callback)
90            self.set_allow_unknown_ca(True)
91            self.set_verify(SSL.verify_peer, 10, callback=callb)
92
93def read_simple_accessdb(fn, auth, mask=[]):
94    """
95    Read a simple access database.  Each line is a fedid (of the form
96    fedid:hexstring) and a comma separated list of atributes to be assigned to
97    it.  This parses out the fedids and adds the attributes to the authorizer.
98    comments (preceded with a #) and blank lines are ignored.  Exceptions (e.g.
99    file exceptions and ValueErrors from badly parsed lines) are propagated.
100    """
101
102    rv = [ ]
103    lineno = 0
104    fedid_line = re.compile("fedid:([" + string.hexdigits + "]+)\s+" +\
105            "(\w+\s*(,\s*\w+)*)")
106
107    # If a single string came in, make it a list
108    if isinstance(mask, basestring): mask = [ mask ]
109
110    f = open(fn, 'r')
111    for line in f:
112        lineno += 1
113        line = line.strip()
114        if line.startswith('#') or len(line) == 0: 
115            continue
116        m = fedid_line.match(line)
117        if m :
118            fid = fedid(hexstr=m.group(1))
119            for a in [ a.strip() for a in m.group(2).split(",") \
120                    if not mask or a.strip() in mask ]:
121                auth.set_attribute(fid, a.strip())
122        else:
123            raise ValueError("Badly formatted line in accessdb: %s line %d" %\
124                    (fn, lineno))
125    f.close()
126    return rv
127       
128
129def pack_id(id):
130    """
131    Return a dictionary with the field name set by the id type.  Handy for
132    creating dictionaries to be converted to messages.
133    """
134    if isinstance(id, fedid): return { 'fedid': id }
135    elif id.startswith("http:") or id.startswith("https:"): return { 'uri': id }
136    else: return { 'localname': id}
137
138def unpack_id(id):
139    """return id as a type determined by the key"""
140    for k in ("localname", "fedid", "uri", "kerberosUsername"):
141        if id.has_key(k): return id[k]
142    return None
143
144def set_log_level(config, sect, log):
145    """ Set the logging level to the value passed in sect of config."""
146    # The getattr sleight of hand finds the logging level constant
147    # corrersponding to the string.  We're a little paranoid to avoid user
148    # mayhem.
149    if config.has_option(sect, "log_level"):
150        level_str = config.get(sect, "log_level")
151        try:
152            level = int(getattr(logging, level_str.upper(), -1))
153
154            if  logging.DEBUG <= level <= logging.CRITICAL:
155                log.setLevel(level)
156            else:
157                log.error("Bad experiment_log value: %s" % level_str)
158
159        except ValueError:
160            log.error("Bad experiment_log value: %s" % level_str)
161
162def copy_file(src, dest, size=1024):
163    """
164    Exceedingly simple file copy.  Throws an IOError if there's a problem.
165    """
166    s = open(src,'r')
167    d = open(dest, 'w')
168
169    buf = s.read(size)
170    while buf != "":
171        d.write(buf)
172        buf = s.read(size)
173    s.close()
174    d.close()
175
176def get_url(url, cf, destdir, fn=None, max_retries=5):
177    """
178    Get data from a federated data store.  This presents the client cert/fedid
179    to the http server.  We retry up to max_retries times.
180    """
181    po = urlparse(url)
182    if not fn:
183        fn = po.path.rpartition('/')[2]
184    retries = 0
185    ok = False
186    failed_exception = None
187    while not ok and retries < 5:
188        try:
189            conn = httplib.HTTPSConnection(po.hostname, port=po.port, 
190                    cert_file=cf, key_file=cf)
191            conn.putrequest('GET', po.path)
192            conn.endheaders()
193            response = conn.getresponse()
194
195            lf = open("%s/%s" % (destdir, fn), "w")
196            buf = response.read(4096)
197            while buf:
198                lf.write(buf)
199                buf = response.read(4096)
200            lf.close()
201            ok = True
202        except IOError, e:
203            failed_excpetion = e
204            retries += 1
205        except httplib.HTTPException, e:
206            failed_exception = e
207            retries += 1
208        except SSLError, e:
209            failed_exception = e
210            retries += 1
211
212    if retries > 5 and failed_exception:
213        raise failed_excpetion
214
215# Functions to manipulate composite testbed names
216def testbed_base(tb):
217    """
218    Simple code to get the base testebd name.
219    """
220    i = tb.find('/')
221    if i == -1: return tb
222    else: return tb[0:i]
223
224def testbed_suffix(tb):
225    """
226    Simple code to get a testbed suffix, if nay.  No suffix returns None.
227    """
228    i = tb.find('/')
229    if i != -1: return tb[i+1:]
230    else: return None
231
232def split_testbed(tb):
233    """
234    Return a testbed and a suffix as a tuple.  No suffix returns None for that
235    field
236    """
237
238    i = tb.find('/')
239    if i != -1: return (tb[0:i], tb[i+1:])
240    else: return (tb, None)
241
242def join_testbed(base, suffix=None):
243    """
244    Build a testbed with suffix.  If base is itself a tuple, combine them,
245    otherwise combine the two.
246    """
247    if isinstance(base, tuple):
248        if len(base) == 2:
249            return '/'.join(base)
250        else:
251            raise RuntimeError("Too many tuple elements for join_testbed")
252    else:
253        if suffix:
254            return '/'.join((base, suffix))
255        else:
256            return base
Note: See TracBrowser for help on using the repository browser.