Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions homeassistant/components/mqtt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
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']

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
26 changes: 13 additions & 13 deletions homeassistant/components/mqtt/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,29 @@
})
}, 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()
Expand All @@ -63,9 +65,10 @@ def async_shutdown_mqtt_server(event):
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': {
Expand All @@ -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)

Expand Down
65 changes: 55 additions & 10 deletions tests/components/mqtt/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest

from homeassistant.const import CONF_PASSWORD
from homeassistant.setup import setup_component
import homeassistant.components.mqtt as mqtt

Expand All @@ -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."""
Expand All @@ -32,14 +30,61 @@ def teardown_method(self, method):
@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)
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_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)
Expand All @@ -51,8 +96,8 @@ def test_creating_config_with_http_pass(self, mock_mqtt):
@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_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()

Expand Down