diff --git a/README.md b/README.md index d7278c2..67acad8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A security shield for protecting a number of small web applications. ## Requirements Python 3.7+ PostgreSQL 11.5+ -Python packages: `gunicorn aiohttp aiohttp_jinja2 asyncpg passlib argon2_cffi` +Python packages: `wheel gunicorn aiohttp aiohttp_jinja2 asyncpg passlib argon2_cffi uvloop` ## Install ``` diff --git a/buckler.py b/buckler.py index 8d6950e..0c0fee8 100644 --- a/buckler.py +++ b/buckler.py @@ -4,6 +4,7 @@ A security shield for protecting a number of small web applications. """ import secrets import functools +from datetime import datetime from aiohttp import web import jinja2 @@ -11,9 +12,11 @@ import aiohttp_jinja2 from aiohttp_jinja2 import render_template from passlib.hash import argon2 import asyncpg +import uvloop import config +uvloop.install() routes = web.RouteTableDef() def auth_required(func): @@ -26,17 +29,42 @@ def auth_required(func): sid = request.cookies.get('session') try: userid = int(request.cookies.get('userid')) - except ValueError: + except (ValueError, TypeError): userid = None if not sid or not userid: raise web.HTTPFound(location=login_url) async with request.app['pool'].acquire() as conn: sessions = await conn.fetch( "SELECT * FROM user_session " - "WHERE user_id = $1", + "WHERE user_id = $1 AND expiry > NOW() ", userid) - if sid in [s.get('id') for s in sessions]: - return await func(request, *args, **kwargs) + session = [s for s in sessions if s.get('id') == sid] + if session: + session = session[0] + tz = session['last_used'].tzinfo + delta = datetime.now(tz) - session['last_used'] + if delta.seconds > 600: + async with request.app['pool'].acquire() as conn: + await conn.execute( + "UPDATE user_session SET last_used = NOW()," + "expiry = NOW() + INTERVAL '30 DAYS'" + "WHERE user_id = $1", + userid) + resp = await func(request, *args, **kwargs) + if delta.seconds > 600: + resp.set_cookie( + 'userid', + userid, + max_age=30*24*60*60, + secure=True, + httponly=True) + resp.set_cookie( + 'session', + sid, + max_age=30*24*60*60, + secure=True, + httponly=True) + return resp else: raise web.HTTPFound(location=login_url) return wrapper @@ -71,7 +99,12 @@ async def login(request): if argon2.verify(password, user_info['password_hash']): index_url = request.app.router['index'].url_for() resp = web.HTTPFound(location=index_url) - resp.set_cookie('userid', user_info['id']) + resp.set_cookie( + 'userid', + user_info['id'], + max_age=30*24*60*60, + secure=True, + httponly=True) sid = secrets.token_urlsafe(64) ip_address = request.headers['X-Real-IP'] async with request.app['pool'].acquire() as conn: @@ -81,7 +114,11 @@ async def login(request): user_info['id'], sid, ip_address) - resp.set_cookie('session', sid) + resp.set_cookie( + 'session',sid, + max_age=30*24*60*60, + secure=True, + httponly=True) raise resp else: return render_template("login.html", request, locals()) @@ -91,28 +128,26 @@ async def login(request): @routes.post(config.url_prefix + '/register', name='register') async def register(request): """Register new accounts.""" - if request.method == 'GET': - return render_template("register.html", request, locals()) + if request.method == 'POST': + data = await request.post() + username = data.get('username') + email = data.get('email') + password = data.get('password') + password_verify = data.get('password_verify') - data = await request.post() - username = data.get('username') - email = data.get('email') - password = data.get('password') - password_verify = data.get('password_verify') + errors = {} + if password != password_verify: + errors[''] = '' - if password != password_verify: - raise web.HTTPNotAcceptable - - pw_hash = argon2.hash(password) - async with request.app['pool'].acquire() as conn: - await conn.execute( - "INSERT INTO user_info (username, email, password_hash) " - "VALUES ($1, $2, $3)", - username, - email, - pw_hash) - login_url = request.app.router['login'].url_for() - raise web.HTTPFound(location=login_url) + pw_hash = argon2.hash(password) + async with request.app['pool'].acquire() as conn: + await conn.execute( + "INSERT INTO user_info (username, email, password_hash) " + "VALUES ($1, $2, $3)", + username, email, pw_hash) + login_url = request.app.router['login'].url_for() + raise web.HTTPFound(location=login_url) + return render_template("register.html", request, locals()) async def init_app(): diff --git a/buckler.sql b/buckler.sql index 7e6da82..b9e9bbf 100644 --- a/buckler.sql +++ b/buckler.sql @@ -6,7 +6,10 @@ CREATE TABLE IF NOT EXISTS user_info ( ); CREATE TABLE IF NOT EXISTS user_session ( - user_id INTEGER references user_info(id), + user_id INTEGER references user_info(id) ON DELETE CASCADE, id TEXT PRIMARY KEY, - ip_address TEXT NOT NULL + ip_address INET NOT NULL, + date_created TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + expiry TIMESTAMP WITH TIME ZONE DEFAULT NOW() + INTERVAL '30 DAYS', + last_used TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); diff --git a/templates/register.html b/templates/register.html index 2a6e38d..bb3c7dd 100644 --- a/templates/register.html +++ b/templates/register.html @@ -7,14 +7,14 @@