From 5660a37c09c5341b13a3f4381e75431576ebb867 Mon Sep 17 00:00:00 2001 From: iou1name Date: Fri, 8 Nov 2019 13:11:57 -0500 Subject: [PATCH] converted flask app to aiohttp app --- README.md | 4 +- auth.py | 316 ---------------------------- buckler_aiohttp.py | 61 ++++++ buckler_flask.py | 107 ---------- db.py | 193 ----------------- juice.py | 120 +++++------ models.py | 2 +- static/cbor.js | 406 ------------------------------------ static/juice-auth.js | 60 ------ templates/index.html | 2 - templates/login.html | 24 --- templates/manage.html | 68 ------ templates/register.html | 34 --- templates/register_key.html | 25 --- tools.py | 7 +- 15 files changed, 122 insertions(+), 1307 deletions(-) delete mode 100644 auth.py create mode 100644 buckler_aiohttp.py delete mode 100644 buckler_flask.py delete mode 100644 db.py delete mode 100644 static/cbor.js delete mode 100644 static/juice-auth.js delete mode 100644 templates/login.html delete mode 100644 templates/manage.html delete mode 100644 templates/register.html delete mode 100644 templates/register_key.html diff --git a/README.md b/README.md index 0e46bb2..039c93f 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ A hub for controlling IOT devices. ## Requirements Python 3.6+ -Python packages: `flask gunicorn requests passlib argon2_cffi fido2` +Python packages: `gunicorn aiohttp aiohttp_jinja2 requests` ## Install 1. Get on the floor 2. Walk the dinosaur ## Usage -`gunicorn -b localhost:5300 juice:app` +`gunicorn juice:init_app --bind localhost:5300 --worker-class aiohttp.GunicornWebWorker` diff --git a/auth.py b/auth.py deleted file mode 100644 index 30fda68..0000000 --- a/auth.py +++ /dev/null @@ -1,316 +0,0 @@ -#!/usr/bin/env python3 -""" -Contains authentication methods for the app. -""" -import os -import re -import time -import sqlite3 -import functools -from datetime import datetime - -from fido2.client import ClientData -from fido2.server import Fido2Server, RelyingParty -from fido2.ctap2 import AttestationObject, AuthenticatorData, \ - AttestedCredentialData -from fido2 import cbor -from flask import Blueprint, session, render_template, request, \ - redirect, url_for, jsonify -from passlib.hash import argon2 -from werkzeug.useragents import UserAgent - -import db -import config -from tools import make_error - -auth_views = Blueprint("auth_views", __name__) - -rp = RelyingParty('steelbea.me', 'Juice') -server = Fido2Server(rp) - - -def auth_required(func): - """ - Wrapper for views which should be protected by authentication. - """ - @functools.wraps(func) - def wrapper(*args, **kwargs): - username = session.get('username') - token = session.get('token') - if not username or not token: - return redirect(url_for('auth_views.login')) - - user = db.get_user(username) - if not user: - session.pop('username') - session.pop('token') - return redirect(url_for('auth_views.login')) - user_id = user['id'] - - tokens = db.get_tokens(user_id) - if not tokens: - return redirect(url_for('auth_views.login')) - - for token_line in tokens: - token_hash = token_line['token_hash'] - date_expired = token_line['date_expired'] - if int(time.time()) >= date_expired: - continue - if argon2.verify(token, token_hash): - db.refresh_token(token_line['id']) - return func(*args, **kwargs) - else: - session.pop('token') - return redirect(url_for('auth_views.login')) - return wrapper - - -@auth_views.route('/api/register/begin', methods=['POST']) -def register_begin(): - if not config.registration_open: - return "Registration is closed." - - username = session.get('username') - if not username: - return 'invalid' - - user = db.get_user(username) - if not user: - session.pop('username') - return 'invalid' - user_id = user['id'] - - exist_cred = db.get_credentials(user_id) - exist_cred = [AttestedCredentialData(c['credential']) for c in exist_cred] - - registration_data, state = server.register_begin({ - 'id': str(user_id).encode('utf8'), - 'name': username, - 'displayName': username, - }, exist_cred, user_verification='discouraged') - - session['state'] = state - return cbor.encode(registration_data) - - -@auth_views.route('/api/register/complete', methods=['POST']) -def register_complete(): - if not config.registration_open: - return "Registration is closed." - - username = session.get('username') - if not username: - return 'invalid' - session.pop('username') - - user = db.get_user(username) - if not user: - return 'invalid' - user_id = user['id'] - - data = cbor.decode(request.get_data()) - client_data = ClientData(data['clientDataJSON']) - att_obj = AttestationObject(data['attestationObject']) - - nick = data['security_key_nick'] - try: - assert 64 >= len(nick) >= 1 - except AssertionError: - return make_error(400, "security key nick too long/short") - - auth_data = server.register_complete( - session.pop('state'), - client_data, - att_obj - ) - - db.set_credential(user_id, nick, auth_data.credential_data) - return jsonify(ok=True) - - -@auth_views.route('/api/authenticate/begin', methods=['POST']) -def authenticate_begin(): - data = cbor.decode(request.get_data()) - username = data.get('username') - user = db.get_user(username) - if not user: - return make_error(404, "username not found") - session['username'] = username - user_id = user['id'] - - credentials = db.get_credentials(user_id) - credentials =[AttestedCredentialData(c['credential']) for c in credentials] - - auth_data, state = server.authenticate_begin(credentials) - session['state'] = state - return cbor.encode(auth_data) - - -@auth_views.route('/api/authenticate/complete', methods=['POST']) -def authenticate_complete(): - username = session.get('username') - user = db.get_user(username) - if not user: - session.pop('username') - return make_error(404, "username not found") - user_id = user['id'] - - credentials = db.get_credentials(user_id) - credentials =[AttestedCredentialData(c['credential']) for c in credentials] - - data = cbor.decode(request.get_data()) - credential_id = data['credentialId'] - client_data = ClientData(data['clientDataJSON']) - auth_data = AuthenticatorData(data['authenticatorData']) - signature = data['signature'] - - server.authenticate_complete( - session.pop('state'), - credentials, - credential_id, - client_data, - auth_data, - signature - ) - - token = os.urandom(32) - token_hash = argon2.hash(token) - user_agent = request.user_agent.string - ip_address = request.headers.get("X-Real-Ip") - db.set_token(user_id, user_agent, ip_address, token_hash) - session['token'] = token - - return jsonify(ok=True) - - -@auth_views.route('/register', methods=['GET', 'POST']) -def register(): - """ - Registration page. - """ - if not config.registration_open: - return "Registration is closed." - - if request.method == 'GET': - params = { - 'form_url': url_for('auth_views.register'), - 'url_prefix': config.url_prefix, - } - return render_template('register.html', **params) - - username = request.form.get('username') - email = request.form.get('email') - - try: - assert 64 >= len(username) >= 3 - except AssertionError: - return make_error(400, "username too long/short") - - try: - assert 100 >= len(email) - except AssertionError: - return "email too long" - - try: - user_id = db.set_user(username, email) - except sqlite3.IntegrityError as e: - field = re.search(r'user\.(.*)', str(e)).group(1) - return make_error(400, f"{field} already exists") - session['username'] = username - session['user_id'] = user_id - - params = { - 'url_prefix': config.url_prefix, - } - return render_template('register_key.html', **params) - - -@auth_views.route('/login') -def login(): - """ - Login page. - """ - params = { - 'url_prefix': config.url_prefix, - } - return render_template('login.html', **params) - - -@auth_views.route('/manage') -@auth_required -def manage(): - """ - Allows a user to manage their security keys and tokens. - """ - url_prefix = config.url_prefix - username = session['username'] - user_id = db.get_user(username)['id'] - - credentials = db.get_credentials(user_id) - - tokens = db.get_tokens(user_id) - tokens_pretty = [] - for token in tokens: - token_pretty = {} - token_pretty['id'] = token['id'] - token_pretty['user_agent'] = UserAgent(token['user_agent']) - token_pretty['ip_address'] = token['ip_address'] - di = token['date_issued'] - di = datetime.utcfromtimestamp(di).strftime('%Y-%m-%d') - token_pretty['date_issued'] = di - de = token['date_expired'] - de = datetime.utcfromtimestamp(de).strftime('%Y-%m-%d') - token_pretty['date_expired'] = de - tokens_pretty.append(token_pretty) - return render_template('manage.html', **locals()) - - -@auth_views.route('/delete_key') -@auth_required -def delete_key(): - """ - Allows a user to delete a security key credential. - """ - cred_id = request.args.get('key_id') - username = session['username'] - user_id = db.get_user(username)['id'] - - cred = db.get_credential(cred_id) - if not cred: - return make_error(404, "security key not found") - if cred['user_id'] == user_id: - db.delete_credential(cred_id) - return jsonify(ok=True) - else: - return make_error(404, "security key not found") - - -@auth_views.route('/delete_token') -@auth_required -def delete_token(): - """ - Allows a user to delete a token. - """ - token_id = request.args.get('token_id') - username = session['username'] - user_id = db.get_user(username)['id'] - - token = db.get_token(token_id) - if not token: - return make_error(404, "token not found") - if token['user_id'] == user_id: - db.delete_token(token_id) - return jsonify(ok=True) - else: - return make_error(404, "token not found") - -@auth_views.route('/add_key') -@auth_required -def add_key(): - """ - Allows a user to add a new security key to their account. - """ - params = { - 'url_prefix': config.url_prefix, - } - return render_template('register_key.html', **params) diff --git a/buckler_aiohttp.py b/buckler_aiohttp.py new file mode 100644 index 0000000..bb10047 --- /dev/null +++ b/buckler_aiohttp.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +Session interface middlewares to integrate the aiohttp app with Buckler. +""" +import json +from datetime import datetime + +import aiohttp +from aiohttp import web + +import config + +@web.middleware +async def buckler_session(request, handler): + """ + Verifies the user with the configured Buckler app and retrieves any + session data they may have. Redirects them to the login page otherwise. + """ + user_id = request.cookies.get('userid', '') + user_sid = request.cookies.get('session', '') + + url = config.buckler['url'] + '/get_session' + params = { + 'app_id': config.buckler['app_id'], + 'app_key': config.buckler['app_key'], + 'userid': user_id, + 'session': user_sid + } + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params) as resp: + data = await resp.json() + if data.get('error'): + raise web.HTTPFound(location=config.buckler['login_url']) + request['session'] = data['session_data'] + request['meta'] = data['meta'] + + resp = await handler(request) + + if request['session'] != data['session_data']: # session data modified + url = config.buckler['url'] + '/set_session' + data = json.dumps(request['session']) + session.post(url, params=params, data=data) # TODO: error handle? + + last_used = datetime.fromisoformat(request['meta']['last_used']) + now = datetime.now(last_used.tzinfo) + delta = now - last_used + if delta.seconds > 600: + resp.set_cookie( + 'userid', + user_id, + max_age=30*24*60*60, + secure=True, + httponly=True) + resp.set_cookie( + 'session', + user_sid, + max_age=30*24*60*60, + secure=True, + httponly=True) + + return resp diff --git a/buckler_flask.py b/buckler_flask.py deleted file mode 100644 index 0ed5429..0000000 --- a/buckler_flask.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 -""" -Session interface middlewares to integrate the flask app with Buckler. -""" -import json -import urllib.parse -import urllib.request -from datetime import datetime - -from flask.sessions import SessionInterface, SessionMixin -from flask import session, redirect, request - -import config - -class BucklerSessionInterface(SessionInterface): - """ - Queries the Buckler server for session data to the current user and - application. - """ - - def __init__(self): - self.url = config.buckler['url'] - self.app_id = config.buckler['app_id'] - self.app_key = config.buckler['app_key'] - - def open_session(self, app, request): - """Called when a request is initiated.""" - user_id = request.cookies.get('userid') - user_sid = request.cookies.get('session') - - params = { - 'app_id': self.app_id, - 'app_key': self.app_key, - 'userid': user_id, - 'session': user_sid - } - params = urllib.parse.urlencode(params) - req = urllib.request.Request(self.url + f"/get_session?{params}") - res = urllib.request.urlopen(req) - data = json.loads(res.read()) - if data.get('error'): - return None - session = BucklerSession() - session.update(data['session_data']) - session.meta = data['meta'] - session.cookies = request.cookies - return session - - def save_session(self, app, session, response): - """Called at the end of a request.""" - if not session.modified: - return - user_id = session.meta.get('user_id') - user_sid = session.meta.get('user_sid') - - params = { - 'app_id': self.app_id, - 'app_key': self.app_key, - 'userid': user_id, - 'session': user_sid - } - params = urllib.parse.urlencode(params) - data = json.dumps(session) - req = urllib.request.Request( - self.url + f"/set_session?{params}", - data=data.encode('utf8'), - method='POST') - res = urllib.request.urlopen(req) - - last_used = datetime.fromisoformat(session.meta['last_used']) - now = datetime.now(last_used.tzinfo) - delta = now - last_used - if delta.seconds > 600: - response.set_cookie( - 'userid', - session.cookies['userid'], - max_age=30*24*60*60, - secure=True, - httponly=True) - response.set_cookie( - 'session', - session.cookies['session'], - max_age=30*24*60*60, - secure=True, - httponly=True) - - -class BucklerSession(dict, SessionMixin): - """A server side session class based on the Buckler security shield.""" - def __init__(self): - super().__init__() - self.modified = False - - def __setitem__(self, key, value): - super().__setitem__(key, value) - self.modified = True - - -def require_auth(): - """ - Requires the user to be properly authenticated with Buckler before - accessing any views on the application. - """ - if not hasattr(session, 'meta'): - resp = redirect(config.buckler['login_url']) - resp.set_cookie('redirect', request.url) - return resp diff --git a/db.py b/db.py deleted file mode 100644 index fbbb513..0000000 --- a/db.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env python3 -""" -A module for interacting with Juice's database. -""" -import time -import sqlite3 -import threading - - -DB_LOCK = threading.Lock() - - -def init_db(): - """ - Checks to see if the database is initialized yet or not. If not, - the appropriate tables are created. - """ - con = sqlite3.connect('juice.db') - cur = con.cursor() - try: - cur.execute("SELECT * FROM user LIMIT 1").fetchone() - cur.execute("SELECT * FROM credential LIMIT 1").fetchone() - cur.execute("SELECT * FROM token LIMIT 1").fetchone() - except sqlite3.OperationalError: - cur.execute("CREATE TABLE user(" - "id INTEGER PRIMARY KEY, " - "username TEXT UNIQUE, " - "email TEXT UNIQUE" - ")" - ) - cur.execute("CREATE TABLE credential(" - "id INTEGER PRIMARY KEY, " - "user_id INTEGER REFERENCES user(id) ON UPDATE CASCADE, " - "nick TEXT, " - "credential BLOB" - ")" - ) - cur.execute("CREATE TABLE token(" - "id INTEGER PRIMARY KEY, " - "user_id INTEGER REFERENCES user(id) ON UPDATE CASCADE, " - "user_agent TEXT, " - "ip_address TEXT, " - "token_hash TEXT, " - "date_issued INTEGER, " - "date_expired INTEGER" - ")" - ) - con.commit() - con.close() -init_db() - - -def db_execute(*args, **kwargs): - """ - Opens a connection to the app's database and executes the SQL - statements passed to this function. - """ - with sqlite3.connect('juice.db') as con: - DB_LOCK.acquire() - con.row_factory = sqlite3.Row - cur = con.cursor() - res = cur.execute(*args, **kwargs) - DB_LOCK.release() - return res - - -def set_user(username, email): - """ - Adds a new user. - """ - user_id = db_execute( - "INSERT INTO user(username, email) VALUES (?, ?)", - (username, email) - ).lastrowid - return user_id - - -def get_user(username): - """ - Returns a user entry. - """ - data = db_execute( - "SELECT * FROM user WHERE username = ?", - (username,) - ).fetchone() - return data - - -def set_token(user_id, user_agent, ip_address, token_hash): - """ - Sets a user's token hash. - """ - date_issued = int(time.time()) - date_expired = date_issued + 30*24*60*60 - token_id = db_execute( - "INSERT INTO " - "token(" - "user_id, user_agent, ip_address, token_hash, date_issued,date_expired" - ") " - "VALUES (?,?,?,?,?,?)", - (user_id, user_agent, ip_address, token_hash, date_issued,date_expired) - ).lastrowid - return token_id - - -def get_token(token_id): - """ - Returns the token of the specified id. - """ - data = db_execute( - "SELECT * FROM token WHERE id = ?", - (token_id,) - ).fetchone() - return data - - -def delete_token(token_id): - """ - Deletes the token of the specified id. - """ - db_execute( - "DELETE FROM token WHERE id = ?", - (token_id,) - ).fetchone() - return True - - -def get_tokens(user_id): - """ - Returns all tokens assigned to a user. - """ - data = db_execute( - "SELECT * FROM token WHERE user_id = ?", - (user_id,) - ).fetchall() - return data - - -def refresh_token(token_id): - """ - Extends a token's expiration date. - """ - new_date_expired = int(time.time()) + 30*24*60*60 - db_execute( - "UPDATE token SET date_expired = ? WHERE id = ?", - (new_date_expired, token_id) - ) - return True - - -def set_credential(user_id, nick, credential): - """ - Adds a credential to the database. - """ - cred_id = db_execute( - "INSERT INTO credential(user_id, nick, credential) " - "VALUES (?, ?, ?)", - (user_id, nick, credential) - ).lastrowid - return cred_id - - -def get_credential(cred_id): - """ - Returns the credential of the specified id. - """ - data = db_execute( - "SELECT * FROM credential WHERE id = ?", - (cred_id,) - ).fetchone() - return data - - -def delete_credential(cred_id): - """ - Deletes the credential of the specified id. - """ - db_execute( - "DELETE FROM credential WHERE id = ?", - (cred_id,) - ).fetchone() - return True - - -def get_credentials(user_id): - """ - Returns all credentials registered to a user. - """ - data = db_execute( - "SELECT * FROM credential WHERE user_id = ?", - (user_id,) - ).fetchall() - return data diff --git a/juice.py b/juice.py index 4924f3a..f12ccf6 100644 --- a/juice.py +++ b/juice.py @@ -7,23 +7,25 @@ import re import copy import json -from flask import Flask, render_template, request, abort, jsonify -from flask import Blueprint +from aiohttp import web +import jinja2 +import aiohttp_jinja2 +from aiohttp_jinja2 import render_template +#import uvloop +import requests -import auth import config import models -from auth import auth_required +import buckler_aiohttp from tools import make_error -import buckler_flask -app_views = Blueprint('app_views', __name__) +#uvloop.install() +network = models.init_network() +routes = web.RouteTableDef() -@app_views.route('/') -def index(): - """ - The index page. - """ +@routes.get('/', name='index') +async def index(request): + """The index page.""" global network init_state = {} for device in network: @@ -31,17 +33,16 @@ def index(): for sub_dev in device.sub_devices: device_state[sub_dev.id] = sub_dev.state init_state[device.id] = device_state - return render_template('index.html', network=network,init_state=init_state) + d = {'network': network, 'init_state': init_state} + return render_template('index.html', request, d) -@app_views.route('/toggle') -def toggle(): - """ - Toggles the state of a RelayDevice. - """ +@routes.get('/toggle', name='toggle') +async def toggle(request): + """Toggles the state of a RelayDevice.""" global network - device_id = request.args.get('device_id') - sub_dev_id = request.args.get('sub_dev_id') + device_id = request.query.get('device_id') + sub_dev_id = request.query.get('sub_dev_id') for device in network: if device.id == device_id: @@ -52,18 +53,16 @@ def toggle(): if not res: return make_error(404, "sub_dev_id not found") models.save_network(network) - return res + return web.json_response(res) -@app_views.route('/edit') -def edit(): - """ - Edits the text of a particular field. - """ +@routes.get('/edit', name='edit') +async def edit(request): + """Edits the text of a particular field.""" global network - device_id = request.args.get('device_id') - sub_dev_id = request.args.get('sub_dev_id') - field = request.args.get('field') - value = request.args.get('value') + device_id = request.query.get('device_id') + sub_dev_id = request.query.get('sub_dev_id') + field = request.query.get('field') + value = request.query.get('value') for device in network: if device.id == device_id: @@ -96,17 +95,17 @@ def edit(): 'value': value } models.save_network(network) - return json.dumps(data) + return web.json_response(data) -@app_views.route('/new_device') -def new_device(): +@routes.get('/new_device', name='new_device') +async def new_device(request): """ Allows adding a new device. Accepts device_type parameter, returns the device_id. """ global network - device_type = request.args.get('device_type') + device_type = request.query.get('device_type') if device_type == 'RelayDevice': device = models.RelayDevice() @@ -127,17 +126,15 @@ def new_device(): network.append(device) models.save_network(network) data = {'device_id': device.id} - return json.dumps(data) + return web.json_response(data) -@app_views.route('/lock_device') -def lock_device(): - """ - Locks or unlocks a device to prevent or allow editing it's fields. - """ +@routes.get('/lock_device', name='lock_device') +async def lock_device(request): + """Locks or unlocks a device to prevent or allow editing it's fields.""" global network - device_id = request.args.get('device_id') - locked = request.args.get('locked') == 'true' + device_id = request.query.get('device_id') + locked = request.query.get('locked') == 'true' for device in network: if device.id == device_id: @@ -151,16 +148,14 @@ def lock_device(): device.locked = False models.save_network(network) - return jsonify(device_id=device.id, locked=device.locked) + return web.json_response({'device_id': device.id, 'locked': device.locked}) -@app_views.route('/delete') -def delete(): - """ - Deletes a device. - """ +@routes.get('/delete', name='delete') +async def delete(request): + """Deletes a device.""" global network - device_id = request.args.get('device_id') + device_id = request.query.get('device_id') for device in network: if device.id == device_id: @@ -171,25 +166,18 @@ def delete(): network.remove(device) models.save_network(network) - return jsonify(True) + return web.json_response(True) -app = Flask(__name__) -app.session_interface = buckler_flask.BucklerSessionInterface() -app.before_request(buckler_flask.require_auth) -app.register_blueprint(app_views, url_prefix=config.url_prefix) -#app.register_blueprint(auth.auth_views, url_prefix=config.url_prefix) -app.jinja_env.undefined = "StrictUndefined" -if os.path.isfile('secret_key'): - with open('secret_key', 'rb') as file: - app.secret_key = file.read() -else: - secret_key = os.urandom(32) - app.secret_key = secret_key - with open('secret_key', 'wb') as file: - file.write(secret_key) -network = models.init_network() +async def init_app(): + """Initializes the application.""" + app = web.Application(middlewares=[buckler_aiohttp.buckler_session]) + aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates')) + #app.on_startup.append(start_background_tasks) + #app.on_cleanup.append(cleanup_background_tasks) + app.router.add_routes(routes) -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5300, debug=True) + app_wrap = web.Application() + app_wrap.add_subapp(config.url_prefix, app) + return app_wrap diff --git a/models.py b/models.py index ee6c2ab..6384ff2 100644 --- a/models.py +++ b/models.py @@ -63,7 +63,7 @@ class RelayDevice: ).groups()[0] == 'High' sub_dev.state = state - return json.dumps({'ok': True, sub_dev_id: state}) + return {'ok': True, sub_dev_id: state} def from_dict(self, data): """ diff --git a/static/cbor.js b/static/cbor.js deleted file mode 100644 index 3e1f300..0000000 --- a/static/cbor.js +++ /dev/null @@ -1,406 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2016 Patrick Gansterer - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -(function(global, undefined) { "use strict"; -var POW_2_24 = 5.960464477539063e-8, - POW_2_32 = 4294967296, - POW_2_53 = 9007199254740992; - -function encode(value) { - var data = new ArrayBuffer(256); - var dataView = new DataView(data); - var lastLength; - var offset = 0; - - function prepareWrite(length) { - var newByteLength = data.byteLength; - var requiredLength = offset + length; - while (newByteLength < requiredLength) - newByteLength <<= 1; - if (newByteLength !== data.byteLength) { - var oldDataView = dataView; - data = new ArrayBuffer(newByteLength); - dataView = new DataView(data); - var uint32count = (offset + 3) >> 2; - for (var i = 0; i < uint32count; ++i) - dataView.setUint32(i << 2, oldDataView.getUint32(i << 2)); - } - - lastLength = length; - return dataView; - } - function commitWrite() { - offset += lastLength; - } - function writeFloat64(value) { - commitWrite(prepareWrite(8).setFloat64(offset, value)); - } - function writeUint8(value) { - commitWrite(prepareWrite(1).setUint8(offset, value)); - } - function writeUint8Array(value) { - var dataView = prepareWrite(value.length); - for (var i = 0; i < value.length; ++i) - dataView.setUint8(offset + i, value[i]); - commitWrite(); - } - function writeUint16(value) { - commitWrite(prepareWrite(2).setUint16(offset, value)); - } - function writeUint32(value) { - commitWrite(prepareWrite(4).setUint32(offset, value)); - } - function writeUint64(value) { - var low = value % POW_2_32; - var high = (value - low) / POW_2_32; - var dataView = prepareWrite(8); - dataView.setUint32(offset, high); - dataView.setUint32(offset + 4, low); - commitWrite(); - } - function writeTypeAndLength(type, length) { - if (length < 24) { - writeUint8(type << 5 | length); - } else if (length < 0x100) { - writeUint8(type << 5 | 24); - writeUint8(length); - } else if (length < 0x10000) { - writeUint8(type << 5 | 25); - writeUint16(length); - } else if (length < 0x100000000) { - writeUint8(type << 5 | 26); - writeUint32(length); - } else { - writeUint8(type << 5 | 27); - writeUint64(length); - } - } - - function encodeItem(value) { - var i; - - if (value === false) - return writeUint8(0xf4); - if (value === true) - return writeUint8(0xf5); - if (value === null) - return writeUint8(0xf6); - if (value === undefined) - return writeUint8(0xf7); - - switch (typeof value) { - case "number": - if (Math.floor(value) === value) { - if (0 <= value && value <= POW_2_53) - return writeTypeAndLength(0, value); - if (-POW_2_53 <= value && value < 0) - return writeTypeAndLength(1, -(value + 1)); - } - writeUint8(0xfb); - return writeFloat64(value); - - case "string": - var utf8data = []; - for (i = 0; i < value.length; ++i) { - var charCode = value.charCodeAt(i); - if (charCode < 0x80) { - utf8data.push(charCode); - } else if (charCode < 0x800) { - utf8data.push(0xc0 | charCode >> 6); - utf8data.push(0x80 | charCode & 0x3f); - } else if (charCode < 0xd800) { - utf8data.push(0xe0 | charCode >> 12); - utf8data.push(0x80 | (charCode >> 6) & 0x3f); - utf8data.push(0x80 | charCode & 0x3f); - } else { - charCode = (charCode & 0x3ff) << 10; - charCode |= value.charCodeAt(++i) & 0x3ff; - charCode += 0x10000; - - utf8data.push(0xf0 | charCode >> 18); - utf8data.push(0x80 | (charCode >> 12) & 0x3f); - utf8data.push(0x80 | (charCode >> 6) & 0x3f); - utf8data.push(0x80 | charCode & 0x3f); - } - } - - writeTypeAndLength(3, utf8data.length); - return writeUint8Array(utf8data); - - default: - var length; - if (Array.isArray(value)) { - length = value.length; - writeTypeAndLength(4, length); - for (i = 0; i < length; ++i) - encodeItem(value[i]); - } else if (value instanceof Uint8Array) { - writeTypeAndLength(2, value.length); - writeUint8Array(value); - } else { - var keys = Object.keys(value); - length = keys.length; - writeTypeAndLength(5, length); - for (i = 0; i < length; ++i) { - var key = keys[i]; - encodeItem(key); - encodeItem(value[key]); - } - } - } - } - - encodeItem(value); - - if ("slice" in data) - return data.slice(0, offset); - - var ret = new ArrayBuffer(offset); - var retView = new DataView(ret); - for (var i = 0; i < offset; ++i) - retView.setUint8(i, dataView.getUint8(i)); - return ret; -} - -function decode(data, tagger, simpleValue) { - var dataView = new DataView(data); - var offset = 0; - - if (typeof tagger !== "function") - tagger = function(value) { return value; }; - if (typeof simpleValue !== "function") - simpleValue = function() { return undefined; }; - - function commitRead(length, value) { - offset += length; - return value; - } - function readArrayBuffer(length) { - return commitRead(length, new Uint8Array(data, offset, length)); - } - function readFloat16() { - var tempArrayBuffer = new ArrayBuffer(4); - var tempDataView = new DataView(tempArrayBuffer); - var value = readUint16(); - - var sign = value & 0x8000; - var exponent = value & 0x7c00; - var fraction = value & 0x03ff; - - if (exponent === 0x7c00) - exponent = 0xff << 10; - else if (exponent !== 0) - exponent += (127 - 15) << 10; - else if (fraction !== 0) - return (sign ? -1 : 1) * fraction * POW_2_24; - - tempDataView.setUint32(0, sign << 16 | exponent << 13 | fraction << 13); - return tempDataView.getFloat32(0); - } - function readFloat32() { - return commitRead(4, dataView.getFloat32(offset)); - } - function readFloat64() { - return commitRead(8, dataView.getFloat64(offset)); - } - function readUint8() { - return commitRead(1, dataView.getUint8(offset)); - } - function readUint16() { - return commitRead(2, dataView.getUint16(offset)); - } - function readUint32() { - return commitRead(4, dataView.getUint32(offset)); - } - function readUint64() { - return readUint32() * POW_2_32 + readUint32(); - } - function readBreak() { - if (dataView.getUint8(offset) !== 0xff) - return false; - offset += 1; - return true; - } - function readLength(additionalInformation) { - if (additionalInformation < 24) - return additionalInformation; - if (additionalInformation === 24) - return readUint8(); - if (additionalInformation === 25) - return readUint16(); - if (additionalInformation === 26) - return readUint32(); - if (additionalInformation === 27) - return readUint64(); - if (additionalInformation === 31) - return -1; - throw "Invalid length encoding"; - } - function readIndefiniteStringLength(majorType) { - var initialByte = readUint8(); - if (initialByte === 0xff) - return -1; - var length = readLength(initialByte & 0x1f); - if (length < 0 || (initialByte >> 5) !== majorType) - throw "Invalid indefinite length element"; - return length; - } - - function appendUtf16Data(utf16data, length) { - for (var i = 0; i < length; ++i) { - var value = readUint8(); - if (value & 0x80) { - if (value < 0xe0) { - value = (value & 0x1f) << 6 - | (readUint8() & 0x3f); - length -= 1; - } else if (value < 0xf0) { - value = (value & 0x0f) << 12 - | (readUint8() & 0x3f) << 6 - | (readUint8() & 0x3f); - length -= 2; - } else { - value = (value & 0x0f) << 18 - | (readUint8() & 0x3f) << 12 - | (readUint8() & 0x3f) << 6 - | (readUint8() & 0x3f); - length -= 3; - } - } - - if (value < 0x10000) { - utf16data.push(value); - } else { - value -= 0x10000; - utf16data.push(0xd800 | (value >> 10)); - utf16data.push(0xdc00 | (value & 0x3ff)); - } - } - } - - function decodeItem() { - var initialByte = readUint8(); - var majorType = initialByte >> 5; - var additionalInformation = initialByte & 0x1f; - var i; - var length; - - if (majorType === 7) { - switch (additionalInformation) { - case 25: - return readFloat16(); - case 26: - return readFloat32(); - case 27: - return readFloat64(); - } - } - - length = readLength(additionalInformation); - if (length < 0 && (majorType < 2 || 6 < majorType)) - throw "Invalid length"; - - switch (majorType) { - case 0: - return length; - case 1: - return -1 - length; - case 2: - if (length < 0) { - var elements = []; - var fullArrayLength = 0; - while ((length = readIndefiniteStringLength(majorType)) >= 0) { - fullArrayLength += length; - elements.push(readArrayBuffer(length)); - } - var fullArray = new Uint8Array(fullArrayLength); - var fullArrayOffset = 0; - for (i = 0; i < elements.length; ++i) { - fullArray.set(elements[i], fullArrayOffset); - fullArrayOffset += elements[i].length; - } - return fullArray; - } - return readArrayBuffer(length); - case 3: - var utf16data = []; - if (length < 0) { - while ((length = readIndefiniteStringLength(majorType)) >= 0) - appendUtf16Data(utf16data, length); - } else - appendUtf16Data(utf16data, length); - return String.fromCharCode.apply(null, utf16data); - case 4: - var retArray; - if (length < 0) { - retArray = []; - while (!readBreak()) - retArray.push(decodeItem()); - } else { - retArray = new Array(length); - for (i = 0; i < length; ++i) - retArray[i] = decodeItem(); - } - return retArray; - case 5: - var retObject = {}; - for (i = 0; i < length || length < 0 && !readBreak(); ++i) { - var key = decodeItem(); - retObject[key] = decodeItem(); - } - return retObject; - case 6: - return tagger(decodeItem(), length); - case 7: - switch (length) { - case 20: - return false; - case 21: - return true; - case 22: - return null; - case 23: - return undefined; - default: - return simpleValue(length); - } - } - } - - var ret = decodeItem(); - if (offset !== data.byteLength) - throw "Remaining bytes"; - return ret; -} - -var obj = { encode: encode, decode: decode }; - -if (typeof define === "function" && define.amd) - define("cbor/cbor", obj); -else if (typeof module !== "undefined" && module.exports) - module.exports = obj; -else if (!global.CBOR) - global.CBOR = obj; - -})(this); diff --git a/static/juice-auth.js b/static/juice-auth.js deleted file mode 100644 index bb8f46b..0000000 --- a/static/juice-auth.js +++ /dev/null @@ -1,60 +0,0 @@ -function register() { - fetch(url_prefix + '/api/register/begin', { - method: 'POST', - }).then(function(response) { - if(!response.ok) { throw new Error('Error getting registration data!'); } - return response.arrayBuffer(); - }).then(CBOR.decode).then(function(options) { - return navigator.credentials.create(options); - }).then(function(attestation) { - return fetch(url_prefix + '/api/register/complete', { - method: 'POST', - headers: {'Content-Type': 'application/cbor'}, - body: CBOR.encode({ - "attestationObject": new Uint8Array(attestation.response.attestationObject), - "clientDataJSON": new Uint8Array(attestation.response.clientDataJSON), - "security_key_nick": document.querySelector('#security_key_nick').value, - }) - }); - }).then(function(response) { - return response.json(); - }).then(function(json) { - console.log(json); - if (!json.ok) { throw new Error('HTTP error, status = ' + json.status + ', message = ' + json.message); } - window.location = url_prefix + '/login'; - }); -} - -function login() { - fetch(url_prefix + '/api/authenticate/begin', { - method: 'POST', - headers: {'Content-Type': 'application/cbor'}, - body: CBOR.encode({ - "username": document.querySelector('#username').value, - }) - }).then(function(response) { - if(!response.ok) { throw new Error('Error getting registration data!'); } - return response.arrayBuffer(); - }).then(CBOR.decode).then(function(options) { - return navigator.credentials.get(options); - }).then(function(assertion) { - return fetch(url_prefix + '/api/authenticate/complete', { - method: 'POST', - headers: {'Content-Type': 'application/cbor'}, - body: CBOR.encode({ - "credentialId": new Uint8Array(assertion.rawId), - "authenticatorData": new Uint8Array(assertion.response.authenticatorData), - "clientDataJSON": new Uint8Array(assertion.response.clientDataJSON), - "signature": new Uint8Array(assertion.response.signature) - }) - }); - }, function(reason) { - console.log('navigator.credentials.get() failed for the following reason: ' + reason); - }).then(function(response) { - return response.json(); - }).then(function(json) { - console.log(json); - if (!json.ok) { throw new Error('HTTP error, status = ' + json.status + ', message = ' + json.message); } - window.location = url_prefix + '/'; - }); -} diff --git a/templates/index.html b/templates/index.html index 1ea49db..2be219e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -22,9 +22,7 @@
diff --git a/templates/login.html b/templates/login.html deleted file mode 100644 index dab674a..0000000 --- a/templates/login.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - Juice - Login - - - - - - - - - -
-
-
-

Login

-

-

-

-
-
- - diff --git a/templates/manage.html b/templates/manage.html deleted file mode 100644 index 30cf57d..0000000 --- a/templates/manage.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - Juice - Manage - - - - - - - - -
-
-
-

Security Keys

- - - - - - - - - {% for credential in credentials %} - - - - - {% endfor %} - - - - - - - -
NickDelete
{{ credential.nick }}
New key
-

Tokens

- - - - - - - - - - - - - {% for token in tokens_pretty %} - - - - - - - - - {% endfor %} - -
OSBrowserIp AddressDate IssuedDate ExpiresDelete
{{ token.user_agent.platform }}{{ token.user_agent.browser }} {{token.user_agent.version }}{{ token.ip_address }}{{ token.date_issued }}{{ token.date_expired }}
-
-
-
- - diff --git a/templates/register.html b/templates/register.html deleted file mode 100644 index a73e081..0000000 --- a/templates/register.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - Juice - Register - - - - - - -
-
-
-

Register

-
- - - - - - - - - - - - -
Username
Email
-
-
-
-
- - diff --git a/templates/register_key.html b/templates/register_key.html deleted file mode 100644 index 35e86f8..0000000 --- a/templates/register_key.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - Juice - Register Security Key - - - - - - - - - -
-
-
-

Register Security Key

-

Upon clicking submit, your security key will begin flashing. Have it ready. -

-

-

-
-
- - diff --git a/tools.py b/tools.py index e3a22fd..3bfea86 100644 --- a/tools.py +++ b/tools.py @@ -2,13 +2,14 @@ """ Miscellaneous tools and helper functions. """ -from flask import jsonify +from aiohttp import web def make_error(code, message): """ Returns a JSON error. """ - res = jsonify(ok=False, status=code, message=message) - res.status_code = code + d = {'ok': False, 'status': code, 'message': message} + res = web.json_response(d) + res.set_status(code) return res