mirror of
https://github.com/esphome/esphome.git
synced 2024-11-28 09:44:12 +01:00
Dashboard authentication
This commit is contained in:
parent
135117714b
commit
ec3366cce0
5 changed files with 75 additions and 8 deletions
|
@ -21,9 +21,13 @@
|
||||||
"map": [
|
"map": [
|
||||||
"config:rw"
|
"config:rw"
|
||||||
],
|
],
|
||||||
"options": {},
|
"options": {
|
||||||
|
"password": ""
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"password": "str?"
|
||||||
|
},
|
||||||
"environment": {
|
"environment": {
|
||||||
"ESPHOMEYAML_OTA_HOST_PORT": "6053"
|
"ESPHOMEYAML_OTA_HOST_PORT": "6053"
|
||||||
},
|
},
|
||||||
"schema": {}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -436,6 +436,8 @@ def parse_args(argv):
|
||||||
help="Create a simple webserver for a dashboard.")
|
help="Create a simple webserver for a dashboard.")
|
||||||
dashboard.add_argument("--port", help="The HTTP port to open connections on.", type=int,
|
dashboard.add_argument("--port", help="The HTTP port to open connections on.", type=int,
|
||||||
default=6052)
|
default=6052)
|
||||||
|
dashboard.add_argument("--password", help="The optional password to require for all requests.",
|
||||||
|
type=str, default='')
|
||||||
|
|
||||||
return parser.parse_args(argv[1:])
|
return parser.parse_args(argv[1:])
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"6052/tcp": 6052,
|
"6052/tcp": 6052,
|
||||||
"6053/tcp": 6053
|
"6053/tcp": 6053
|
||||||
},
|
},
|
||||||
|
"arch": [
|
||||||
|
"amd64",
|
||||||
|
"armhf",
|
||||||
|
"i386"
|
||||||
|
],
|
||||||
"auto_uart": true,
|
"auto_uart": true,
|
||||||
"map": [
|
"map": [
|
||||||
"config:rw"
|
"config:rw"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import codecs
|
import codecs
|
||||||
|
import hmac
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -27,6 +28,13 @@ except ImportError as err:
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
CONFIG_DIR = ''
|
CONFIG_DIR = ''
|
||||||
|
PASSWORD = ''
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
class BaseHandler(tornado.web.RequestHandler):
|
||||||
|
def is_authenticated(self):
|
||||||
|
return not PASSWORD or self.get_secure_cookie('authenticated') == 'yes'
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method, arguments-differ
|
# pylint: disable=abstract-method, arguments-differ
|
||||||
|
@ -37,6 +45,8 @@ class EsphomeyamlCommandWebSocket(tornado.websocket.WebSocketHandler):
|
||||||
self.closed = False
|
self.closed = False
|
||||||
|
|
||||||
def on_message(self, message):
|
def on_message(self, message):
|
||||||
|
if PASSWORD and self.get_secure_cookie('authenticated') != 'yes':
|
||||||
|
return
|
||||||
if self.proc is not None:
|
if self.proc is not None:
|
||||||
return
|
return
|
||||||
command = self.build_command(message)
|
command = self.build_command(message)
|
||||||
|
@ -103,8 +113,11 @@ class EsphomeyamlValidateHandler(EsphomeyamlCommandWebSocket):
|
||||||
return ["esphomeyaml", config_file, "config"]
|
return ["esphomeyaml", config_file, "config"]
|
||||||
|
|
||||||
|
|
||||||
class SerialPortRequestHandler(tornado.web.RequestHandler):
|
class SerialPortRequestHandler(BaseHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not self.is_authenticated():
|
||||||
|
self.redirect('/login')
|
||||||
|
return
|
||||||
ports = get_serial_ports()
|
ports = get_serial_ports()
|
||||||
data = []
|
data = []
|
||||||
for port, desc in ports:
|
for port, desc in ports:
|
||||||
|
@ -119,10 +132,13 @@ class SerialPortRequestHandler(tornado.web.RequestHandler):
|
||||||
self.write(json.dumps(sorted(data, reverse=True)))
|
self.write(json.dumps(sorted(data, reverse=True)))
|
||||||
|
|
||||||
|
|
||||||
class WizardRequestHandler(tornado.web.RequestHandler):
|
class WizardRequestHandler(BaseHandler):
|
||||||
def post(self):
|
def post(self):
|
||||||
from esphomeyaml import wizard
|
from esphomeyaml import wizard
|
||||||
|
|
||||||
|
if not self.is_authenticated():
|
||||||
|
self.redirect('/login')
|
||||||
|
return
|
||||||
kwargs = {k: ''.join(v) for k, v in self.request.arguments.iteritems()}
|
kwargs = {k: ''.join(v) for k, v in self.request.arguments.iteritems()}
|
||||||
config = wizard.wizard_file(**kwargs)
|
config = wizard.wizard_file(**kwargs)
|
||||||
destination = os.path.join(CONFIG_DIR, kwargs['name'] + '.yaml')
|
destination = os.path.join(CONFIG_DIR, kwargs['name'] + '.yaml')
|
||||||
|
@ -132,8 +148,12 @@ class WizardRequestHandler(tornado.web.RequestHandler):
|
||||||
self.redirect('/?begin=True')
|
self.redirect('/?begin=True')
|
||||||
|
|
||||||
|
|
||||||
class DownloadBinaryRequestHandler(tornado.web.RequestHandler):
|
class DownloadBinaryRequestHandler(BaseHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not self.is_authenticated():
|
||||||
|
self.redirect('/login')
|
||||||
|
return
|
||||||
|
|
||||||
configuration = self.get_argument('configuration')
|
configuration = self.get_argument('configuration')
|
||||||
config_file = os.path.join(CONFIG_DIR, configuration)
|
config_file = os.path.join(CONFIG_DIR, configuration)
|
||||||
core.CONFIG_PATH = config_file
|
core.CONFIG_PATH = config_file
|
||||||
|
@ -151,8 +171,12 @@ class DownloadBinaryRequestHandler(tornado.web.RequestHandler):
|
||||||
self.finish()
|
self.finish()
|
||||||
|
|
||||||
|
|
||||||
class MainRequestHandler(tornado.web.RequestHandler):
|
class MainRequestHandler(BaseHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not self.is_authenticated():
|
||||||
|
self.redirect('/login')
|
||||||
|
return
|
||||||
|
|
||||||
begin = bool(self.get_argument('begin', False))
|
begin = bool(self.get_argument('begin', False))
|
||||||
files = sorted([f for f in os.listdir(CONFIG_DIR) if f.endswith('.yaml') and
|
files = sorted([f for f in os.listdir(CONFIG_DIR) if f.endswith('.yaml') and
|
||||||
not f.startswith('.')])
|
not f.startswith('.')])
|
||||||
|
@ -161,10 +185,26 @@ class MainRequestHandler(tornado.web.RequestHandler):
|
||||||
version=const.__version__, begin=begin)
|
version=const.__version__, begin=begin)
|
||||||
|
|
||||||
|
|
||||||
|
class LoginHandler(BaseHandler):
|
||||||
|
def get(self):
|
||||||
|
self.write('<html><body><form action="/login" method="post">'
|
||||||
|
'Password: <input type="password" name="password">'
|
||||||
|
'<input type="submit" value="Sign in">'
|
||||||
|
'</form></body></html>')
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
password = str(self.get_argument("password", ''))
|
||||||
|
password = hmac.new(password).digest()
|
||||||
|
if hmac.compare_digest(PASSWORD, password):
|
||||||
|
self.set_secure_cookie("authenticated", "yes")
|
||||||
|
self.redirect("/")
|
||||||
|
|
||||||
|
|
||||||
def make_app(debug=False):
|
def make_app(debug=False):
|
||||||
static_path = os.path.join(os.path.dirname(__file__), 'static')
|
static_path = os.path.join(os.path.dirname(__file__), 'static')
|
||||||
return tornado.web.Application([
|
return tornado.web.Application([
|
||||||
(r"/", MainRequestHandler),
|
(r"/", MainRequestHandler),
|
||||||
|
(r"/login", LoginHandler),
|
||||||
(r"/logs", EsphomeyamlLogsHandler),
|
(r"/logs", EsphomeyamlLogsHandler),
|
||||||
(r"/run", EsphomeyamlRunHandler),
|
(r"/run", EsphomeyamlRunHandler),
|
||||||
(r"/compile", EsphomeyamlCompileHandler),
|
(r"/compile", EsphomeyamlCompileHandler),
|
||||||
|
@ -173,11 +213,12 @@ def make_app(debug=False):
|
||||||
(r"/serial-ports", SerialPortRequestHandler),
|
(r"/serial-ports", SerialPortRequestHandler),
|
||||||
(r"/wizard.html", WizardRequestHandler),
|
(r"/wizard.html", WizardRequestHandler),
|
||||||
(r'/static/(.*)', tornado.web.StaticFileHandler, {'path': static_path}),
|
(r'/static/(.*)', tornado.web.StaticFileHandler, {'path': static_path}),
|
||||||
], debug=debug)
|
], debug=debug, cookie_secret=PASSWORD)
|
||||||
|
|
||||||
|
|
||||||
def start_web_server(args):
|
def start_web_server(args):
|
||||||
global CONFIG_DIR
|
global CONFIG_DIR
|
||||||
|
global PASSWORD
|
||||||
|
|
||||||
if tornado is None:
|
if tornado is None:
|
||||||
raise ESPHomeYAMLError("Attempted to load dashboard, but tornado is not installed! "
|
raise ESPHomeYAMLError("Attempted to load dashboard, but tornado is not installed! "
|
||||||
|
@ -187,6 +228,21 @@ def start_web_server(args):
|
||||||
if not os.path.exists(CONFIG_DIR):
|
if not os.path.exists(CONFIG_DIR):
|
||||||
os.makedirs(CONFIG_DIR)
|
os.makedirs(CONFIG_DIR)
|
||||||
|
|
||||||
|
# HassIO options storage
|
||||||
|
PASSWORD = args.password
|
||||||
|
if os.path.isfile('/data/options.json'):
|
||||||
|
with open('/data/options.json') as f:
|
||||||
|
js = json.load(f)
|
||||||
|
PASSWORD = js.get('password') or PASSWORD
|
||||||
|
|
||||||
|
if PASSWORD:
|
||||||
|
PASSWORD = hmac.new(PASSWORD).digest()
|
||||||
|
# Use the digest of the password as our cookie secret. This makes sure the cookie
|
||||||
|
# isn't too short. It, of course, enables local hash brute forcing (because the cookie
|
||||||
|
# secret can be brute forced without making requests). But the hashing algorithm used
|
||||||
|
# by tornado is apparently strong enough to make brute forcing even a short string pretty
|
||||||
|
# hard.
|
||||||
|
|
||||||
_LOGGER.info("Starting dashboard web server on port %s and configuration dir %s...",
|
_LOGGER.info("Starting dashboard web server on port %s and configuration dir %s...",
|
||||||
args.port, CONFIG_DIR)
|
args.port, CONFIG_DIR)
|
||||||
app = make_app(args.verbose)
|
app = make_app(args.verbose)
|
||||||
|
|
|
@ -524,7 +524,7 @@
|
||||||
let ports = [];
|
let ports = [];
|
||||||
|
|
||||||
const fetchSerialPorts = (begin=false) => {
|
const fetchSerialPorts = (begin=false) => {
|
||||||
fetch('/serial-ports').then(res => res.json())
|
fetch('/serial-ports', {credentials: "same-origin"}).then(res => res.json())
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (ports.length === response.length) {
|
if (ports.length === response.length) {
|
||||||
let allEqual = true;
|
let allEqual = true;
|
||||||
|
|
Loading…
Reference in a new issue