From d0e4c95bbc3a95aa4db17f504db454c86403bdc2 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 13 Aug 2018 02:26:06 -0700 Subject: [PATCH] MQTT embedded broker has to set its own password. (#15929) --- homeassistant/components/mqtt/__init__.py | 16 +++++- homeassistant/components/mqtt/server.py | 26 ++++----- tests/components/mqtt/test_server.py | 65 +++++++++++++++++++---- 3 files changed, 82 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3928eb945aa..70d4d7aa5d7 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -32,7 +32,8 @@ from homeassistant.util.async_ import ( from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) -from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA + +from .server import HBMQTT_CONFIG_SCHEMA REQUIREMENTS = ['paho-mqtt==1.3.1'] @@ -306,7 +307,8 @@ async def _async_setup_server(hass: HomeAssistantType, return None success, broker_config = \ - await server.async_start(hass, conf.get(CONF_EMBEDDED)) + await server.async_start( + hass, conf.get(CONF_PASSWORD), conf.get(CONF_EMBEDDED)) if not success: return None @@ -349,6 +351,16 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if CONF_EMBEDDED not in conf and CONF_BROKER in conf: broker_config = None else: + if (conf.get(CONF_PASSWORD) is None and + config.get('http') is not None and + config['http'].get('api_password') is not None): + _LOGGER.error("Starting from 0.77, embedded MQTT broker doesn't" + " use api_password as default password any more." + " Please set password configuration. See https://" + "home-assistant.io/docs/mqtt/broker#embedded-broker" + " for details") + return False + broker_config = await _async_setup_server(hass, config) if CONF_BROKER in conf: diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index 8a012928792..5fc365342ae 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -27,27 +27,29 @@ HBMQTT_CONFIG_SCHEMA = vol.Any(None, vol.Schema({ }) }, extra=vol.ALLOW_EXTRA)) +_LOGGER = logging.getLogger(__name__) + @asyncio.coroutine -def async_start(hass, server_config): +def async_start(hass, password, server_config): """Initialize MQTT Server. This method is a coroutine. """ from hbmqtt.broker import Broker, BrokerException + passwd = tempfile.NamedTemporaryFile() try: - passwd = tempfile.NamedTemporaryFile() - if server_config is None: - server_config, client_config = generate_config(hass, passwd) + server_config, client_config = generate_config( + hass, passwd, password) else: client_config = None broker = Broker(server_config, hass.loop) yield from broker.start() except BrokerException: - logging.getLogger(__name__).exception("Error initializing MQTT server") + _LOGGER.exception("Error initializing MQTT server") return False, None finally: passwd.close() @@ -63,9 +65,10 @@ def async_start(hass, server_config): return True, client_config -def generate_config(hass, passwd): +def generate_config(hass, passwd, password): """Generate a configuration based on current Home Assistant instance.""" - from homeassistant.components.mqtt import PROTOCOL_311 + from . import PROTOCOL_311 + config = { 'listeners': { 'default': { @@ -79,29 +82,26 @@ def generate_config(hass, passwd): }, }, 'auth': { - 'allow-anonymous': hass.config.api.api_password is None + 'allow-anonymous': password is None }, 'plugins': ['auth_anonymous'], } - if hass.config.api.api_password: + if password: username = 'homeassistant' - password = hass.config.api.api_password # Encrypt with what hbmqtt uses to verify from passlib.apps import custom_app_context passwd.write( 'homeassistant:{}\n'.format( - custom_app_context.encrypt( - hass.config.api.api_password)).encode('utf-8')) + custom_app_context.encrypt(password)).encode('utf-8')) passwd.flush() config['auth']['password-file'] = passwd.name config['plugins'].append('auth_file') else: username = None - password = None client_config = ('localhost', 1883, username, password, None, PROTOCOL_311) diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 1c37c9049f3..d5d54f457d6 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -4,6 +4,7 @@ import sys import pytest +from homeassistant.const import CONF_PASSWORD from homeassistant.setup import setup_component import homeassistant.components.mqtt as mqtt @@ -19,9 +20,6 @@ class TestMQTT: def setup_method(self, method): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - setup_component(self.hass, 'http', { - 'api_password': 'super_secret' - }) def teardown_method(self, method): """Stop everything that was started.""" @@ -32,14 +30,36 @@ class TestMQTT: @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) @patch('homeassistant.components.mqtt.MQTT') - def test_creating_config_with_http_pass(self, mock_mqtt): - """Test if the MQTT server gets started and subscribe/publish msg.""" + def test_creating_config_with_http_pass_only(self, mock_mqtt): + """Test if the MQTT server failed starts. + + Since 0.77, MQTT server has to setup its own password. + If user has api_password but don't have mqtt.password, MQTT component + will fail to start + """ mock_mqtt().async_connect.return_value = mock_coro(True) self.hass.bus.listen_once = MagicMock() - password = 'super_secret' + assert not setup_component(self.hass, mqtt.DOMAIN, { + 'http': {'api_password': 'http_secret'} + }) - self.hass.config.api = MagicMock(api_password=password) - assert setup_component(self.hass, mqtt.DOMAIN, {}) + @patch('passlib.apps.custom_app_context', Mock(return_value='')) + @patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) + @patch('homeassistant.components.mqtt.MQTT') + def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): + """Test if the MQTT server gets started with password. + + Since 0.77, MQTT server has to setup its own password. + """ + mock_mqtt().async_connect.return_value = mock_coro(True) + self.hass.bus.listen_once = MagicMock() + password = 'mqtt_secret' + + assert setup_component(self.hass, mqtt.DOMAIN, { + mqtt.DOMAIN: {CONF_PASSWORD: password}, + }) assert mock_mqtt.called from pprint import pprint pprint(mock_mqtt.mock_calls) @@ -51,8 +71,33 @@ class TestMQTT: @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) @patch('homeassistant.components.mqtt.MQTT') - def test_creating_config_with_http_no_pass(self, mock_mqtt): - """Test if the MQTT server gets started and subscribe/publish msg.""" + def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): + """Test if the MQTT server gets started with password. + + Since 0.77, MQTT server has to setup its own password. + """ + mock_mqtt().async_connect.return_value = mock_coro(True) + self.hass.bus.listen_once = MagicMock() + password = 'mqtt_secret' + + self.hass.config.api = MagicMock(api_password='api_password') + assert setup_component(self.hass, mqtt.DOMAIN, { + 'http': {'api_password': 'http_secret'}, + mqtt.DOMAIN: {CONF_PASSWORD: password}, + }) + assert mock_mqtt.called + from pprint import pprint + pprint(mock_mqtt.mock_calls) + assert mock_mqtt.mock_calls[1][1][5] == 'homeassistant' + assert mock_mqtt.mock_calls[1][1][6] == password + + @patch('passlib.apps.custom_app_context', Mock(return_value='')) + @patch('tempfile.NamedTemporaryFile', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) + @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) + @patch('homeassistant.components.mqtt.MQTT') + def test_creating_config_without_pass(self, mock_mqtt): + """Test if the MQTT server gets started without password.""" mock_mqtt().async_connect.return_value = mock_coro(True) self.hass.bus.listen_once = MagicMock()