diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 2bb35dd8f3f..d6e03e76619 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -19,7 +19,6 @@ from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently import homeassistant.helpers.config_validation as cv import homeassistant.remote as rem import homeassistant.util as hass_util -from homeassistant.components import persistent_notification from homeassistant.const import ( SERVER_PORT, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) @@ -27,7 +26,7 @@ from homeassistant.core import is_callback from homeassistant.util.logging import HideSensitiveDataFilter from .auth import auth_middleware -from .ban import ban_middleware, process_wrong_login +from .ban import ban_middleware from .const import ( KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS, KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD, @@ -51,8 +50,6 @@ CONF_TRUSTED_NETWORKS = 'trusted_networks' CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' CONF_IP_BAN_ENABLED = 'ip_ban_enabled' -NOTIFICATION_ID_LOGIN = 'http-login' - # TLS configuation follows the best-practice guidelines specified here: # https://wiki.mozilla.org/Security/Server_Side_TLS # Intermediate guidelines are followed. @@ -409,13 +406,6 @@ def request_handler_factory(view, handler): authenticated = request.get(KEY_AUTHENTICATED, False) if view.requires_auth and not authenticated: - yield from process_wrong_login(request) - _LOGGER.warning('Login attempt or request with an invalid ' - 'password from %s', remote_addr) - persistent_notification.async_create( - request.app['hass'], - 'Invalid password used from {}'.format(remote_addr), - 'Login attempt failed', NOTIFICATION_ID_LOGIN) raise HTTPUnauthorized() _LOGGER.info('Serving %s to %s (auth: %s)', diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index b3f17c1dd57..96a32d1ae6e 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -5,7 +5,7 @@ from datetime import datetime from ipaddress import ip_address import logging -from aiohttp.web_exceptions import HTTPForbidden +from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized import voluptuous as vol from homeassistant.components import persistent_notification @@ -19,6 +19,7 @@ from .const import ( from .util import get_real_ip NOTIFICATION_ID_BAN = 'ip-ban' +NOTIFICATION_ID_LOGIN = 'http-login' IP_BANS_FILE = 'ip_bans.yaml' ATTR_BANNED_AT = "banned_at" @@ -52,7 +53,11 @@ def ban_middleware(app, handler): if is_banned: raise HTTPForbidden() - return handler(request) + try: + return (yield from handler(request)) + except HTTPUnauthorized: + yield from process_wrong_login(request) + raise return ban_middleware_handler @@ -60,6 +65,15 @@ def ban_middleware(app, handler): @asyncio.coroutine def process_wrong_login(request): """Process a wrong login attempt.""" + remote_addr = get_real_ip(request) + + msg = ('Login attempt or request with invalid authentication ' + 'from {}'.format(remote_addr)) + _LOGGER.warning(msg) + persistent_notification.async_create( + request.app['hass'], msg, 'Login attempt failed', + NOTIFICATION_ID_LOGIN) + if (not request.app[KEY_BANS_ENABLED] or request.app[KEY_LOGIN_THRESHOLD] < 1): return @@ -67,8 +81,6 @@ def process_wrong_login(request): if KEY_FAILED_LOGIN_ATTEMPTS not in request.app: request.app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int) - remote_addr = get_real_ip(request) - request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1 if (request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] > diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 70b35e00247..a3557a301c5 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -18,6 +18,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.auth import validate_password from homeassistant.components.http.const import KEY_AUTHENTICATED +from homeassistant.components.http.ban import process_wrong_login DOMAIN = 'websocket_api' @@ -256,9 +257,9 @@ class ActiveConnection: else: self.debug('Invalid password') self.send_message(auth_invalid_message('Invalid password')) - return wsock if not authenticated: + yield from process_wrong_login(self.request) return wsock self.send_message(auth_ok_message()) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index c210bc3f0e0..b01535206ff 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -97,7 +97,6 @@ class TestHttp: with patch('homeassistant.components.http.' 'ban.get_real_ip', return_value=ip_address("200.201.202.204")): - print("GETTING API") return requests.get( _url(const.URL_API), headers={const.HTTP_HEADER_HA_AUTH: 'Wrong password'}) diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 3cdc77414ee..658a5e0be53 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -9,7 +9,7 @@ import pytest from homeassistant.core import callback from homeassistant.components import websocket_api as wapi, frontend -from tests.common import mock_http_component_app +from tests.common import mock_http_component_app, mock_coro API_PASSWORD = 'test1234' @@ -66,13 +66,16 @@ def test_auth_via_msg(no_auth_websocket_client): @asyncio.coroutine def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): """Test authenticating.""" - no_auth_websocket_client.send_json({ - 'type': wapi.TYPE_AUTH, - 'api_password': API_PASSWORD + 'wrong' - }) + with patch('homeassistant.components.websocket_api.process_wrong_login', + return_value=mock_coro()) as mock_process_wrong_login: + no_auth_websocket_client.send_json({ + 'type': wapi.TYPE_AUTH, + 'api_password': API_PASSWORD + 'wrong' + }) - msg = yield from no_auth_websocket_client.receive_json() + msg = yield from no_auth_websocket_client.receive_json() + assert mock_process_wrong_login.called assert msg['type'] == wapi.TYPE_AUTH_INVALID assert msg['message'] == 'Invalid password'