mirror of
https://github.com/esphome/esphome.git
synced 2025-02-13 00:18:21 +00:00
Login page
This commit is contained in:
parent
05926f634f
commit
714704f95f
@ -1,6 +1,7 @@
|
||||
# pylint: disable=wrong-import-position
|
||||
from __future__ import print_function
|
||||
|
||||
import binascii
|
||||
import collections
|
||||
import hmac
|
||||
import json
|
||||
@ -10,7 +11,6 @@ import os
|
||||
import random
|
||||
import subprocess
|
||||
import threading
|
||||
import urllib2
|
||||
|
||||
import tornado
|
||||
import tornado.concurrent
|
||||
@ -33,13 +33,23 @@ from typing import Optional # noqa
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
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
|
||||
class BaseHandler(tornado.web.RequestHandler):
|
||||
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
|
||||
@ -50,7 +60,10 @@ class EsphomeyamlCommandWebSocket(tornado.websocket.WebSocketHandler):
|
||||
self.closed = False
|
||||
|
||||
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
|
||||
if self.proc is not None:
|
||||
return
|
||||
@ -346,15 +359,53 @@ PING_REQUEST = threading.Event()
|
||||
|
||||
class LoginHandler(BaseHandler):
|
||||
def get(self):
|
||||
if USING_HASSIO_AUTH:
|
||||
self.render_hassio_login()
|
||||
return
|
||||
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 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):
|
||||
if USING_HASSIO_AUTH:
|
||||
self.post_hassio_login()
|
||||
return
|
||||
|
||||
password = str(self.get_argument("password", ''))
|
||||
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.redirect("/")
|
||||
|
||||
@ -394,23 +445,19 @@ def make_app(debug=False):
|
||||
(r"/ping", PingRequestHandler),
|
||||
(r"/wizard.html", WizardRequestHandler),
|
||||
(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
|
||||
|
||||
|
||||
HASSIO_MQTT_CONFIG = None
|
||||
|
||||
|
||||
def _get_mqtt_config_impl():
|
||||
token = os.getenv('HASSIO_TOKEN')
|
||||
if token is None:
|
||||
raise ValueError
|
||||
import requests
|
||||
|
||||
req = urllib2.Request('http://hassio/services/mqtt')
|
||||
req.add_header('X-HASSIO-KEY', token)
|
||||
resp = urllib2.urlopen(req)
|
||||
content = resp.read()
|
||||
mqtt_config = json.loads(content)
|
||||
headers = {
|
||||
'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'),
|
||||
}
|
||||
|
||||
req = requests.get('http://hassio/services/mqtt', headers=headers)
|
||||
mqtt_config = req.json()
|
||||
return {
|
||||
'addon': mqtt_config['addon'],
|
||||
'host': mqtt_config['host'],
|
||||
@ -422,7 +469,7 @@ def _get_mqtt_config_impl():
|
||||
def get_mqtt_config_lazy():
|
||||
global HASSIO_MQTT_CONFIG
|
||||
|
||||
if HASSIO_MQTT_CONFIG is None:
|
||||
if not ON_HASSIO or HASSIO_MQTT_CONFIG is None:
|
||||
return None
|
||||
|
||||
if not HASSIO_MQTT_CONFIG:
|
||||
@ -436,26 +483,32 @@ def get_mqtt_config_lazy():
|
||||
|
||||
def start_web_server(args):
|
||||
global CONFIG_DIR
|
||||
global PASSWORD
|
||||
global HASSIO_MQTT_CONFIG
|
||||
global PASSWORD_DIGEST
|
||||
global USING_PASSWORD
|
||||
global ON_HASSIO
|
||||
global USING_HASSIO_AUTH
|
||||
global COOKIE_SECRET
|
||||
|
||||
CONFIG_DIR = args.configuration
|
||||
if not os.path.exists(CONFIG_DIR):
|
||||
os.makedirs(CONFIG_DIR)
|
||||
|
||||
# HassIO options storage
|
||||
PASSWORD = args.password
|
||||
|
||||
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:
|
||||
PASSWORD = hmac.new(str(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.
|
||||
if USING_HASSIO_AUTH or USING_PASSWORD:
|
||||
cookie_secret_path = os.path.join(CONFIG_DIR, '.esphomeyaml', '.cookie_secret')
|
||||
if os.path.exists(cookie_secret_path):
|
||||
with open(cookie_secret_path, 'r') as f:
|
||||
COOKIE_SECRET = f.read()
|
||||
else:
|
||||
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...",
|
||||
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