MQTT embedded broker has to set its own password. (#15929)

This commit is contained in:
Jason Hu 2018-08-13 02:26:06 -07:00 committed by Paulus Schoutsen
parent 6aee535d7c
commit 45f12dd3c7
3 changed files with 82 additions and 25 deletions

View file

@ -32,7 +32,8 @@ from homeassistant.util.async_ import (
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME,
CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) 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'] REQUIREMENTS = ['paho-mqtt==1.3.1']
@ -306,7 +307,8 @@ async def _async_setup_server(hass: HomeAssistantType,
return None return None
success, broker_config = \ 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: if not success:
return None 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: if CONF_EMBEDDED not in conf and CONF_BROKER in conf:
broker_config = None broker_config = None
else: 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) broker_config = await _async_setup_server(hass, config)
if CONF_BROKER in conf: if CONF_BROKER in conf:

View file

@ -27,27 +27,29 @@ HBMQTT_CONFIG_SCHEMA = vol.Any(None, vol.Schema({
}) })
}, extra=vol.ALLOW_EXTRA)) }, extra=vol.ALLOW_EXTRA))
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine @asyncio.coroutine
def async_start(hass, server_config): def async_start(hass, password, server_config):
"""Initialize MQTT Server. """Initialize MQTT Server.
This method is a coroutine. This method is a coroutine.
""" """
from hbmqtt.broker import Broker, BrokerException from hbmqtt.broker import Broker, BrokerException
try:
passwd = tempfile.NamedTemporaryFile() passwd = tempfile.NamedTemporaryFile()
try:
if server_config is None: if server_config is None:
server_config, client_config = generate_config(hass, passwd) server_config, client_config = generate_config(
hass, passwd, password)
else: else:
client_config = None client_config = None
broker = Broker(server_config, hass.loop) broker = Broker(server_config, hass.loop)
yield from broker.start() yield from broker.start()
except BrokerException: except BrokerException:
logging.getLogger(__name__).exception("Error initializing MQTT server") _LOGGER.exception("Error initializing MQTT server")
return False, None return False, None
finally: finally:
passwd.close() passwd.close()
@ -63,9 +65,10 @@ def async_start(hass, server_config):
return True, client_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.""" """Generate a configuration based on current Home Assistant instance."""
from homeassistant.components.mqtt import PROTOCOL_311 from . import PROTOCOL_311
config = { config = {
'listeners': { 'listeners': {
'default': { 'default': {
@ -79,29 +82,26 @@ def generate_config(hass, passwd):
}, },
}, },
'auth': { 'auth': {
'allow-anonymous': hass.config.api.api_password is None 'allow-anonymous': password is None
}, },
'plugins': ['auth_anonymous'], 'plugins': ['auth_anonymous'],
} }
if hass.config.api.api_password: if password:
username = 'homeassistant' username = 'homeassistant'
password = hass.config.api.api_password
# Encrypt with what hbmqtt uses to verify # Encrypt with what hbmqtt uses to verify
from passlib.apps import custom_app_context from passlib.apps import custom_app_context
passwd.write( passwd.write(
'homeassistant:{}\n'.format( 'homeassistant:{}\n'.format(
custom_app_context.encrypt( custom_app_context.encrypt(password)).encode('utf-8'))
hass.config.api.api_password)).encode('utf-8'))
passwd.flush() passwd.flush()
config['auth']['password-file'] = passwd.name config['auth']['password-file'] = passwd.name
config['plugins'].append('auth_file') config['plugins'].append('auth_file')
else: else:
username = None username = None
password = None
client_config = ('localhost', 1883, username, password, None, PROTOCOL_311) client_config = ('localhost', 1883, username, password, None, PROTOCOL_311)

View file

@ -4,6 +4,7 @@ import sys
import pytest import pytest
from homeassistant.const import CONF_PASSWORD
from homeassistant.setup import setup_component from homeassistant.setup import setup_component
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
@ -19,9 +20,6 @@ class TestMQTT:
def setup_method(self, method): def setup_method(self, method):
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
setup_component(self.hass, 'http', {
'api_password': 'super_secret'
})
def teardown_method(self, method): def teardown_method(self, method):
"""Stop everything that was started.""" """Stop everything that was started."""
@ -32,14 +30,36 @@ class TestMQTT:
@patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock()))
@patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro()))
@patch('homeassistant.components.mqtt.MQTT') @patch('homeassistant.components.mqtt.MQTT')
def test_creating_config_with_http_pass(self, mock_mqtt): def test_creating_config_with_http_pass_only(self, mock_mqtt):
"""Test if the MQTT server gets started and subscribe/publish msg.""" """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) mock_mqtt().async_connect.return_value = mock_coro(True)
self.hass.bus.listen_once = MagicMock() 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) @patch('passlib.apps.custom_app_context', Mock(return_value=''))
assert setup_component(self.hass, mqtt.DOMAIN, {}) @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 assert mock_mqtt.called
from pprint import pprint from pprint import pprint
pprint(mock_mqtt.mock_calls) pprint(mock_mqtt.mock_calls)
@ -51,8 +71,33 @@ class TestMQTT:
@patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock())) @patch('hbmqtt.broker.Broker', Mock(return_value=MagicMock()))
@patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro())) @patch('hbmqtt.broker.Broker.start', Mock(return_value=mock_coro()))
@patch('homeassistant.components.mqtt.MQTT') @patch('homeassistant.components.mqtt.MQTT')
def test_creating_config_with_http_no_pass(self, mock_mqtt): def test_creating_config_with_pass_and_http_pass(self, mock_mqtt):
"""Test if the MQTT server gets started and subscribe/publish msg.""" """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) mock_mqtt().async_connect.return_value = mock_coro(True)
self.hass.bus.listen_once = MagicMock() self.hass.bus.listen_once = MagicMock()