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:
parent
4ebdda80f9
commit
2647bd3881
2 changed files with 62 additions and 12 deletions
|
@ -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
|
||||||
|
|
|
@ -30,6 +30,7 @@ class EventProcessor(object):
|
||||||
if 'http' not in backends: return
|
if 'http' not in backends: return
|
||||||
|
|
||||||
backend = get_backend('http')
|
backend = get_backend('http')
|
||||||
|
if backend:
|
||||||
backend.notify_web_clients(event)
|
backend.notify_web_clients(event)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue