From 143f638a4b1bafdc4a2a811a9f4c97a5392a0acc Mon Sep 17 00:00:00 2001 From: Jon Bergli Heier Date: Fri, 30 Aug 2013 23:21:42 +0200 Subject: Added settings for registrations and uploads. * create_active_users: Controls whether new user accounts are created as 'active'; accounts that are not marked as active will not be able to log in, and will be automatically logged out if they're already logged in. * allow_registration: Allow or disallow creation of new user accounts. Any attempts to access the registration page will result in an error message. * allow_anonymous_uploads: Allow or disallow uploading of files by anonymous (not logged in) users. Combined with either allow_registration or create_active_users, this will effectively create a private filebin where the admin must explicitly create or activate new users. --- db.py | 7 ++- fbin.py | 122 ++++++++++++++++++++++++++++++++-------------- templates/base.tmpl | 25 ++++++---- templates/changepass.tmpl | 2 +- templates/delete.tmpl | 4 +- templates/help.tmpl | 2 +- templates/images.tmpl | 2 +- templates/login.tmpl | 2 +- templates/register.tmpl | 2 +- templates/upload.tmpl | 2 +- 10 files changed, 113 insertions(+), 57 deletions(-) diff --git a/db.py b/db.py index 8458704..f571809 100644 --- a/db.py +++ b/db.py @@ -1,4 +1,4 @@ -from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, Index, ForeignKey +from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, Index, ForeignKey, Boolean from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relation, backref from sqlalchemy.orm.exc import NoResultFound @@ -16,11 +16,14 @@ class User(Base): id = Column(Integer, primary_key = True) username = Column(String, unique = True, index = True) password = Column(String) + last_login = Column(DateTime) + active = Column(Boolean, nullable = False) files = relation('File', backref = 'user', order_by = 'File.date.desc()') - def __init__(self, username, password): + def __init__(self, username, password, active): self.username = username self.password = password + self.active = active class File(Base): __tablename__ = 'files' diff --git a/fbin.py b/fbin.py index 51d3203..1d70712 100755 --- a/fbin.py +++ b/fbin.py @@ -22,6 +22,10 @@ except OSError: else: has_mogrify = True +class BinError(Exception): pass +class InvalidCookieError(BinError): pass +class InactiveLoginError(BinError): pass + class FileUploadFieldStorage(cgi.FieldStorage): def make_file(self, binary = None): # Make a temporary file in the destination directory, which will be renamed on completion. @@ -57,10 +61,10 @@ class Application(object): finally: session.close() - def add_user(self, username, password): + def add_user(self, username, password, active): session = db.Session() try: - user = db.User(username, password) + user = db.User(username, password, active) session.add(user) session.commit() except db.IntegrityError: @@ -81,6 +85,18 @@ class Application(object): finally: session.close() + def update_user_login(self, user, login_time = None): + if login_time is None: + login_time = datetime.datetime.utcnow() + session = db.Session() + try: + user.last_login = login_time + session.add(user) + session.commit() + session.refresh(user) + finally: + session.close() + def get_file(self, hash, update_accessed = False): session = db.Session() try: @@ -137,14 +153,17 @@ class Application(object): if not user: return None digest = hashlib.sha1(user.username + user.password).hexdigest() - return user if (digest == identifier) else None - - user = self.get_user_by_id(cookie['uid'].value) - if not user: - return None + else: + user = self.get_user_by_id(cookie['uid'].value) + if not user: + return None + digest = hashlib.sha1(str(user.id) + user.password).hexdigest() - digest = hashlib.sha1(str(user.id) + user.password).hexdigest() - return user if (digest == identifier) else None + if digest != identifier: + raise InvalidCookieError(user.username) + if not user.active: + raise InactiveLoginError(user.username) + return user def get_file_by_file_hash(self, file_hash): session = db.Session() @@ -166,6 +185,10 @@ class Application(object): finally: session.close() + def redirect(self, environ, start_response, dest): + start_response('302 Found', [('Location', settings.virtual_root + dest)]) + return [] + def not_modified(self, environ, date): if not 'HTTP_IF_MODIFIED_SINCE' in environ: return False @@ -246,8 +269,16 @@ class Application(object): if environ['REQUEST_METHOD'] != 'POST' or not 'file' in form or not 'filename' in form: if 'file' in form: form['file'].file.delete = True - start_response('200 OK', [('Content-Type', 'text/html')]) - return str(templates.upload(searchList = {'root': settings.virtual_root, 'user': user})) + if settings.allow_anonymous_uploads: + start_response('200 OK', [('Content-Type', 'text/html')]) + return str(templates.upload(searchList = {'settings': settings, 'user': user})) + else: + return self.redirect(environ, start_response, 'l') + + if not user and not settings.allow_anonymous_uploads: + form['file'].file.delete = True + start_response('403 Forbidden', [('Content-Type', 'text/plain')]) + return ['Anonymous uploads are disabled by the administrator.'] filename = form.getvalue('filename') temp = form['file'].file @@ -295,7 +326,7 @@ class Application(object): else: start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.uploaded(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, 'hash': hash, 'filename': filename, @@ -312,7 +343,7 @@ class Application(object): if environ['REQUEST_METHOD'] != 'POST' or not 'username' in form or not 'password' in form: start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.login(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, 'error': None, 'next': next, @@ -323,15 +354,24 @@ class Application(object): user = self.get_user(username, password) + error = None if user == None: + error = 'Login failed' + elif not user.active: + user = None + error = 'User account is not active' + + if error: start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.login(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, - 'error': 'Login failed', + 'error': error, 'next': next, })) + self.update_user_login(user) + c = Cookie.SimpleCookie() c['uid'] = user.id c['identifier'] = hashlib.sha1(str(user.id) + password).hexdigest() @@ -348,13 +388,17 @@ class Application(object): return [] def register(self, environ, start_response, path): + if not settings.allow_registration: + start_response('403 Forbidden', [('Content-Type', 'text/plain')]) + return ['Registrations are disabled by the administrator.'] + c = Cookie.SimpleCookie(environ['HTTP_COOKIE'] if 'HTTP_COOKIE' in environ else None) user = self.validate_cookie(c) form = cgi.FieldStorage(fp = environ['wsgi.input'], environ = environ) if environ['REQUEST_METHOD'] != 'POST' or not 'username' in form or not 'password' in form or not 'password2' in form: start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.register(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, 'error': None, })) @@ -365,22 +409,21 @@ class Application(object): if password != password2: start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.register(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, 'error': 'Passwords doesn\'t match', })) - user = self.add_user(username, hashlib.sha1(password).hexdigest()) + user = self.add_user(username, hashlib.sha1(password).hexdigest(), settings.create_active_users) if not user: start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.register(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': None, 'error': 'Username already taken.', })) - start_response('302 Found', [('Location', settings.virtual_root + 'l')]) - return [] + return self.redirect(environ, start_response, 'l') def logout(self, environ, start_response, path): c = Cookie.SimpleCookie() @@ -398,11 +441,13 @@ class Application(object): def changepass(self, environ, start_response, path): c = Cookie.SimpleCookie(environ['HTTP_COOKIE'] if 'HTTP_COOKIE' in environ else None) user = self.validate_cookie(c) + if not user: + return self.redirect(environ, start_response, 'l?' + urllib.urlencode({'next': settings.virtual_root + 'c'})) form = cgi.FieldStorage(fp = environ['wsgi.input'], environ = environ) if environ['REQUEST_METHOD'] != 'POST' or not 'oldpass' in form or not 'password' in form or not 'password2' in form: start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.changepass(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, 'error': None, })) @@ -414,7 +459,7 @@ class Application(object): if oldpass != user.password: start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.changepass(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, 'error': 'Invalid password.', })) @@ -422,7 +467,7 @@ class Application(object): if password != password2: start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.changepass(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, 'error': 'Passwords doesn\'t match.', })) @@ -460,7 +505,7 @@ class Application(object): user = self.validate_cookie(c) start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.help(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, 'scheme': environ['wsgi.url_scheme'], 'host': environ['HTTP_HOST'], @@ -475,7 +520,7 @@ class Application(object): files = self.get_files(user) start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.my(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, 'files': files, 'total_size': db.File.pretty_size(sum([f.get_size() for f in files])), @@ -490,7 +535,7 @@ class Application(object): files = [f for f in self.get_files(user) if f.is_image()] start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.images(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, 'files': files, 'total_size': db.File.pretty_size(sum([f.get_size() for f in files])), @@ -539,7 +584,7 @@ class Application(object): else: start_response('200 OK', [('Content-Type', 'text/html')]) return str(templates.delete(searchList = { - 'root': settings.virtual_root, + 'settings': settings, 'user': user, 'hash': hash, 'filename': file.filename, @@ -586,15 +631,18 @@ class Application(object): a = api def __call__(self, environ, start_response): - path = environ['PATH_INFO'].split('/')[1:] - module = path[0] if len(path) else '' - if len(module) and module in 'fulshmitorcda': - return getattr(self, module)(environ, start_response, path) - elif path == ['favicon.ico']: - return self.static(environ, start_response, ['s'] + path) - else: - start_response('302 Found', [('Location', settings.virtual_root + 'u')]) - return [] + try: + path = environ['PATH_INFO'].split('/')[1:] + module = path[0] if len(path) else '' + if len(module) and module in 'fulshmitorcda': + return getattr(self, module)(environ, start_response, path) + elif path == ['favicon.ico']: + return self.static(environ, start_response, ['s'] + path) + else: + start_response('302 Found', [('Location', settings.virtual_root + 'u')]) + return [] + except (InactiveLoginError, InvalidCookieError): + return self.logout(environ, start_response, path) if __name__ == '__main__': import sys diff --git a/templates/base.tmpl b/templates/base.tmpl index fe9120b..5c1f894 100644 --- a/templates/base.tmpl +++ b/templates/base.tmpl @@ -4,33 +4,38 @@ $title - + #block head #end block + $settings
$error
-
+

current password

new password

diff --git a/templates/delete.tmpl b/templates/delete.tmpl index 62216d5..a3ed7be 100644 --- a/templates/delete.tmpl +++ b/templates/delete.tmpl @@ -2,8 +2,8 @@ #def header: delete #extends templates.base #def content - +

Are you sure you want to delete the file $filename?

-

+

#end def diff --git a/templates/help.tmpl b/templates/help.tmpl index bb59424..6451b25 100644 --- a/templates/help.tmpl +++ b/templates/help.tmpl @@ -2,7 +2,7 @@ #def header: help #extends templates.base #def content -

Usage: POST to $scheme://${host}${root}u with filedata given to "file" and original filename to "filename". +

Usage: POST to $scheme://${host}${settings.virtual_root}u with filedata given to "file" and original filename to "filename". Login is sent by cookies with either: