├── .gitignore ├── Makefile ├── app ├── LICENSE ├── README.md └── bottlesession.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | lib 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | PYTHONPATH=~/projects/tumas/tumas-web2/lib python bottlesession.py 3 | 4 | clean: 5 | rm -f bottlesession.pyc 6 | -------------------------------------------------------------------------------- /app: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from bottle import route, view 4 | import bottle, bottlesession 5 | 6 | 7 | #################### 8 | #session_manager = bottlesession.PickleSession() 9 | session_manager = bottlesession.CookieSession() 10 | valid_user = bottlesession.authenticator(session_manager) 11 | 12 | @route('/') 13 | @route('/:name') 14 | @valid_user() 15 | def hello(name = 'world'): 16 | return '

Hello %s!

' % 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 |
Notice{{error}}
98 | %end 99 | 100 |
101 | Login name:
102 | Password:
103 | 104 |
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 | --------------------------------------------------------------------------------