third commit
This commit is contained in:
parent
46b5a65990
commit
04156e39b3
|
@ -4,7 +4,7 @@ A security shield for protecting a number of small web applications.
|
||||||
## Requirements
|
## Requirements
|
||||||
Python 3.7+
|
Python 3.7+
|
||||||
PostgreSQL 11.5+
|
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
|
## Install
|
||||||
```
|
```
|
||||||
|
|
87
buckler.py
87
buckler.py
|
@ -4,6 +4,7 @@ A security shield for protecting a number of small web applications.
|
||||||
"""
|
"""
|
||||||
import secrets
|
import secrets
|
||||||
import functools
|
import functools
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import jinja2
|
import jinja2
|
||||||
|
@ -11,9 +12,11 @@ import aiohttp_jinja2
|
||||||
from aiohttp_jinja2 import render_template
|
from aiohttp_jinja2 import render_template
|
||||||
from passlib.hash import argon2
|
from passlib.hash import argon2
|
||||||
import asyncpg
|
import asyncpg
|
||||||
|
import uvloop
|
||||||
|
|
||||||
import config
|
import config
|
||||||
|
|
||||||
|
uvloop.install()
|
||||||
routes = web.RouteTableDef()
|
routes = web.RouteTableDef()
|
||||||
|
|
||||||
def auth_required(func):
|
def auth_required(func):
|
||||||
|
@ -26,17 +29,42 @@ def auth_required(func):
|
||||||
sid = request.cookies.get('session')
|
sid = request.cookies.get('session')
|
||||||
try:
|
try:
|
||||||
userid = int(request.cookies.get('userid'))
|
userid = int(request.cookies.get('userid'))
|
||||||
except ValueError:
|
except (ValueError, TypeError):
|
||||||
userid = None
|
userid = None
|
||||||
if not sid or not userid:
|
if not sid or not userid:
|
||||||
raise web.HTTPFound(location=login_url)
|
raise web.HTTPFound(location=login_url)
|
||||||
async with request.app['pool'].acquire() as conn:
|
async with request.app['pool'].acquire() as conn:
|
||||||
sessions = await conn.fetch(
|
sessions = await conn.fetch(
|
||||||
"SELECT * FROM user_session "
|
"SELECT * FROM user_session "
|
||||||
"WHERE user_id = $1",
|
"WHERE user_id = $1 AND expiry > NOW() ",
|
||||||
userid)
|
userid)
|
||||||
if sid in [s.get('id') for s in sessions]:
|
session = [s for s in sessions if s.get('id') == sid]
|
||||||
return await func(request, *args, **kwargs)
|
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:
|
else:
|
||||||
raise web.HTTPFound(location=login_url)
|
raise web.HTTPFound(location=login_url)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
@ -71,7 +99,12 @@ async def login(request):
|
||||||
if argon2.verify(password, user_info['password_hash']):
|
if argon2.verify(password, user_info['password_hash']):
|
||||||
index_url = request.app.router['index'].url_for()
|
index_url = request.app.router['index'].url_for()
|
||||||
resp = web.HTTPFound(location=index_url)
|
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)
|
sid = secrets.token_urlsafe(64)
|
||||||
ip_address = request.headers['X-Real-IP']
|
ip_address = request.headers['X-Real-IP']
|
||||||
async with request.app['pool'].acquire() as conn:
|
async with request.app['pool'].acquire() as conn:
|
||||||
|
@ -81,7 +114,11 @@ async def login(request):
|
||||||
user_info['id'],
|
user_info['id'],
|
||||||
sid,
|
sid,
|
||||||
ip_address)
|
ip_address)
|
||||||
resp.set_cookie('session', sid)
|
resp.set_cookie(
|
||||||
|
'session',sid,
|
||||||
|
max_age=30*24*60*60,
|
||||||
|
secure=True,
|
||||||
|
httponly=True)
|
||||||
raise resp
|
raise resp
|
||||||
else:
|
else:
|
||||||
return render_template("login.html", request, locals())
|
return render_template("login.html", request, locals())
|
||||||
|
@ -91,28 +128,26 @@ async def login(request):
|
||||||
@routes.post(config.url_prefix + '/register', name='register')
|
@routes.post(config.url_prefix + '/register', name='register')
|
||||||
async def register(request):
|
async def register(request):
|
||||||
"""Register new accounts."""
|
"""Register new accounts."""
|
||||||
if request.method == 'GET':
|
if request.method == 'POST':
|
||||||
return render_template("register.html", request, locals())
|
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()
|
errors = {}
|
||||||
username = data.get('username')
|
if password != password_verify:
|
||||||
email = data.get('email')
|
errors[''] = ''
|
||||||
password = data.get('password')
|
|
||||||
password_verify = data.get('password_verify')
|
|
||||||
|
|
||||||
if password != password_verify:
|
pw_hash = argon2.hash(password)
|
||||||
raise web.HTTPNotAcceptable
|
async with request.app['pool'].acquire() as conn:
|
||||||
|
await conn.execute(
|
||||||
pw_hash = argon2.hash(password)
|
"INSERT INTO user_info (username, email, password_hash) "
|
||||||
async with request.app['pool'].acquire() as conn:
|
"VALUES ($1, $2, $3)",
|
||||||
await conn.execute(
|
username, email, pw_hash)
|
||||||
"INSERT INTO user_info (username, email, password_hash) "
|
login_url = request.app.router['login'].url_for()
|
||||||
"VALUES ($1, $2, $3)",
|
raise web.HTTPFound(location=login_url)
|
||||||
username,
|
return render_template("register.html", request, locals())
|
||||||
email,
|
|
||||||
pw_hash)
|
|
||||||
login_url = request.app.router['login'].url_for()
|
|
||||||
raise web.HTTPFound(location=login_url)
|
|
||||||
|
|
||||||
|
|
||||||
async def init_app():
|
async def init_app():
|
||||||
|
|
|
@ -6,7 +6,10 @@ CREATE TABLE IF NOT EXISTS user_info (
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_session (
|
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,
|
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()
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,14 +7,14 @@
|
||||||
<h1>Buckler Register</h1>
|
<h1>Buckler Register</h1>
|
||||||
<form action="{{ request.app.router['register'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
|
<form action="{{ request.app.router['register'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input id="username" name="username" type="text"><br>
|
<input id="username" name="username" type="text" minlength="3" maxlength="20"><br>
|
||||||
<label for="email">Email</label>
|
<label for="email">Email</label>
|
||||||
<input id="email" name="email" type="email"><br>
|
<input id="email" name="email" type="email"><br>
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input id="password" name="password" type="password"><br>
|
<input id="password" name="password" type="password" minlength="8" maxlength="10240"><br>
|
||||||
<label for="password_verify">Verify Password</label>
|
<label for="password_verify">Verify Password</label>
|
||||||
<input id="password_verify" name="password_verify" type="password"><br>
|
<input id="password_verify" name="password_verify" type="password" minlength="8" maxlength="10240"><br>
|
||||||
<input type="submit" value="Login">
|
<input type="submit" value="Register">
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user