second commit

This commit is contained in:
iou1name 2019-09-15 19:40:58 -04:00
parent f709f78bcf
commit 46b5a65990
6 changed files with 169 additions and 11 deletions

View File

@ -4,9 +4,19 @@ 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 psycopg2 passlib argon2_cffi` Python packages: `gunicorn aiohttp aiohttp_jinja2 asyncpg passlib argon2_cffi`
## Install ## Install
```
$ psql
postgres=# CREATE DATABASE "buckler";
postgres=# CREATE USER "buckler" WITH PASSWORD 'password';
postgres=# ALTER ROLE "buckler" SET client_encoding TO 'utf8';
postgres=# ALTER ROLE "buckler" SET default_transaction_isolation TO 'read committed';
postgres=# ALTER ROLE "buckler" SET timezone TO 'UTC';
postgres=# GRANT ALL PRIVILEGES ON DATABASE "buckler" TO "buckler";
postgres=# \q
```
1. Get on the floor 1. Get on the floor
2. Walk the dinosaur 2. Walk the dinosaur

View File

@ -2,24 +2,132 @@
""" """
A security shield for protecting a number of small web applications. A security shield for protecting a number of small web applications.
""" """
import secrets
import functools
from aiohttp import web from aiohttp import web
import jinja2 import jinja2
import aiohttp_jinja2 import aiohttp_jinja2
from aiohttp_jinja2 import render_template from aiohttp_jinja2 import render_template
from passlib.hash import argon2
import asyncpg
import config import config
app = web.Application()
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates'))
routes = web.RouteTableDef() routes = web.RouteTableDef()
@routes.get(config.url_prefix + "/", name='index') def auth_required(func):
"""
Wrapper for views with should be protected by authtication.
"""
@functools.wraps(func)
async def wrapper(request, *args, **kwargs):
login_url = request.app.router['login'].url_for()
sid = request.cookies.get('session')
try:
userid = int(request.cookies.get('userid'))
except ValueError:
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",
userid)
if sid in [s.get('id') for s in sessions]:
return await func(request, *args, **kwargs)
else:
raise web.HTTPFound(location=login_url)
return wrapper
@routes.get(config.url_prefix + '/', name='index')
@auth_required
async def index(request): async def index(request):
"""The index page.""" """The index page."""
return render_template("index.html", request, locals()) return render_template("index.html", request, locals())
app.router.add_routes(routes)
@routes.get(config.url_prefix + '/login', name='login')
@routes.post(config.url_prefix + '/login', name='login')
async def login(request):
"""Handle login."""
if request.method == 'GET':
return render_template("login.html", request, locals())
data = await request.post()
username = data.get('username')
password = data.get('password')
async with request.app['pool'].acquire() as conn:
user_info = await conn.fetchrow(
"SELECT * FROM user_info WHERE username = $1",
username)
if not user_info:
return render_template("login.html", request, locals())
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'])
sid = secrets.token_urlsafe(64)
ip_address = request.headers['X-Real-IP']
async with request.app['pool'].acquire() as conn:
await conn.execute(
"INSERT INTO user_session (user_id, id, ip_address)"
"VALUES ($1, $2, $3)",
user_info['id'],
sid,
ip_address)
resp.set_cookie('session', sid)
raise resp
else:
return render_template("login.html", request, locals())
@routes.get(config.url_prefix + '/register', name='register')
@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())
data = await request.post()
username = data.get('username')
email = data.get('email')
password = data.get('password')
password_verify = data.get('password_verify')
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)
async def init_app():
"""Initializes the application."""
app = web.Application()
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates'))
app['pool'] = await asyncpg.create_pool(**config.db)
async with app['pool'].acquire() as conn:
with open('buckler.sql', 'r') as file:
await conn.execute(file.read())
app.router.add_routes(routes)
return app
if __name__ == "__main__": if __name__ == "__main__":
app = init_app()
web.run_app(app, host='0.0.0.0', port=5400) web.run_app(app, host='0.0.0.0', port=5400)

12
buckler.sql Normal file
View File

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS user_info (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL,
email TEXT NOT NULL,
password_hash TEXT
);
CREATE TABLE IF NOT EXISTS user_session (
user_id INTEGER references user_info(id),
id TEXT PRIMARY KEY,
ip_address TEXT NOT NULL
);

View File

@ -1,7 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Configuration settings for Buckler. Configuration settings for Buckler.
url_prefix` is the root path you wish app to reside at `url_prefix` is the root path you wish app to reside at
eg. https://example.com/buckler eg. https://example.com/buckler
`db` specifies parameters for connecting to the PostgreSQL database.
""" """
url_prefix = '/buckler' url_prefix = '/buckler'
db = {
'database': 'buckler',
'user': 'buckler',
'password': 'password',
'host': 'localhost',
'port': 5432,
}

View File

@ -5,13 +5,12 @@
</head> </head>
<body> <body>
<h1>Buckler Login</h1> <h1>Buckler Login</h1>
<form action="/login" method="post" enctype="application/x-www-form-urlencoded"> <form action="{{ request.app.router['login'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
<label for="username">Username</label> <label for="username">Username</label>
<input id="username" name="password" type="text"> <input id="username" name="username" type="text"><br>
<label for="password">Password</label> <label for="password">Password</label>
<input id="password" name="password" type="password"> <input id="password" name="password" type="password"><br>
<input type="submit" value="Login"> <input type="submit" value="Login">
</form> </form>
</body> </body>
</html> </html>

20
templates/register.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Buckler - Register</title>
</head>
<body>
<h1>Buckler Register</h1>
<form action="{{ request.app.router['register'].url_for() }}" method="post" enctype="application/x-www-form-urlencoded">
<label for="username">Username</label>
<input id="username" name="username" type="text"><br>
<label for="email">Email</label>
<input id="email" name="email" type="email"><br>
<label for="password">Password</label>
<input id="password" name="password" type="password"><br>
<label for="password_verify">Verify Password</label>
<input id="password_verify" name="password_verify" type="password"><br>
<input type="submit" value="Login">
</form>
</body>
</html>