Compare commits
No commits in common. "43026fa63a19424f8dd79a5b058d03de9b1cf471" and "4e40fbdfb6017f70fc4fde3a4f9970cb66e0ea7d" have entirely different histories.
43026fa63a
...
4e40fbdfb6
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -3,3 +3,6 @@ __pycache__/
|
||||||
*.swo
|
*.swo
|
||||||
devices.json
|
devices.json
|
||||||
config.py
|
config.py
|
||||||
|
secret_key
|
||||||
|
cred
|
||||||
|
juice.db
|
||||||
|
|
|
@ -3,11 +3,11 @@ A hub for controlling IOT devices.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
Python 3.6+
|
Python 3.6+
|
||||||
Python packages: `gunicorn aiohttp aiohttp_jinja2 requests`
|
Python packages: `flask gunicorn requests passlib argon2_cffi fido2`
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
1. Get on the floor
|
1. Get on the floor
|
||||||
2. Walk the dinosaur
|
2. Walk the dinosaur
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
`gunicorn juice:init_app --bind localhost:5300 --worker-class aiohttp.GunicornWebWorker`
|
`gunicorn -b localhost:5300 juice:app`
|
||||||
|
|
316
auth.py
Normal file
316
auth.py
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
#!/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)
|
|
@ -1,61 +0,0 @@
|
||||||
#!/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
|
|
107
buckler_flask.py
Normal file
107
buckler_flask.py
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
#!/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
Normal file
193
db.py
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
#!/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
120
juice.py
|
@ -7,25 +7,23 @@ import re
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from aiohttp import web
|
from flask import Flask, render_template, request, abort, jsonify
|
||||||
import jinja2
|
from flask import Blueprint
|
||||||
import aiohttp_jinja2
|
|
||||||
from aiohttp_jinja2 import render_template
|
|
||||||
#import uvloop
|
|
||||||
import requests
|
|
||||||
|
|
||||||
|
import auth
|
||||||
import config
|
import config
|
||||||
import models
|
import models
|
||||||
import buckler_aiohttp
|
from auth import auth_required
|
||||||
from tools import make_error
|
from tools import make_error
|
||||||
|
import buckler_flask
|
||||||
|
|
||||||
#uvloop.install()
|
app_views = Blueprint('app_views', __name__)
|
||||||
network = models.init_network()
|
|
||||||
routes = web.RouteTableDef()
|
|
||||||
|
|
||||||
@routes.get('/', name='index')
|
@app_views.route('/')
|
||||||
async def index(request):
|
def index():
|
||||||
"""The index page."""
|
"""
|
||||||
|
The index page.
|
||||||
|
"""
|
||||||
global network
|
global network
|
||||||
init_state = {}
|
init_state = {}
|
||||||
for device in network:
|
for device in network:
|
||||||
|
@ -33,16 +31,17 @@ async def index(request):
|
||||||
for sub_dev in device.sub_devices:
|
for sub_dev in device.sub_devices:
|
||||||
device_state[sub_dev.id] = sub_dev.state
|
device_state[sub_dev.id] = sub_dev.state
|
||||||
init_state[device.id] = device_state
|
init_state[device.id] = device_state
|
||||||
d = {'network': network, 'init_state': init_state}
|
return render_template('index.html', network=network,init_state=init_state)
|
||||||
return render_template('index.html', request, d)
|
|
||||||
|
|
||||||
|
|
||||||
@routes.get('/toggle', name='toggle')
|
@app_views.route('/toggle')
|
||||||
async def toggle(request):
|
def toggle():
|
||||||
"""Toggles the state of a RelayDevice."""
|
"""
|
||||||
|
Toggles the state of a RelayDevice.
|
||||||
|
"""
|
||||||
global network
|
global network
|
||||||
device_id = request.query.get('device_id')
|
device_id = request.args.get('device_id')
|
||||||
sub_dev_id = request.query.get('sub_dev_id')
|
sub_dev_id = request.args.get('sub_dev_id')
|
||||||
|
|
||||||
for device in network:
|
for device in network:
|
||||||
if device.id == device_id:
|
if device.id == device_id:
|
||||||
|
@ -53,16 +52,18 @@ async def toggle(request):
|
||||||
if not res:
|
if not res:
|
||||||
return make_error(404, "sub_dev_id not found")
|
return make_error(404, "sub_dev_id not found")
|
||||||
models.save_network(network)
|
models.save_network(network)
|
||||||
return web.json_response(res)
|
return res
|
||||||
|
|
||||||
@routes.get('/edit', name='edit')
|
@app_views.route('/edit')
|
||||||
async def edit(request):
|
def edit():
|
||||||
"""Edits the text of a particular field."""
|
"""
|
||||||
|
Edits the text of a particular field.
|
||||||
|
"""
|
||||||
global network
|
global network
|
||||||
device_id = request.query.get('device_id')
|
device_id = request.args.get('device_id')
|
||||||
sub_dev_id = request.query.get('sub_dev_id')
|
sub_dev_id = request.args.get('sub_dev_id')
|
||||||
field = request.query.get('field')
|
field = request.args.get('field')
|
||||||
value = request.query.get('value')
|
value = request.args.get('value')
|
||||||
|
|
||||||
for device in network:
|
for device in network:
|
||||||
if device.id == device_id:
|
if device.id == device_id:
|
||||||
|
@ -95,17 +96,17 @@ async def edit(request):
|
||||||
'value': value
|
'value': value
|
||||||
}
|
}
|
||||||
models.save_network(network)
|
models.save_network(network)
|
||||||
return web.json_response(data)
|
return json.dumps(data)
|
||||||
|
|
||||||
|
|
||||||
@routes.get('/new_device', name='new_device')
|
@app_views.route('/new_device')
|
||||||
async def new_device(request):
|
def new_device():
|
||||||
"""
|
"""
|
||||||
Allows adding a new device. Accepts device_type parameter, returns
|
Allows adding a new device. Accepts device_type parameter, returns
|
||||||
the device_id.
|
the device_id.
|
||||||
"""
|
"""
|
||||||
global network
|
global network
|
||||||
device_type = request.query.get('device_type')
|
device_type = request.args.get('device_type')
|
||||||
|
|
||||||
if device_type == 'RelayDevice':
|
if device_type == 'RelayDevice':
|
||||||
device = models.RelayDevice()
|
device = models.RelayDevice()
|
||||||
|
@ -126,15 +127,17 @@ async def new_device(request):
|
||||||
network.append(device)
|
network.append(device)
|
||||||
models.save_network(network)
|
models.save_network(network)
|
||||||
data = {'device_id': device.id}
|
data = {'device_id': device.id}
|
||||||
return web.json_response(data)
|
return json.dumps(data)
|
||||||
|
|
||||||
|
|
||||||
@routes.get('/lock_device', name='lock_device')
|
@app_views.route('/lock_device')
|
||||||
async def lock_device(request):
|
def lock_device():
|
||||||
"""Locks or unlocks a device to prevent or allow editing it's fields."""
|
"""
|
||||||
|
Locks or unlocks a device to prevent or allow editing it's fields.
|
||||||
|
"""
|
||||||
global network
|
global network
|
||||||
device_id = request.query.get('device_id')
|
device_id = request.args.get('device_id')
|
||||||
locked = request.query.get('locked') == 'true'
|
locked = request.args.get('locked') == 'true'
|
||||||
|
|
||||||
for device in network:
|
for device in network:
|
||||||
if device.id == device_id:
|
if device.id == device_id:
|
||||||
|
@ -148,14 +151,16 @@ async def lock_device(request):
|
||||||
device.locked = False
|
device.locked = False
|
||||||
models.save_network(network)
|
models.save_network(network)
|
||||||
|
|
||||||
return web.json_response({'device_id': device.id, 'locked': device.locked})
|
return jsonify(device_id=device.id, locked=device.locked)
|
||||||
|
|
||||||
|
|
||||||
@routes.get('/delete', name='delete')
|
@app_views.route('/delete')
|
||||||
async def delete(request):
|
def delete():
|
||||||
"""Deletes a device."""
|
"""
|
||||||
|
Deletes a device.
|
||||||
|
"""
|
||||||
global network
|
global network
|
||||||
device_id = request.query.get('device_id')
|
device_id = request.args.get('device_id')
|
||||||
|
|
||||||
for device in network:
|
for device in network:
|
||||||
if device.id == device_id:
|
if device.id == device_id:
|
||||||
|
@ -166,18 +171,25 @@ async def delete(request):
|
||||||
network.remove(device)
|
network.remove(device)
|
||||||
models.save_network(network)
|
models.save_network(network)
|
||||||
|
|
||||||
return web.json_response(True)
|
return jsonify(True)
|
||||||
|
|
||||||
|
|
||||||
async def init_app():
|
app = Flask(__name__)
|
||||||
"""Initializes the application."""
|
app.session_interface = buckler_flask.BucklerSessionInterface()
|
||||||
app = web.Application(middlewares=[buckler_aiohttp.buckler_session])
|
app.before_request(buckler_flask.require_auth)
|
||||||
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates'))
|
app.register_blueprint(app_views, url_prefix=config.url_prefix)
|
||||||
#app.on_startup.append(start_background_tasks)
|
#app.register_blueprint(auth.auth_views, url_prefix=config.url_prefix)
|
||||||
#app.on_cleanup.append(cleanup_background_tasks)
|
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()
|
||||||
|
|
||||||
app.router.add_routes(routes)
|
|
||||||
|
|
||||||
app_wrap = web.Application()
|
if __name__ == '__main__':
|
||||||
app_wrap.add_subapp(config.url_prefix, app)
|
app.run(host='0.0.0.0', port=5300, debug=True)
|
||||||
return app_wrap
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ class RelayDevice:
|
||||||
).groups()[0] == 'High'
|
).groups()[0] == 'High'
|
||||||
sub_dev.state = state
|
sub_dev.state = state
|
||||||
|
|
||||||
return {'ok': True, sub_dev_id: state}
|
return json.dumps({'ok': True, sub_dev_id: state})
|
||||||
|
|
||||||
def from_dict(self, data):
|
def from_dict(self, data):
|
||||||
"""
|
"""
|
||||||
|
|
406
static/cbor.js
Normal file
406
static/cbor.js
Normal file
|
@ -0,0 +1,406 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
60
static/juice-auth.js
Normal file
60
static/juice-auth.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
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 + '/';
|
||||||
|
});
|
||||||
|
}
|
|
@ -22,7 +22,9 @@
|
||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
<span class="font-awesome" title="Home"><a href="./"></a></span>
|
<span class="font-awesome" title="Home"><a href="./"></a></span>
|
||||||
|
<span class="font-awesome" title="Manage security keys and tokens"><a href="./manage"></a></span>
|
||||||
<span class="font-awesome" title="Add new device" onclick="new_device()"></span>
|
<span class="font-awesome" title="Add new device" onclick="new_device()"></span>
|
||||||
|
<span class="font-awesome" title="Logout"><a href="./logout"></a></span>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
|
24
templates/login.html
Normal file
24
templates/login.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!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>
|
68
templates/manage.html
Normal file
68
templates/manage.html
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<!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 }})"></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'"></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 }})"></span></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
34
templates/register.html
Normal file
34
templates/register.html
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<!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>
|
25
templates/register_key.html
Normal file
25
templates/register_key.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<!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>
|
7
tools.py
7
tools.py
|
@ -2,14 +2,13 @@
|
||||||
"""
|
"""
|
||||||
Miscellaneous tools and helper functions.
|
Miscellaneous tools and helper functions.
|
||||||
"""
|
"""
|
||||||
from aiohttp import web
|
from flask import jsonify
|
||||||
|
|
||||||
|
|
||||||
def make_error(code, message):
|
def make_error(code, message):
|
||||||
"""
|
"""
|
||||||
Returns a JSON error.
|
Returns a JSON error.
|
||||||
"""
|
"""
|
||||||
d = {'ok': False, 'status': code, 'message': message}
|
res = jsonify(ok=False, status=code, message=message)
|
||||||
res = web.json_response(d)
|
res.status_code = code
|
||||||
res.set_status(code)
|
|
||||||
return res
|
return res
|
||||||
|
|
Loading…
Reference in New Issue
Block a user