mirror of
https://github.com/esphome/esphome.git
synced 2025-02-13 08:28:19 +00:00
Login page
This commit is contained in:
parent
05926f634f
commit
714704f95f
@ -1,6 +1,7 @@
|
|||||||
# pylint: disable=wrong-import-position
|
# pylint: disable=wrong-import-position
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import binascii
|
||||||
import collections
|
import collections
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
@ -10,7 +11,6 @@ import os
|
|||||||
import random
|
import random
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import urllib2
|
|
||||||
|
|
||||||
import tornado
|
import tornado
|
||||||
import tornado.concurrent
|
import tornado.concurrent
|
||||||
@ -33,13 +33,23 @@ from typing import Optional # noqa
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
CONFIG_DIR = ''
|
CONFIG_DIR = ''
|
||||||
PASSWORD = ''
|
PASSWORD_DIGEST = ''
|
||||||
|
COOKIE_SECRET = None
|
||||||
|
USING_PASSWORD = False
|
||||||
|
ON_HASSIO = False
|
||||||
|
USING_HASSIO_AUTH = True
|
||||||
|
HASSIO_MQTT_CONFIG = {}
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
# pylint: disable=abstract-method
|
||||||
class BaseHandler(tornado.web.RequestHandler):
|
class BaseHandler(tornado.web.RequestHandler):
|
||||||
def is_authenticated(self):
|
def is_authenticated(self):
|
||||||
return not PASSWORD or self.get_secure_cookie('authenticated') == 'yes'
|
has_cookie = self.get_secure_cookie('authenticated') == 'yes'
|
||||||
|
|
||||||
|
if ON_HASSIO:
|
||||||
|
return not USING_HASSIO_AUTH or has_cookie
|
||||||
|
|
||||||
|
return not USING_PASSWORD or has_cookie
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method, arguments-differ
|
# pylint: disable=abstract-method, arguments-differ
|
||||||
@ -50,7 +60,10 @@ 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':
|
has_cookie = self.get_secure_cookie('authenticated') == 'yes'
|
||||||
|
if USING_PASSWORD and not has_cookie:
|
||||||
|
return
|
||||||
|
if ON_HASSIO and (USING_HASSIO_AUTH and not has_cookie):
|
||||||
return
|
return
|
||||||
if self.proc is not None:
|
if self.proc is not None:
|
||||||
return
|
return
|
||||||
@ -346,15 +359,53 @@ PING_REQUEST = threading.Event()
|
|||||||
|
|
||||||
class LoginHandler(BaseHandler):
|
class LoginHandler(BaseHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if USING_HASSIO_AUTH:
|
||||||
|
self.render_hassio_login()
|
||||||
|
return
|
||||||
self.write('<html><body><form action="/login" method="post">'
|
self.write('<html><body><form action="/login" method="post">'
|
||||||
'Password: <input type="password" name="password">'
|
'Password: <input type="password" name="password">'
|
||||||
'<input type="submit" value="Sign in">'
|
'<input type="submit" value="Sign in">'
|
||||||
'</form></body></html>')
|
'</form></body></html>')
|
||||||
|
|
||||||
|
def render_hassio_login(self, error=None):
|
||||||
|
version = const.__version__
|
||||||
|
docs_link = 'https://beta.esphomelib.com/esphomeyaml/' if 'b' in version else \
|
||||||
|
'https://esphomelib.com/esphomeyaml/'
|
||||||
|
|
||||||
|
self.render("templates/login.html", version=version, docs_link=docs_link, error=error)
|
||||||
|
|
||||||
|
def post_hassio_login(self):
|
||||||
|
import requests
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'),
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
'username': str(self.get_argument('username', '')),
|
||||||
|
'password': str(self.get_argument('password', ''))
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
req = requests.post('http://hassio/auth', headers=headers, data=data)
|
||||||
|
if req.status_code == 200:
|
||||||
|
self.set_secure_cookie("authenticated", "yes")
|
||||||
|
self.redirect('/')
|
||||||
|
return
|
||||||
|
except Exception as err: # pylint: disable=broad-except
|
||||||
|
_LOGGER.warn("Error during HassIO auth request: %s", err)
|
||||||
|
self.set_status(500)
|
||||||
|
self.render_hassio_login(error="Internal server error")
|
||||||
|
return
|
||||||
|
self.set_status(401)
|
||||||
|
self.render_hassio_login(error="Invalid username or password")
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if USING_HASSIO_AUTH:
|
||||||
|
self.post_hassio_login()
|
||||||
|
return
|
||||||
|
|
||||||
password = str(self.get_argument("password", ''))
|
password = str(self.get_argument("password", ''))
|
||||||
password = hmac.new(password).digest()
|
password = hmac.new(password).digest()
|
||||||
if hmac.compare_digest(PASSWORD, password):
|
if hmac.compare_digest(PASSWORD_DIGEST, password):
|
||||||
self.set_secure_cookie("authenticated", "yes")
|
self.set_secure_cookie("authenticated", "yes")
|
||||||
self.redirect("/")
|
self.redirect("/")
|
||||||
|
|
||||||
@ -394,23 +445,19 @@ def make_app(debug=False):
|
|||||||
(r"/ping", PingRequestHandler),
|
(r"/ping", PingRequestHandler),
|
||||||
(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, cookie_secret=PASSWORD, log_function=log_function)
|
], debug=debug, cookie_secret=COOKIE_SECRET, log_function=log_function)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
HASSIO_MQTT_CONFIG = None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_mqtt_config_impl():
|
def _get_mqtt_config_impl():
|
||||||
token = os.getenv('HASSIO_TOKEN')
|
import requests
|
||||||
if token is None:
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
req = urllib2.Request('http://hassio/services/mqtt')
|
headers = {
|
||||||
req.add_header('X-HASSIO-KEY', token)
|
'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'),
|
||||||
resp = urllib2.urlopen(req)
|
}
|
||||||
content = resp.read()
|
|
||||||
mqtt_config = json.loads(content)
|
req = requests.get('http://hassio/services/mqtt', headers=headers)
|
||||||
|
mqtt_config = req.json()
|
||||||
return {
|
return {
|
||||||
'addon': mqtt_config['addon'],
|
'addon': mqtt_config['addon'],
|
||||||
'host': mqtt_config['host'],
|
'host': mqtt_config['host'],
|
||||||
@ -422,7 +469,7 @@ def _get_mqtt_config_impl():
|
|||||||
def get_mqtt_config_lazy():
|
def get_mqtt_config_lazy():
|
||||||
global HASSIO_MQTT_CONFIG
|
global HASSIO_MQTT_CONFIG
|
||||||
|
|
||||||
if HASSIO_MQTT_CONFIG is None:
|
if not ON_HASSIO or HASSIO_MQTT_CONFIG is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not HASSIO_MQTT_CONFIG:
|
if not HASSIO_MQTT_CONFIG:
|
||||||
@ -436,26 +483,32 @@ def get_mqtt_config_lazy():
|
|||||||
|
|
||||||
def start_web_server(args):
|
def start_web_server(args):
|
||||||
global CONFIG_DIR
|
global CONFIG_DIR
|
||||||
global PASSWORD
|
global PASSWORD_DIGEST
|
||||||
global HASSIO_MQTT_CONFIG
|
global USING_PASSWORD
|
||||||
|
global ON_HASSIO
|
||||||
|
global USING_HASSIO_AUTH
|
||||||
|
global COOKIE_SECRET
|
||||||
|
|
||||||
CONFIG_DIR = args.configuration
|
CONFIG_DIR = args.configuration
|
||||||
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 args.hassio:
|
if args.hassio:
|
||||||
HASSIO_MQTT_CONFIG = False
|
ON_HASSIO = True
|
||||||
|
USING_HASSIO_AUTH = not bool(os.getenv('DISABLE_HA_AUTHENTICATION'))
|
||||||
|
elif args.password:
|
||||||
|
USING_PASSWORD = True
|
||||||
|
PASSWORD_DIGEST = hmac.new(args.password).digest()
|
||||||
|
|
||||||
if PASSWORD:
|
if USING_HASSIO_AUTH or USING_PASSWORD:
|
||||||
PASSWORD = hmac.new(str(PASSWORD)).digest()
|
cookie_secret_path = os.path.join(CONFIG_DIR, '.esphomeyaml', '.cookie_secret')
|
||||||
# Use the digest of the password as our cookie secret. This makes sure the cookie
|
if os.path.exists(cookie_secret_path):
|
||||||
# isn't too short. It, of course, enables local hash brute forcing (because the cookie
|
with open(cookie_secret_path, 'r') as f:
|
||||||
# secret can be brute forced without making requests). But the hashing algorithm used
|
COOKIE_SECRET = f.read()
|
||||||
# by tornado is apparently strong enough to make brute forcing even a short string pretty
|
else:
|
||||||
# hard.
|
COOKIE_SECRET = binascii.hexlify(os.urandom(64))
|
||||||
|
with open(cookie_secret_path, 'w') as f:
|
||||||
|
f.write(COOKIE_SECRET)
|
||||||
|
|
||||||
_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)
|
||||||
|
125
esphomeyaml/dashboard/templates/login.html
Normal file
125
esphomeyaml/dashboard/templates/login.html
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>esphomeyaml Dashboard</title>
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/css/materialize.min.css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/materialize-stepper.min.css">
|
||||||
|
|
||||||
|
<!-- jQuery :( -->
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
|
||||||
|
<script src="https://code.jquery.com/ui/1.8.5/jquery-ui.min.js" integrity="sha256-fOse6WapxTrUSJOJICXXYwHRJOPa6C1OUQXi7C9Ddy8=" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
|
||||||
|
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.15.0/jquery.validate.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="/static/materialize-stepper.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
nav .brand-logo {
|
||||||
|
margin-left: 48px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .container {
|
||||||
|
margin-top: -12vh;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ribbon {
|
||||||
|
width: 100%;
|
||||||
|
height: 17vh;
|
||||||
|
background-color: #3F51B5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.very-large {
|
||||||
|
font-size: 8rem;
|
||||||
|
padding-top: 2px;
|
||||||
|
color: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-content {
|
||||||
|
padding-left: 18px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-footer {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<div class="nav-wrapper indigo">
|
||||||
|
<a href="#" class="brand-logo left">esphomeyaml Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="ribbon"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col card s10 offset-s1 m10 offset-m1 l8 offset-l2">
|
||||||
|
<form action="/login" method="post">
|
||||||
|
<div class="card-content">
|
||||||
|
<span class="card-title">Enter credentials</span>
|
||||||
|
<p>
|
||||||
|
Please login using your Home Assistant credentials.
|
||||||
|
</p>
|
||||||
|
{% if error is not None %}
|
||||||
|
<p>
|
||||||
|
{{ escape(error) }}
|
||||||
|
</p>
|
||||||
|
{% end %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" class="validate" name="username" id="username" />
|
||||||
|
</div>
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" class="validate" name="password" id="password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-action right-align">
|
||||||
|
<button class="btn indigo waves-effect waves-light" type="submit" name="action">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="page-footer indigo darken-1">
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="footer-copyright">
|
||||||
|
<div class="container">
|
||||||
|
© 2018 Copyright Otto Winter, Made with <a class="grey-text text-lighten-4" href="https://materializecss.com/" target="_blank">Materialize</a>
|
||||||
|
<a class="grey-text text-lighten-4 right" href="{{ docs_link }}" target="_blank">esphomeyaml {{ version }} Documentation</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user