Implemented token verification over HTTP calls.

The token can be provided either:

* GET parameter (``?token=abc``)
* JSON payload request (``{..your_request.., "_token":"abc"})
* HTTP header (``X-Token: abc``)
* Basic HTTP auth (any username works, password: token)
This commit is contained in:
Fabio Manganiello 2018-07-08 21:36:58 +02:00
parent 4ebdda80f9
commit 2647bd3881
2 changed files with 62 additions and 12 deletions

View File

@ -9,7 +9,9 @@ import time
from threading import Thread, get_ident from threading import Thread, get_ident
from multiprocessing import Process from multiprocessing import Process
from flask import Flask, abort, jsonify, request as http_request, render_template, send_from_directory from flask import Flask, Response, abort, jsonify, request as http_request, \
render_template, send_from_directory
from redis import Redis from redis import Redis
from platypush.config import Config from platypush.config import Config
@ -41,6 +43,11 @@ class HttpBackend(Backend):
* To display a fullscreen dashboard with your configured widgets, by default available under ``/dashboard`` * To display a fullscreen dashboard with your configured widgets, by default available under ``/dashboard``
Note that if you set up a main token, it will be required for any HTTP
interaction - either as ``X-Token`` HTTP header, on the query string
(attribute name: ``token``), as part of the JSON payload root (attribute
name: ``_token``), or via HTTP basic auth (any username works).
Requires: Requires:
* **flask** (``pip install flask``) * **flask** (``pip install flask``)
@ -53,7 +60,7 @@ class HttpBackend(Backend):
} }
def __init__(self, port=8008, websocket_port=8009, disable_websocket=False, def __init__(self, port=8008, websocket_port=8009, disable_websocket=False,
redis_queue='platypush/http', token=None, dashboard={}, redis_queue='platypush/http', dashboard={},
maps={}, **kwargs): maps={}, **kwargs):
""" """
:param port: Listen port for the web server (default: 8008) :param port: Listen port for the web server (default: 8008)
@ -68,9 +75,6 @@ class HttpBackend(Backend):
:param redis_queue: Name of the Redis queue used to synchronize messages with the web server process (default: ``platypush/http``) :param redis_queue: Name of the Redis queue used to synchronize messages with the web server process (default: ``platypush/http``)
:type redis_queue: str :type redis_queue: str
:param token: If set (recommended) any interaction with the web server needs to bear an ``X-Token: <token>`` header, or it will fail with a 403: Forbidden
:type token: str
:param dashboard: Set it if you want to use the dashboard service. It will contain the configuration for the widgets to be used (look under ``platypush/backend/http/templates/widgets/`` for the available widgets). :param dashboard: Set it if you want to use the dashboard service. It will contain the configuration for the widgets to be used (look under ``platypush/backend/http/templates/widgets/`` for the available widgets).
Example configuration:: Example configuration::
@ -102,7 +106,6 @@ class HttpBackend(Backend):
self.port = port self.port = port
self.websocket_port = websocket_port self.websocket_port = websocket_port
self.redis_queue = redis_queue self.redis_queue = redis_queue
self.token = token
self.dashboard = dashboard self.dashboard = dashboard
self.maps = maps self.maps = maps
self.server_proc = None self.server_proc = None
@ -174,6 +177,39 @@ class HttpBackend(Backend):
self.bus.post(msg) self.bus.post(msg)
@classmethod
def _authenticate(cls):
return Response('Authentication required', 401,
{'WWW-Authenticate': 'Basic realm="Login required"'})
@classmethod
def _authentication_ok(cls):
token = Config.get('token')
if not token:
return True
user_token = None
# Check if
if 'X-Token' in http_request.headers:
user_token = http_request.headers['X-Token']
elif http_request.authorization:
# TODO support for user check
user_token = http_request.authorization.password
elif 'token' in http_request.args:
user_token = http_request.args.get('token')
else:
try:
args = json.loads(http_request.data.decode('utf-8'))
user_token = args.get('_token')
except:
pass
if user_token == token:
return True
return False
def webserver(self): def webserver(self):
""" Web server main process """ """ Web server main process """
basedir = os.path.dirname(inspect.getfile(self.__class__)) basedir = os.path.dirname(inspect.getfile(self.__class__))
@ -187,12 +223,18 @@ class HttpBackend(Backend):
def execute(): def execute():
""" Endpoint to execute commands """ """ Endpoint to execute commands """
args = json.loads(http_request.data.decode('utf-8')) args = json.loads(http_request.data.decode('utf-8'))
token = http_request.headers['X-Token'] if 'X-Token' in http_request.headers else None if not self._authentication_ok(): return self._authenticate()
if token != self.token: abort(401)
if '_token' in args:
del args['_token']
args = json.loads(http_request.data.decode('utf-8'))
msg = Message.build(args) msg = Message.build(args)
self.logger.info('Received message on the HTTP backend: {}'.format(msg)) self.logger.info('Received message on the HTTP backend: {}'.format(msg))
if Config.get('token'):
msg.token = Config.get('token')
if isinstance(msg, Request): if isinstance(msg, Request):
try: try:
response = msg.execute(async=False) response = msg.execute(async=False)
@ -210,6 +252,8 @@ class HttpBackend(Backend):
@app.route('/') @app.route('/')
def index(): def index():
""" Route to the main web panel """ """ Route to the main web panel """
if not self._authentication_ok(): return self._authenticate()
configured_plugins = Config.get_plugins() configured_plugins = Config.get_plugins()
enabled_plugins = {} enabled_plugins = {}
hidden_plugins = {} hidden_plugins = {}
@ -223,7 +267,7 @@ class HttpBackend(Backend):
enabled_plugins[plugin] = conf enabled_plugins[plugin] = conf
return render_template('index.html', plugins=enabled_plugins, hidden_plugins=hidden_plugins, return render_template('index.html', plugins=enabled_plugins, hidden_plugins=hidden_plugins,
token=self.token, websocket_port=self.websocket_port) token=Config.get('token'), websocket_port=self.websocket_port)
@app.route('/widget/<widget>', methods=['POST']) @app.route('/widget/<widget>', methods=['POST'])
@ -243,8 +287,10 @@ class HttpBackend(Backend):
@app.route('/dashboard', methods=['GET']) @app.route('/dashboard', methods=['GET'])
def dashboard(): def dashboard():
""" Route for the fullscreen dashboard """ """ Route for the fullscreen dashboard """
if not self._authentication_ok(): return self._authenticate()
return render_template('dashboard.html', config=self.dashboard, utils=HttpUtils, return render_template('dashboard.html', config=self.dashboard, utils=HttpUtils,
token=self.token, websocket_port=self.websocket_port) token=Config.get('token'), websocket_port=self.websocket_port)
@app.route('/map', methods=['GET']) @app.route('/map', methods=['GET'])
def map(): def map():
@ -303,6 +349,9 @@ class HttpBackend(Backend):
return (now + datetime.timedelta(**params)).isoformat() return (now + datetime.timedelta(**params)).isoformat()
if not self._authentication_ok(): return self._authenticate()
try: try:
api_key = self.maps['api_key'] api_key = self.maps['api_key']
except KeyError: except KeyError:
@ -314,7 +363,7 @@ class HttpBackend(Backend):
return render_template('map.html', config=self.maps, return render_template('map.html', config=self.maps,
utils=HttpUtils, start=start, end=end, utils=HttpUtils, start=start, end=end,
zoom=zoom, token=self.token, api_key=api_key, zoom=zoom, token=Config.get('token'), api_key=api_key,
websocket_port=self.websocket_port) websocket_port=self.websocket_port)
return app return app

View File

@ -30,7 +30,8 @@ class EventProcessor(object):
if 'http' not in backends: return if 'http' not in backends: return
backend = get_backend('http') backend = get_backend('http')
backend.notify_web_clients(event) if backend:
backend.notify_web_clients(event)
def process_event(self, event): def process_event(self, event):