diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 71271341f33..5fe5d1306be 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -9,6 +9,7 @@ import logging import os import socket import time +import ssl import requests.certs import voluptuous as vol @@ -48,6 +49,7 @@ CONF_CERTIFICATE = 'certificate' CONF_CLIENT_KEY = 'client_key' CONF_CLIENT_CERT = 'client_cert' CONF_TLS_INSECURE = 'tls_insecure' +CONF_TLS_VERSION = 'tls_version' CONF_BIRTH_MESSAGE = 'birth_message' CONF_WILL_MESSAGE = 'will_message' @@ -67,6 +69,7 @@ DEFAULT_RETAIN = False DEFAULT_PROTOCOL = PROTOCOL_311 DEFAULT_DISCOVERY = False DEFAULT_DISCOVERY_PREFIX = 'homeassistant' +DEFAULT_TLS_PROTOCOL = 'auto' ATTR_TOPIC = 'topic' ATTR_PAYLOAD = 'payload' @@ -122,6 +125,9 @@ CONFIG_SCHEMA = vol.Schema({ vol.Inclusive(CONF_CLIENT_CERT, 'client_key_auth', msg=CLIENT_KEY_AUTH_MSG): cv.isfile, vol.Optional(CONF_TLS_INSECURE): cv.boolean, + vol.Optional(CONF_TLS_VERSION, + default=DEFAULT_TLS_PROTOCOL): vol.Any('auto', '1.0', + '1.1', '1.2'), vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])), vol.Optional(CONF_EMBEDDED): HBMQTT_CONFIG_SCHEMA, @@ -318,11 +324,27 @@ def async_setup(hass, config): will_message = conf.get(CONF_WILL_MESSAGE) birth_message = conf.get(CONF_BIRTH_MESSAGE) + # Be able to override versions other than TLSv1.0 under Python3.6 + conf_tls_version = conf.get(CONF_TLS_VERSION) + if conf_tls_version == '1.2': + tls_version = ssl.PROTOCOL_TLSv1_2 + elif conf_tls_version == '1.1': + tls_version = ssl.PROTOCOL_TLSv1_1 + elif conf_tls_version == '1.0': + tls_version = ssl.PROTOCOL_TLSv1 + else: + import sys + # Python3.6 supports automatic negotiation of highest TLS version + if sys.hexversion >= 0x03060000: + tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member + else: + tls_version = ssl.PROTOCOL_TLSv1 + try: hass.data[DATA_MQTT] = MQTT( hass, broker, port, client_id, keepalive, username, password, certificate, client_key, client_cert, tls_insecure, protocol, - will_message, birth_message) + will_message, birth_message, tls_version) except socket.error: _LOGGER.exception("Can't connect to the broker. " "Please check your settings and the broker itself") @@ -380,7 +402,8 @@ class MQTT(object): def __init__(self, hass, broker, port, client_id, keepalive, username, password, certificate, client_key, client_cert, - tls_insecure, protocol, will_message, birth_message): + tls_insecure, protocol, will_message, birth_message, + tls_version): """Initialize Home Assistant MQTT client.""" import paho.mqtt.client as mqtt @@ -409,7 +432,8 @@ class MQTT(object): if certificate is not None: self._mqttc.tls_set( - certificate, certfile=client_cert, keyfile=client_key) + certificate, certfile=client_cert, + keyfile=client_key, tls_version=tls_version) if tls_insecure is not None: self._mqttc.tls_insecure_set(tls_insecure) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index ce9743be6e0..78fc494c561 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -4,6 +4,7 @@ from collections import namedtuple, OrderedDict import unittest from unittest import mock import socket +import ssl import voluptuous as vol @@ -409,6 +410,52 @@ def test_setup_uses_certificate_not_on_mqtts_port(hass): assert mock_MQTT.mock_calls[0][1][7] != mqttsCertificateBundle +@asyncio.coroutine +def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): + """Test setup defaults to TLSv1 under python3.6.""" + test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker'}} + + with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT: + yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg) + + assert mock_MQTT.called + + import sys + if sys.hexversion >= 0x03060000: + expectedTlsVersion = ssl.PROTOCOL_TLS # pylint: disable=no-member + else: + expectedTlsVersion = ssl.PROTOCOL_TLSv1 + + assert mock_MQTT.mock_calls[0][1][14] == expectedTlsVersion + + +@asyncio.coroutine +def test_setup_with_tls_config_uses_tls_version1_2(hass): + """Test setup uses specified TLS version.""" + test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker', + 'tls_version': '1.2'}} + + with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT: + yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg) + + assert mock_MQTT.called + + assert mock_MQTT.mock_calls[0][1][14] == ssl.PROTOCOL_TLSv1_2 + + +@asyncio.coroutine +def test_setup_with_tls_config_of_v1_under_python36_only_uses_v1(hass): + """Test setup uses TLSv1.0 if explicitly chosen.""" + test_broker_cfg = {mqtt.DOMAIN: {mqtt.CONF_BROKER: 'test-broker', + 'tls_version': '1.0'}} + + with mock.patch('homeassistant.components.mqtt.MQTT') as mock_MQTT: + yield from async_setup_component(hass, mqtt.DOMAIN, test_broker_cfg) + + assert mock_MQTT.called + assert mock_MQTT.mock_calls[0][1][14] == ssl.PROTOCOL_TLSv1 + + @asyncio.coroutine def test_birth_message(hass): """Test sending birth message.""" diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 250a8ccc23a..e27bb99fa59 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -108,6 +108,7 @@ class TestCheckConfig(unittest.TestCase): 'protocol': '3.1.1', 'discovery': False, 'discovery_prefix': 'homeassistant', + 'tls_version': 'auto', }, 'light': []}, res['components']