diff --git a/docker/Dockerfile b/docker/Dockerfile index a5edeeec00..9a6ebb1564 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,6 +4,9 @@ FROM ${BUILD_FROM} COPY . . RUN pip2 install --no-cache-dir -e . +ENV USERNAME="" +ENV PASSWORD="" + WORKDIR /config ENTRYPOINT ["esphome"] CMD ["/config", "dashboard"] diff --git a/esphome/__main__.py b/esphome/__main__.py index bb9f789600..9c0b7a33d9 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -477,7 +477,11 @@ def parse_args(argv): help="Create a simple web server for a dashboard.") dashboard.add_argument("--port", help="The HTTP port to open connections on. Defaults to 6052.", type=int, default=6052) - dashboard.add_argument("--password", help="The optional password to require for all requests.", + dashboard.add_argument("--username", help="The optional username to require " + "for authentication.", + type=str, default='') + dashboard.add_argument("--password", help="The optional password to require " + "for authentication.", type=str, default='') dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.", action='store_true') diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 5382ef855c..cc89fbd881 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -46,19 +46,22 @@ class DashboardSettings(object): def __init__(self): self.config_dir = '' self.password_digest = '' + self.username = '' self.using_password = False self.on_hassio = False self.cookie_secret = None def parse_args(self, args): self.on_hassio = args.hassio + password = args.password or os.getenv('PASSWORD', '') if not self.on_hassio: - self.using_password = bool(args.password) + self.username = args.username or os.getenv('USERNAME', '') + self.using_password = bool(password) if self.using_password: if IS_PY2: - self.password_digest = hmac.new(args.password).digest() + self.password_digest = hmac.new(password).digest() else: - self.password_digest = hmac.new(args.password.encode()).digest() + self.password_digest = hmac.new(password.encode()).digest() self.config_dir = args.configuration[0] @property @@ -79,7 +82,7 @@ class DashboardSettings(object): def using_auth(self): return self.using_password or self.using_hassio_auth - def check_password(self, password): + def check_password(self, username, password): if not self.using_auth: return True @@ -87,7 +90,7 @@ class DashboardSettings(object): password = hmac.new(password).digest() else: password = hmac.new(password.encode()).digest() - return hmac.compare_digest(self.password_digest, password) + return username == self.username and hmac.compare_digest(self.password_digest, password) def rel_path(self, *args): return os.path.join(self.config_dir, *args) @@ -585,16 +588,14 @@ PING_REQUEST = threading.Event() class LoginHandler(BaseHandler): def get(self): - if settings.using_hassio_auth: - self.render_hassio_login() - return - self.write('
' - 'Password: ' - '' - '
') + if is_authenticated(self): + self.redirect('/') + else: + self.render_login_page() - def render_hassio_login(self, error=None): - self.render("templates/login.html", error=error, **template_args()) + def render_login_page(self, error=None): + self.render("templates/login.html", error=error, hassio=settings.using_hassio_auth, + has_username=bool(settings.username), **template_args()) def post_hassio_login(self): import requests @@ -615,20 +616,34 @@ class LoginHandler(BaseHandler): except Exception as err: # pylint: disable=broad-except _LOGGER.warning("Error during Hass.io auth request: %s", err) self.set_status(500) - self.render_hassio_login(error="Internal server error") + self.render_login_page(error="Internal server error") return self.set_status(401) - self.render_hassio_login(error="Invalid username or password") + self.render_login_page(error="Invalid username or password") + + def post_native_login(self): + username = str(self.get_argument("username", '').encode('utf-8')) + password = str(self.get_argument("password", '').encode('utf-8')) + if settings.check_password(username, password): + self.set_secure_cookie("authenticated", cookie_authenticated_yes) + self.redirect("/") + return + error_str = "Invalid username or password" if settings.username else "Invalid password" + self.set_status(401) + self.render_login_page(error=error_str) def post(self): if settings.using_hassio_auth: self.post_hassio_login() - return + else: + self.post_native_login() - password = str(self.get_argument("password", '')) - if settings.check_password(password): - self.set_secure_cookie("authenticated", cookie_authenticated_yes) - self.redirect("/") + +class LogoutHandler(BaseHandler): + @authenticated + def get(self): + self.clear_cookie("authenticated") + self.redirect('./login') _STATIC_FILE_HASHES = {} @@ -681,6 +696,7 @@ def make_app(debug=False): app = tornado.web.Application([ (rel + "", MainRequestHandler), (rel + "login", LoginHandler), + (rel + "logout", LogoutHandler), (rel + "logs", EsphomeLogsHandler), (rel + "upload", EsphomeUploadHandler), (rel + "compile", EsphomeCompileHandler), diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index 077cc7a3ba..1539632e78 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -38,8 +38,9 @@ diff --git a/esphome/dashboard/templates/login.html b/esphome/dashboard/templates/login.html index d7b73fd0bb..414617c17f 100644 --- a/esphome/dashboard/templates/login.html +++ b/esphome/dashboard/templates/login.html @@ -31,19 +31,23 @@
Enter credentials -

- Please login using your Home Assistant credentials. -

+ {% if hassio %} +

+ Please login using your Home Assistant credentials. +

+ {% end %} {% if error is not None %}

{{ escape(error) }}

{% end %}
-
- - -
+ {% if has_username or hassio %} +
+ + +
+ {% end %}