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
12 changes: 1 addition & 11 deletions homeassistant/components/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@
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)
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,
Expand All @@ -51,8 +50,6 @@
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.
Expand Down Expand Up @@ -409,13 +406,6 @@ def handle(request):
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)',
Expand Down
20 changes: 16 additions & 4 deletions homeassistant/components/http/ban.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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
Expand All @@ -19,6 +19,7 @@
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"
Expand Down Expand Up @@ -52,23 +53,34 @@ def ban_middleware_handler(request):
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


@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

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] >
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/websocket_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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'

Expand Down Expand Up @@ -256,9 +257,9 @@ def cancel_connection(event):
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())
Expand Down
1 change: 0 additions & 1 deletion tests/components/http/test_ban.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ def call_server():
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'})
Expand Down
15 changes: 9 additions & 6 deletions tests/components/test_websocket_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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'

Expand Down Expand Up @@ -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'

Expand Down