source: fedd/federation/util.py @ 10a7053

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

Deal with another random way SSL can fail us.

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