' % name.title()
17 |
18 | @route('/auth/login')
19 | def login():
20 | session = session_manager.get_session()
21 | session['valid'] = True
22 | session_manager.save(session)
23 | bottle.redirect(bottle.request.get_cookie('validuserloginredirect', '/'))
24 |
25 |
26 | ##################
27 | app = bottle.app()
28 | if __name__ == '__main__':
29 | bottle.debug(True)
30 | bottle.run(app = app)
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, tummy.com, ltd.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 | * Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright
9 | notice, this list of conditions and the following disclaimer in the
10 | documentation and/or other materials provided with the distribution.
11 | * Neither the name of the tummy.com, ltd. nor the
12 | names of its contributors may be used to endorse or promote products
13 | derived from this software without specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL TUMMY.COM, LTD. BE LIABLE FOR ANY
19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Sean Reifschneider
2 | Homepage/Code/bugfixes: [https://github.com/linsomniac/bottlesession](https://github.com/linsomniac/bottlesession)
3 | License: 3-clause BSD
4 |
5 | BottleSession README
6 | ====================
7 |
8 | A simple library to make session authenitcation easy with the Bottle
9 | micro-framework.
10 |
11 | Features:
12 |
13 | * Saves off URL being hit so that you can redirect after the login.
14 | (Stored in a cookie)
15 | * Simple session managers included: store in /tmp and store in cookie.
16 | * Decorator to specify that a login is required.
17 | * Saves off login name to "bottle.request.environ['REMOTE_USER']".
18 |
19 | Bugs:
20 |
21 | * Could probably stand to have some other session managers.
22 | * Each request does not re-verify the user, it just checks the session.
23 | (if the password changes, the session doesn't go invalid)
24 | * The session data is just a dictionary
25 | (ideally, there should probably be a "save()" method or
26 | possibly use the context manager interface?)
27 |
28 | Example
29 | -------
30 |
31 | See "app", for example:
32 |
33 | #session_manager = PickleSession()
34 | session_manager = CookieSession() # NOTE: you should specify a secret
35 | valid_user = authenticator(session_manager)
36 |
37 | @route('/')
38 | @route('/:name')
39 | @valid_user()
40 | def hello(name = 'world'):
41 | return '
Hello %s!
' % name.title()
42 |
43 | The "authenticator" creates a decorator that requires authentication. It
44 | takes a session manager object, see the BaseSession class for the API that
45 | it needs to implement.
46 |
47 | "bottlesession" includes a pickle-based session manager that saves session
48 | files in /tmp, much like the stock PHP session store. See "PickleSession"
49 | in "bottlesession.py" for an example implementation, it's real easy!
50 |
51 | "PickleSession()" stores the session in a pickled file under "/tmp" (or other
52 | location, as specified by the "session_dir" argument). "CookieSession()"
53 | stores the cookie in a secure cookie. If no cookie is specified, it will
54 | try to generate a hard to guess but persistent secret. For anything but
55 | trial applications, you should specify a strong secret
56 |
57 | You need a login page, valid_user() relies on this being at "/auth/login".
58 | Here is a complete example:
59 |
60 | @route('/auth/login')
61 | @post('/auth/login')
62 | @view('html/login.html')
63 | def login():
64 | passwds = { 'guest' : 'guest' }
65 |
66 | username = bottle.request.forms.get('username')
67 | password = bottle.request.forms.get('password')
68 |
69 | if not username or not password:
70 | return { 'error' : 'Please specify username and password' }
71 |
72 | session = session_manager.get_session()
73 | session['valid'] = False
74 |
75 | if password and passwds.get(username) == password:
76 | session['valid'] = True
77 | session['name'] = username
78 |
79 | session_manager.save(session)
80 | if not session['valid']:
81 | return { 'error' : 'Username or password is invalid' }
82 |
83 | bottle.redirect(bottle.request.get_cookie('validuserloginredirect', '/'))
84 |
85 | For a logout, just set the session to invalid:
86 |
87 | @route('/auth/logout')
88 | def logout():
89 | session = session_manager.get_session()
90 | session['valid'] = False
91 | session_manager.save(session)
92 | bottle.redirect('/auth/login')
93 |
94 | And a template for "html/login.html":
95 |
96 | %if error:
97 |
98 | %end
99 |
100 |
105 |
--------------------------------------------------------------------------------
/bottlesession.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Bottle session manager. See README for full documentation.
4 | #
5 | # Written by: Sean Reifschneider
6 | #
7 | # License: 3-clause BSD
8 |
9 | from __future__ import with_statement
10 |
11 | import bottle
12 | import time
13 |
14 |
15 | def authenticator(session_manager, login_url='/auth/login'):
16 | '''Create an authenticator decorator.
17 |
18 | :param session_manager: A session manager class to be used for storing
19 | and retrieving session data. Probably based on
20 | :class:`BaseSession`.
21 | :param login_url: The URL to redirect to if a login is required.
22 | (default: ``'/auth/login'``).
23 | '''
24 | def valid_user(login_url=login_url):
25 | def decorator(handler, *a, **ka):
26 | import functools
27 |
28 | @functools.wraps(handler)
29 | def check_auth(*a, **ka):
30 | try:
31 | data = session_manager.get_session()
32 | if not data['valid']:
33 | raise KeyError('Invalid login')
34 | except (KeyError, TypeError):
35 | bottle.response.set_cookie(
36 | 'validuserloginredirect',
37 | bottle.request.fullpath, path='/',
38 | expires=(int(time.time()) + 3600))
39 | bottle.redirect(login_url)
40 |
41 | # set environment
42 | if data.get('name'):
43 | bottle.request.environ['REMOTE_USER'] = data['name']
44 |
45 | return handler(*a, **ka)
46 | return check_auth
47 | return decorator
48 | return(valid_user)
49 |
50 |
51 | import pickle
52 | import os
53 | import uuid
54 |
55 |
56 | class BaseSession(object):
57 | '''Base class which implements some of the basic functionality required for
58 | session managers. Cannot be used directly.
59 |
60 | :param cookie_expires: Expiration time of session ID cookie, either `None`
61 | if the cookie is not to expire, a number of seconds in the future,
62 | or a datetime object. (default: 30 days)
63 | '''
64 | def __init__(self, cookie_expires=86400 * 30):
65 | self.cookie_expires = cookie_expires
66 |
67 | def load(self, sessionid):
68 | raise NotImplementedError
69 |
70 | def save(self, sessionid, data):
71 | raise NotImplementedError
72 |
73 | def make_session_id(self):
74 | return str(uuid.uuid4())
75 |
76 | def allocate_new_session_id(self):
77 | # retry allocating a unique sessionid
78 | for i in range(100):
79 | sessionid = self.make_session_id()
80 | if not self.load(sessionid):
81 | return sessionid
82 | raise ValueError('Unable to allocate unique session')
83 |
84 | def get_session(self):
85 | # get existing or create new session identifier
86 | sessionid = bottle.request.get_cookie('sessionid')
87 | if not sessionid:
88 | sessionid = self.allocate_new_session_id()
89 | bottle.response.set_cookie(
90 | 'sessionid', sessionid, path='/',
91 | expires=(int(time.time()) + self.cookie_expires))
92 |
93 | # load existing or create new session
94 | data = self.load(sessionid)
95 | if not data:
96 | data = {'sessionid': sessionid, 'valid': False}
97 | self.save(data)
98 |
99 | return data
100 |
101 |
102 | class PickleSession(BaseSession):
103 | '''Class which stores session information in the file-system.
104 |
105 | :param session_dir: Directory that session information is stored in.
106 | (default: ``'/tmp'``).
107 | '''
108 | def __init__(self, session_dir='/tmp', *args, **kwargs):
109 | super(PickleSession, self).__init__(*args, **kwargs)
110 | self.session_dir = session_dir
111 |
112 | def load(self, sessionid):
113 | filename = os.path.join(self.session_dir, 'session-%s' % sessionid)
114 | if not os.path.exists(filename):
115 | return None
116 | with open(filename, 'r') as fp:
117 | session = pickle.load(fp)
118 | return session
119 |
120 | def save(self, data):
121 | sessionid = data['sessionid']
122 | fileName = os.path.join(self.session_dir, 'session-%s' % sessionid)
123 | tmpName = fileName + '.' + str(uuid.uuid4())
124 | with open(tmpName, 'w') as fp:
125 | self.session = pickle.dump(data, fp)
126 | os.rename(tmpName, fileName)
127 |
128 |
129 | class CookieSession(BaseSession):
130 | '''Session manager class which stores session in a signed browser cookie.
131 |
132 | :param cookie_name: Name of the cookie to store the session in.
133 | (default: ``session_data``)
134 | :param secret: Secret to be used for "secure cookie". If ``None``,
135 | a random secret will be generated and written to a temporary
136 | file for future use. This may not be suitable for systems which
137 | have untrusted users on it. (default: ``None``)
138 | :param secret_file: File to read the secret from. If ``secret`` is
139 | ``None`` and ``secret_file`` is set, the first line of this file
140 | is read, and stripped, to produce the secret.
141 | '''
142 |
143 | def __init__(
144 | self, secret=None, secret_file=None, cookie_name='session_data',
145 | secure=False, httponly=True, *args, **kwargs):
146 |
147 | super(CookieSession, self).__init__(*args, **kwargs)
148 | self.cookie_name = cookie_name
149 | self.secure = secure
150 | self.httponly = httponly
151 |
152 | if not secret and secret_file is not None:
153 | with open(secret_file, 'r') as fp:
154 | secret = fp.readline().strip()
155 |
156 | if not secret:
157 | import string
158 | import random
159 | import tempfile
160 | import sys
161 |
162 | tmpfilename = os.path.join(
163 | tempfile.gettempdir(),
164 | '%s.secret' % os.path.basename(sys.argv[0]))
165 |
166 | if os.path.exists(tmpfilename):
167 | with open(tmpfilename, 'r') as fp:
168 | secret = fp.readline().strip()
169 | else:
170 | # save off a secret to a tmp file
171 | secret = ''.join([
172 | random.choice(string.letters)
173 | for x in range(32)])
174 |
175 | old_umask = os.umask(int('077', 8))
176 | with open(tmpfilename, 'w') as fp:
177 | fp.write(secret)
178 | os.umask(old_umask)
179 |
180 | self.secret = secret
181 |
182 | def load(self, sessionid):
183 | cookie = bottle.request.get_cookie(
184 | self.cookie_name,
185 | secret=self.secret)
186 | if cookie is None:
187 | return {}
188 | return pickle.loads(cookie)
189 |
190 | def save(self, data):
191 | args = dict(secret=self.secret,
192 | path='/', expires=int(time.time()) + self.cookie_expires)
193 | if self.secure:
194 | args['secure'] = True
195 | if self.httponly:
196 | args['httponly'] = True
197 |
198 | bottle.response.set_cookie(
199 | self.cookie_name, pickle.dumps(data), **args)
200 |
--------------------------------------------------------------------------------