diff --git a/buckler_flask.py b/buckler_flask.py new file mode 100644 index 0000000..0ed5429 --- /dev/null +++ b/buckler_flask.py @@ -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 diff --git a/config.py.template b/config.py.template index 59afe73..087d5cb 100644 --- a/config.py.template +++ b/config.py.template @@ -4,6 +4,13 @@ Configuration settings for the Juice IOT hub server. `url_prefix` is the root path you wish app to reside at eg. https://example.com/juice. `registration_open` whether or not new accounts may be registered. +`buckler` specifies settings pertaining to the Buckler server. """ url_prefix = '/juice' registration_open = True +buckler = { + 'url': "http://127.0.0.1:5400/buckler", + 'app_id': 2, + 'app_key': """lol""", + 'login_url': "/buckler/login", +} diff --git a/juice.py b/juice.py index f9c91a8..4924f3a 100644 --- a/juice.py +++ b/juice.py @@ -15,11 +15,11 @@ import config import models from auth import auth_required from tools import make_error +import buckler_flask app_views = Blueprint('app_views', __name__) @app_views.route('/') -@auth_required def index(): """ The index page. @@ -35,7 +35,6 @@ def index(): @app_views.route('/toggle') -@auth_required def toggle(): """ Toggles the state of a RelayDevice. @@ -56,7 +55,6 @@ def toggle(): return res @app_views.route('/edit') -@auth_required def edit(): """ Edits the text of a particular field. @@ -102,7 +100,6 @@ def edit(): @app_views.route('/new_device') -@auth_required def new_device(): """ Allows adding a new device. Accepts device_type parameter, returns @@ -112,7 +109,7 @@ def new_device(): device_type = request.args.get('device_type') if device_type == 'RelayDevice': - device = RelayDevice() + device = models.RelayDevice() else: return make_error(400, "Unknown device type") @@ -134,7 +131,6 @@ def new_device(): @app_views.route('/lock_device') -@auth_required def lock_device(): """ Locks or unlocks a device to prevent or allow editing it's fields. @@ -159,7 +155,6 @@ def lock_device(): @app_views.route('/delete') -@auth_required def delete(): """ Deletes a device. @@ -180,8 +175,10 @@ def delete(): 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.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: