root/galaxy-central/eggs/Paste-1.6-py2.6.egg/paste/auth/cookie.py @ 3

リビジョン 3, 15.4 KB (コミッタ: kohda, 14 年 前)

Install Unix tools  http://hannonlab.cshl.edu/galaxy_unix_tools/galaxy.html

行番号 
1# (c) 2005 Clark C. Evans
2# This module is part of the Python Paste Project and is released under
3# the MIT License: http://www.opensource.org/licenses/mit-license.php
4# This code was written with funding by http://prometheusresearch.com
5"""
6Cookie "Saved" Authentication
7
8This authentication middleware saves the current REMOTE_USER,
9REMOTE_SESSION, and any other environment variables specified in a
10cookie so that it can be retrieved during the next request without
11requiring re-authentication. This uses a session cookie on the client
12side (so it goes away when the user closes their window) and does
13server-side expiration.
14
15Following is a very simple example where a form is presented asking for
16a user name (no actual checking), and dummy session identifier (perhaps
17corresponding to a database session id) is stored in the cookie.
18
19::
20
21  >>> from paste.httpserver import serve
22  >>> from paste.fileapp import DataApp
23  >>> from paste.httpexceptions import *
24  >>> from paste.auth.cookie import AuthCookieHandler
25  >>> from paste.wsgilib import parse_querystring
26  >>> def testapp(environ, start_response):
27  ...     user = dict(parse_querystring(environ)).get('user','')
28  ...     if user:
29  ...         environ['REMOTE_USER'] = user
30  ...         environ['REMOTE_SESSION'] = 'a-session-id'
31  ...     if environ.get('REMOTE_USER'):
32  ...         page = '<html><body>Welcome %s (%s)</body></html>'
33  ...         page %= (environ['REMOTE_USER'], environ['REMOTE_SESSION'])
34  ...     else:
35  ...         page = ('<html><body><form><input name="user" />'
36  ...                 '<input type="submit" /></form></body></html>')
37  ...     return DataApp(page, content_type="text/html")(
38  ...                    environ, start_response)
39  >>> serve(AuthCookieHandler(testapp))
40  serving on...
41
42"""
43
44import sha, hmac, base64, random, time, warnings
45from paste.request import get_cookies
46
47def make_time(value):
48    return time.strftime("%Y%m%d%H%M", time.gmtime(value))
49_signature_size = len(hmac.new('x', 'x', sha).digest())
50_header_size = _signature_size + len(make_time(time.time()))
51
52# @@: Should this be using urllib.quote?
53# build encode/decode functions to safely pack away values
54_encode = [('\\', '\\x5c'), ('"', '\\x22'),
55           ('=', '\\x3d'), (';', '\\x3b')]
56_decode = [(v, k) for (k, v) in _encode]
57_decode.reverse()
58def encode(s, sublist = _encode):
59    return reduce((lambda a, (b, c): a.replace(b, c)), sublist, str(s))
60decode = lambda s: encode(s, _decode)
61
62class CookieTooLarge(RuntimeError):
63    def __init__(self, content, cookie):
64        RuntimeError.__init__("Signed cookie exceeds maximum size of 4096")
65        self.content = content
66        self.cookie = cookie
67
68_all_chars = ''.join([chr(x) for x in range(0, 255)])
69def new_secret():
70    """ returns a 64 byte secret """
71    return ''.join(random.sample(_all_chars, 64))
72
73class AuthCookieSigner(object):
74    """
75    save/restore ``environ`` entries via digially signed cookie
76
77    This class converts content into a timed and digitally signed
78    cookie, as well as having the facility to reverse this procedure.
79    If the cookie, after the content is encoded and signed exceeds the
80    maximum length (4096), then CookieTooLarge exception is raised.
81
82    The timeout of the cookie is handled on the server side for a few
83    reasons.  First, if a 'Expires' directive is added to a cookie, then
84    the cookie becomes persistent (lasting even after the browser window
85    has closed). Second, the user's clock may be wrong (perhaps
86    intentionally). The timeout is specified in minutes; and expiration
87    date returned is rounded to one second.
88
89    Constructor Arguments:
90
91        ``secret``
92
93            This is a secret key if you want to syncronize your keys so
94            that the cookie will be good across a cluster of computers.
95            It is recommended via the HMAC specification (RFC 2104) that
96            the secret key be 64 bytes since this is the block size of
97            the hashing.  If you do not provide a secret key, a random
98            one is generated each time you create the handler; this
99            should be sufficient for most cases.
100
101        ``timeout``
102
103            This is the time (in minutes) from which the cookie is set
104            to expire.  Note that on each request a new (replacement)
105            cookie is sent, hence this is effectively a session timeout
106            parameter for your entire cluster.  If you do not provide a
107            timeout, it is set at 30 minutes.
108
109        ``maxlen``
110
111            This is the maximum size of the *signed* cookie; hence the
112            actual content signed will be somewhat less.  If the cookie
113            goes over this size, a ``CookieTooLarge`` exception is
114            raised so that unexpected handling of cookies on the client
115            side are avoided.  By default this is set at 4k (4096 bytes),
116            which is the standard cookie size limit.
117
118    """
119    def __init__(self, secret = None, timeout = None, maxlen = None):
120        self.timeout = timeout or 30
121        if isinstance(timeout, basestring):
122            raise ValueError(
123                "Timeout must be a number (minutes), not a string (%r)"
124                % timeout)
125        self.maxlen  = maxlen or 4096
126        self.secret = secret or new_secret()
127
128    def sign(self, content):
129        """
130        Sign the content returning a valid cookie (that does not
131        need to be escaped and quoted).  The expiration of this
132        cookie is handled server-side in the auth() function.
133        """
134        cookie = base64.encodestring(
135            hmac.new(self.secret, content, sha).digest() +
136            make_time(time.time() + 60*self.timeout) +
137            content).replace("/", "_").replace("=", "~")
138        if len(cookie) > self.maxlen:
139            raise CookieTooLarge(content, cookie)
140        return cookie
141
142    def auth(self, cookie):
143        """
144        Authenticate the cooke using the signature, verify that it
145        has not expired; and return the cookie's content
146        """
147        decode = base64.decodestring(
148            cookie.replace("_", "/").replace("~", "="))
149        signature = decode[:_signature_size]
150        expires = decode[_signature_size:_header_size]
151        content = decode[_header_size:]
152        if signature == hmac.new(self.secret, content, sha).digest():
153            if int(expires) > int(make_time(time.time())):
154                return content
155            else:
156                # This is the normal case of an expired cookie; just
157                # don't bother doing anything here.
158                pass
159        else:
160            # This case can happen if the server is restarted with a
161            # different secret; or if the user's IP address changed
162            # due to a proxy.  However, it could also be a break-in
163            # attempt -- so should it be reported?
164            pass
165
166class AuthCookieEnviron(list):
167    """
168    a list of environment keys to be saved via cookie
169
170    An instance of this object, found at ``environ['paste.auth.cookie']``
171    lists the `environ` keys that were restored from or will be added
172    to the digially signed cookie.  This object can be accessed from an
173    `environ` variable by using this module's name.
174    """
175    def __init__(self, handler, scanlist):
176        list.__init__(self, scanlist)
177        self.handler = handler
178    def append(self, value):
179        if value in self:
180            return
181        list.append(self, str(value))
182
183class AuthCookieHandler(object):
184    """
185    the actual handler that should be put in your middleware stack
186
187    This middleware uses cookies to stash-away a previously authenticated
188    user (and perhaps other variables) so that re-authentication is not
189    needed.  This does not implement sessions; and therefore N servers
190    can be syncronized to accept the same saved authentication if they
191    all use the same cookie_name and secret.
192
193    By default, this handler scans the `environ` for the REMOTE_USER
194    and REMOTE_SESSION key; if found, it is stored. It can be
195    configured to scan other `environ` keys as well -- but be careful
196    not to exceed 2-3k (so that the encoded and signed cookie does not
197    exceed 4k). You can ask it to handle other environment variables
198    by doing:
199
200       ``environ['paste.auth.cookie'].append('your.environ.variable')``
201
202
203    Constructor Arguments:
204
205        ``application``
206
207            This is the wrapped application which will have access to
208            the ``environ['REMOTE_USER']`` restored by this middleware.
209
210        ``cookie_name``
211
212            The name of the cookie used to store this content, by default
213            it is ``PASTE_AUTH_COOKIE``.
214
215        ``scanlist``
216
217            This is the initial set of ``environ`` keys to
218            save/restore to the signed cookie.  By default is consists
219            only of ``REMOTE_USER`` and ``REMOTE_SESSION``; any tuple
220            or list of environment keys will work.  However, be
221            careful, as the total saved size is limited to around 3k.
222
223        ``signer``
224
225            This is the signer object used to create the actual cookie
226            values, by default, it is ``AuthCookieSigner`` and is passed
227            the remaining arguments to this function: ``secret``,
228            ``timeout``, and ``maxlen``.
229
230    At this time, each cookie is individually signed.  To store more
231    than the 4k of data; it is possible to sub-class this object to
232    provide different ``environ_name`` and ``cookie_name``
233    """
234    environ_name = 'paste.auth.cookie'
235    cookie_name  = 'PASTE_AUTH_COOKIE'
236    signer_class = AuthCookieSigner
237    environ_class = AuthCookieEnviron
238
239    def __init__(self, application, cookie_name=None, scanlist=None,
240                 signer=None, secret=None, timeout=None, maxlen=None):
241        if not signer:
242            signer = self.signer_class(secret, timeout, maxlen)
243        self.signer = signer
244        self.scanlist = scanlist or ('REMOTE_USER','REMOTE_SESSION')
245        self.application = application
246        self.cookie_name = cookie_name or self.cookie_name
247
248    def __call__(self, environ, start_response):
249        if self.environ_name in environ:
250            raise AssertionError("AuthCookie already installed!")
251        scanlist = self.environ_class(self, self.scanlist)
252        jar = get_cookies(environ)
253        if jar.has_key(self.cookie_name):
254            content = self.signer.auth(jar[self.cookie_name].value)
255            if content:
256                for pair in content.split(";"):
257                    (k, v) = pair.split("=")
258                    k = decode(k)
259                    if k not in scanlist:
260                        scanlist.append(k)
261                    if k in environ:
262                        continue
263                    environ[k] = decode(v)
264                    if 'REMOTE_USER' == k:
265                        environ['AUTH_TYPE'] = 'cookie'
266        environ[self.environ_name] = scanlist
267        if "paste.httpexceptions" in environ:
268            warnings.warn("Since paste.httpexceptions is hooked in your "
269                "processing chain before paste.auth.cookie, if an "
270                "HTTPRedirection is raised, the cookies this module sets "
271                "will not be included in your response.\n")
272
273        def response_hook(status, response_headers, exc_info=None):
274            """
275            Scan the environment for keys specified in the scanlist,
276            pack up their values, signs the content and issues a cookie.
277            """
278            scanlist = environ.get(self.environ_name)
279            assert scanlist and isinstance(scanlist, self.environ_class)
280            content = []
281            for k in scanlist:
282                v = environ.get(k)
283                if v is not None:
284                    if type(v) is not str:
285                        raise ValueError(
286                            "The value of the environmental variable %r "
287                            "is not a str (only str is allowed; got %r)"
288                            % (k, v))
289                    content.append("%s=%s" % (encode(k), encode(v)))
290            if content:
291                content = ";".join(content)
292                content = self.signer.sign(content)
293                cookie = '%s=%s; Path=/;' % (self.cookie_name, content)
294                if 'https' == environ['wsgi.url_scheme']:
295                    cookie += ' secure;'
296                response_headers.append(('Set-Cookie', cookie))
297            return start_response(status, response_headers, exc_info)
298        return self.application(environ, response_hook)
299
300middleware = AuthCookieHandler
301
302# Paste Deploy entry point:
303def make_auth_cookie(
304    app, global_conf,
305    # Should this get picked up from global_conf somehow?:
306    cookie_name='PASTE_AUTH_COOKIE',
307    scanlist=('REMOTE_USER', 'REMOTE_SESSION'),
308    # signer cannot be set
309    secret=None,
310    timeout=30,
311    maxlen=4096):
312    """
313    This middleware uses cookies to stash-away a previously
314    authenticated user (and perhaps other variables) so that
315    re-authentication is not needed.  This does not implement
316    sessions; and therefore N servers can be syncronized to accept the
317    same saved authentication if they all use the same cookie_name and
318    secret.
319
320    By default, this handler scans the `environ` for the REMOTE_USER
321    and REMOTE_SESSION key; if found, it is stored. It can be
322    configured to scan other `environ` keys as well -- but be careful
323    not to exceed 2-3k (so that the encoded and signed cookie does not
324    exceed 4k). You can ask it to handle other environment variables
325    by doing:
326
327       ``environ['paste.auth.cookie'].append('your.environ.variable')``
328
329    Configuration:
330
331        ``cookie_name``
332
333            The name of the cookie used to store this content, by
334            default it is ``PASTE_AUTH_COOKIE``.
335
336        ``scanlist``
337
338            This is the initial set of ``environ`` keys to
339            save/restore to the signed cookie.  By default is consists
340            only of ``REMOTE_USER`` and ``REMOTE_SESSION``; any
341            space-separated list of environment keys will work.
342            However, be careful, as the total saved size is limited to
343            around 3k.
344
345        ``secret``
346
347            The secret that will be used to sign the cookies.  If you
348            don't provide one (and none is set globally) then a random
349            secret will be created.  Each time the server is restarted
350            a new secret will then be created and all cookies will
351            become invalid!  This can be any string value.
352
353        ``timeout``
354
355            The time to keep the cookie, expressed in minutes.  This
356            is handled server-side, so a new cookie with a new timeout
357            is added to every response.
358
359        ``maxlen``
360
361            The maximum length of the cookie that is sent (default 4k,
362            which is a typical browser maximum)
363       
364    """
365    if isinstance(scanlist, basestring):
366        scanlist = scanlist.split()
367    if secret is None and global_conf.get('secret'):
368        secret = global_conf['secret']
369    try:
370        timeout = int(timeout)
371    except ValueError:
372        raise ValueError('Bad value for timeout (must be int): %r'
373                         % timeout)
374    try:
375        maxlen = int(maxlen)
376    except ValueError:
377        raise ValueError('Bad value for maxlen (must be int): %r'
378                         % maxlen)
379    return AuthCookieHandler(
380        app, cookie_name=cookie_name, scanlist=scanlist,
381        secret=secret, timeout=timeout, maxlen=maxlen)
382
383__all__ = ['AuthCookieHandler', 'AuthCookieSigner', 'AuthCookieEnviron']
384
385if "__main__" == __name__:
386    import doctest
387    doctest.testmod(optionflags=doctest.ELLIPSIS)
388
Note: リポジトリブラウザについてのヘルプは TracBrowser を参照してください。