Compare commits

..

2 Commits

Author SHA1 Message Date
43026fa63a gitignore 2019-11-08 13:13:56 -05:00
5660a37c09 converted flask app to aiohttp app 2019-11-08 13:11:57 -05:00
16 changed files with 122 additions and 1310 deletions

3
.gitignore vendored
View File

@ -3,6 +3,3 @@ __pycache__/
*.swo
devices.json
config.py
secret_key
cred
juice.db

View File

@ -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`

316
auth.py
View File

@ -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)

61
buckler_aiohttp.py Normal file
View File

@ -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

View File

@ -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

193
db.py
View File

@ -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

120
juice.py
View File

@ -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

View File

@ -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):
"""

View File

@ -1,406 +0,0 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2016 Patrick Gansterer <paroga@paroga.com>
*
* 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);

View File

@ -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 + '/';
});
}

View File

@ -22,9 +22,7 @@
</div>
<nav>
<span class="font-awesome" title="Home"><a href="./">&#xe806;</a></span>
<span class="font-awesome" title="Manage security keys and tokens"><a href="./manage">&#xe807;</a></span>
<span class="font-awesome" title="Add new device" onclick="new_device()">&#xe802;</span>
<span class="font-awesome" title="Logout"><a href="./logout">&#xe805;</a></span>
</nav>
</header>
<main>

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Juice - Login</title>
<link rel="stylesheet" type="text/css" href="/static/juice.css">
<script type="text/javascript" src="/static/cbor.js"></script>
<script type="text/javascript" src="/static/juice-auth.js"></script>
<script>url_prefix = '{{ url_prefix }}';</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="An IOT hub.">
<link rel="icon" href="/static/orange-juice.svg">
</head>
<body>
<main>
<div id="devices">
<div class="device">
<h1>Login</h1>
<p><label for="username">Username</label> <input id="username" type="text" minlength="3" maxlength="64" required>
<p><input type="button" value="Login" onclick="login()">
</div>
</div>
</main>
</body>
</html>

View File

@ -1,68 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Juice - Manage</title>
<link rel="stylesheet" type="text/css" href="/static/juice.css">
<script type="text/javascript" src="/static/juice.js"></script>
<script>url_prefix = '{{ url_prefix }}';</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="An IOT hub.">
<link rel="icon" href="/static/orange-juice.svg">
</head>
<body>
<main>
<div id="devices">
<div class="device">
<h2>Security Keys</h2>
<table id="credentials" class="manage-table">
<thead>
<tr>
<th>Nick</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{% for credential in credentials %}
<tr id="key_{{ credential.id }}">
<td>{{ credential.nick }}</td>
<td><span class="delete font-awesome" onclick="delete_key({{ credential.id }})">&#xe804;</span></td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td><span title="Add a new security key" class="add font-awesome" onclick="window.location=url_prefix+'/add_key'">&#xe802;</span></td>
<td>New key</td>
</tr>
</tfoot>
</table>
<h2 class="sub-table">Tokens</h2>
<table id="tokens" class="manage-table">
<thead>
<tr>
<th>OS</th>
<th>Browser</th>
<th>Ip Address</th>
<th>Date Issued</th>
<th>Date Expires</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{% for token in tokens_pretty %}
<tr id="token_{{ token.id }}">
<td>{{ token.user_agent.platform }}</td>
<td>{{ token.user_agent.browser }} {{token.user_agent.version }}</td>
<td>{{ token.ip_address }}</td>
<td>{{ token.date_issued }}</td>
<td>{{ token.date_expired }}</td>
<td><span class="delete font-awesome" onclick="delete_token({{ token.id }})">&#xe804;</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</main>
</body>
</html>

View File

@ -1,34 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Juice - Register</title>
<link rel="stylesheet" type="text/css" href="/static/juice.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="An IOT hub.">
<link rel="icon" href="/static/orange-juice.svg">
</head>
<body>
<main>
<div id="devices">
<div class="device">
<h1>Register</h1>
<form method="post" action="{{ form_url }}">
<table>
<tbody>
<tr>
<td>Username</td><td><input name="username" type="text" minlength="3" maxlength="64" required></td>
</tr>
<tr>
<td>Email</td><td><input name="email" type="email"></td>
</tr>
<tr>
<td><input type="submit" value="Submit"></td>
</tr>
</tbody>
</table>
</form>
</div>
</div>
</main>
</body>
</html>

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Juice - Register Security Key</title>
<link rel="stylesheet" type="text/css" href="/static/juice.css">
<script type="text/javascript" src="/static/cbor.js"></script>
<script type="text/javascript" src="/static/juice-auth.js"></script>
<script>url_prefix = '{{ url_prefix }}';</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="An IOT hub.">
<link rel="icon" href="/static/orange-juice.svg">
</head>
<body>
<main>
<div id="devices">
<div class="device">
<h1>Register Security Key</h1>
<p>Upon clicking submit, your security key will begin flashing. Have it ready.
<p><label for="security_key_nick">Security key nick</label> <input id="security_key_nick" type="text" minlength="1" maxlength="64" required>
<p><input type="button" value="Submit" onclick="register()">
</div>
</div>
</main>
</body>
</html>

View File

@ -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