From ed05ff6fd93938a77f85a3008b78c28cf1c0e1ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 17 Jan 2015 21:55:33 -0800 Subject: [PATCH] Allow for running Home Assistant without password --- README.md | 4 ++- homeassistant/__main__.py | 5 ++- homeassistant/components/http/__init__.py | 39 +++++++++++++++-------- homeassistant/remote.py | 10 ++---- homeassistant/util.py | 19 ++++++++--- tests/test_component_http.py | 5 --- tests/test_remote.py | 11 ++++--- 7 files changed, 54 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 518ea881f31..1535c3a169e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ pip3 install -r requirements.txt python3 -m homeassistant --open-ui ``` -This will start the Home Assistant server and create an initial configuration file in `config/home-assistant.conf` that is setup for demo mode. It will launch its web interface on [http://127.0.0.1:8123](http://127.0.0.1:8123). The default password is 'password'. +This will start the Home Assistant server and launch its webinterface. By default Home Assistant looks for the configuration file `config/home-assistant.conf`. A standard configuration file will be written if none exists. If you're using Docker, you can use @@ -53,6 +53,8 @@ If you're using Docker, you can use docker run -d --name="home-assistant" -v /path/to/homeassistant/config:/config -v /etc/localtime:/etc/localtime:ro --net=host balloob/home-assistant ``` +After you have launched the Docker image, navigate to its web interface on [http://127.0.0.1:8123](http://127.0.0.1:8123). + After you got the demo mode running it is time to enable some real components and get started. An example configuration file has been provided in [/config/home-assistant.conf.example](https://github.com/balloob/home-assistant/blob/master/config/home-assistant.conf.example). *Note:* you can append `?api_password=YOUR_PASSWORD` to the url of the web interface to log in automatically. diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 808545fe791..5744df4eb7a 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -54,9 +54,8 @@ def ensure_config_path(config_dir): if not os.path.isfile(config_path): try: with open(config_path, 'w') as conf: - conf.write("[http]\n") - conf.write("api_password=password\n\n") - conf.write("[demo]\n") + conf.write("[http]\n\n") + conf.write("[demo]\n\n") except IOError: print(('Fatal Error: No configuration file found and unable ' 'to write a default one to {}').format(config_path)) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 8b3f4635ab7..f2b5eefa5a1 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -86,7 +86,7 @@ import homeassistant as ha from homeassistant.const import ( SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER) -from homeassistant.helpers import validate_config, TrackStates +from homeassistant.helpers import TrackStates import homeassistant.remote as rem import homeassistant.util as util from . import frontend @@ -117,13 +117,18 @@ DATA_API_PASSWORD = 'api_password' _LOGGER = logging.getLogger(__name__) -def setup(hass, config): +def setup(hass, config=None): """ Sets up the HTTP API and debug interface. """ - if not validate_config(config, {DOMAIN: [CONF_API_PASSWORD]}, _LOGGER): - return False + if config is None or DOMAIN not in config: + config = {DOMAIN: {}} - api_password = config[DOMAIN][CONF_API_PASSWORD] + api_password = config[DOMAIN].get(CONF_API_PASSWORD) + + no_password_set = api_password is None + + if no_password_set: + api_password = util.get_random_string() # If no server host is given, accept all incoming requests server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0') @@ -132,9 +137,9 @@ def setup(hass, config): development = config[DOMAIN].get(CONF_DEVELOPMENT, "") == "1" - server = HomeAssistantHTTPServer((server_host, server_port), - RequestHandler, hass, api_password, - development) + server = HomeAssistantHTTPServer( + (server_host, server_port), RequestHandler, hass, api_password, + development, no_password_set) hass.bus.listen_once( ha.EVENT_HOMEASSISTANT_START, @@ -155,13 +160,14 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): # pylint: disable=too-many-arguments def __init__(self, server_address, request_handler_class, - hass, api_password, development=False): + hass, api_password, development, no_password_set): super().__init__(server_address, request_handler_class) self.server_address = server_address self.hass = hass self.api_password = api_password self.development = development + self.no_password_set = no_password_set # We will lazy init this one if needed self.event_forwarder = None @@ -270,10 +276,13 @@ class RequestHandler(SimpleHTTPRequestHandler): "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) return - api_password = self.headers.get(AUTH_HEADER) + if self.server.no_password_set: + api_password = self.server.api_password + else: + api_password = self.headers.get(AUTH_HEADER) - if not api_password and DATA_API_PASSWORD in data: - api_password = data[DATA_API_PASSWORD] + if not api_password and DATA_API_PASSWORD in data: + api_password = data[DATA_API_PASSWORD] if '_METHOD' in data: method = data.pop('_METHOD') @@ -357,6 +366,10 @@ class RequestHandler(SimpleHTTPRequestHandler): else: app_url = "frontend-{}.html".format(frontend.VERSION) + # auto login if no password was set, else check api_password param + auth = (self.server.api_password if self.server.no_password_set + else data.get('api_password', '')) + write(("" "" "Home Assistant" @@ -375,7 +388,7 @@ class RequestHandler(SimpleHTTPRequestHandler): " src='/static/webcomponents.min.js'>" "" "" - "").format(app_url, data.get('api_password', ''))) + "").format(app_url, auth)) # pylint: disable=unused-argument def _handle_get_api(self, path_match, data): diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 869c690be6b..662cb3e7086 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -112,17 +112,11 @@ class HomeAssistant(ha.HomeAssistant): self.states = StateMachine(self.bus, self.remote_api) def start(self): - # If there is no local API setup but we do want to connect with remote - # We create a random password and set up a local api + # Ensure a local API exists to connect with remote if self.local_api is None: import homeassistant.components.http as http - import random - # pylint: disable=too-many-format-args - random_password = '{:30}'.format(random.randrange(16**30)) - - http.setup( - self, {http.DOMAIN: {http.CONF_API_PASSWORD: random_password}}) + http.setup(self) ha.Timer(self) diff --git a/homeassistant/util.py b/homeassistant/util.py index 99834b1ea2c..c2efadbd1f3 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -12,6 +12,8 @@ from datetime import datetime, timedelta import re import enum import socket +import random +import string from functools import wraps RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') @@ -134,16 +136,16 @@ def convert(value, to_type, default=None): def ensure_unique_string(preferred_string, current_strings): """ Returns a string that is not present in current_strings. If preferred string exists will append _2, _3, .. """ - string = preferred_string + test_string = preferred_string current_strings = list(current_strings) tries = 1 - while string in current_strings: + while test_string in current_strings: tries += 1 - string = "{}_{}".format(preferred_string, tries) + test_string = "{}_{}".format(preferred_string, tries) - return string + return test_string # Taken from: http://stackoverflow.com/a/11735897 @@ -163,6 +165,15 @@ def get_local_ip(): return socket.gethostbyname(socket.gethostname()) +# Taken from http://stackoverflow.com/a/23728630 +def get_random_string(length=10): + """ Returns a random string with letters and digits. """ + generator = random.SystemRandom() + source_chars = string.ascii_letters + string.digits + + return ''.join(generator.choice(source_chars) for _ in range(length)) + + class OrderedEnum(enum.Enum): """ Taken from Python 3.4.0 docs. """ # pylint: disable=no-init, too-few-public-methods diff --git a/tests/test_component_http.py b/tests/test_component_http.py index ba547e2bbe4..13ada8efbc0 100644 --- a/tests/test_component_http.py +++ b/tests/test_component_http.py @@ -58,11 +58,6 @@ def tearDownModule(): # pylint: disable=invalid-name class TestHTTP(unittest.TestCase): """ Test the HTTP debug interface and API. """ - def test_setup(self): - """ Test http.setup. """ - self.assertFalse(http.setup(hass, {})) - self.assertFalse(http.setup(hass, {http.DOMAIN: {}})) - def test_frontend_and_static(self): """ Tests if we can get the frontend. """ req = requests.get(_url("")) diff --git a/tests/test_remote.py b/tests/test_remote.py index e22eca3e49f..da491e69016 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -68,12 +68,13 @@ class TestRemoteMethods(unittest.TestCase): """ Test Python API validate_api. """ self.assertEqual(remote.APIStatus.OK, remote.validate_api(master_api)) - self.assertEqual(remote.APIStatus.INVALID_PASSWORD, - remote.validate_api( - remote.API("127.0.0.1", API_PASSWORD + "A"))) + self.assertEqual( + remote.APIStatus.INVALID_PASSWORD, + remote.validate_api( + remote.API("127.0.0.1", API_PASSWORD + "A", 8122))) - self.assertEqual(remote.APIStatus.CANNOT_CONNECT, - remote.validate_api(broken_api)) + self.assertEqual( + remote.APIStatus.CANNOT_CONNECT, remote.validate_api(broken_api)) def test_get_event_listeners(self): """ Test Python API get_event_listeners. """