Skip to content

Commit

Permalink
Reset failed login attempts counter when login success (home-assistan…
Browse files Browse the repository at this point in the history
  • Loading branch information
awarecan authored and michaeldavie committed Jul 31, 2018
1 parent 13ef694 commit b5aa5d8
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 7 deletions.
29 changes: 27 additions & 2 deletions homeassistant/components/http/ban.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ async def ban_middleware(request, handler):


async def process_wrong_login(request):
"""Process a wrong login attempt."""
"""Process a wrong login attempt.
Increase failed login attempts counter for remote IP address.
Add ip ban entry if failed login attempts exceeds threshold.
"""
remote_addr = request[KEY_REAL_IP]

msg = ('Login attempt or request with invalid authentication '
Expand Down Expand Up @@ -107,7 +111,28 @@ async def process_wrong_login(request):
'Banning IP address', NOTIFICATION_ID_BAN)


class IpBan(object):
async def process_success_login(request):
"""Process a success login attempt.
Reset failed login attempts counter for remote IP address.
No release IP address from banned list function, it can only be done by
manual modify ip bans config file.
"""
remote_addr = request[KEY_REAL_IP]

# Check if ban middleware is loaded
if (KEY_BANNED_IPS not in request.app or
request.app[KEY_LOGIN_THRESHOLD] < 1):
return

if remote_addr in request.app[KEY_FAILED_LOGIN_ATTEMPTS] and \
request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] > 0:
_LOGGER.debug('Login success, reset failed login attempts counter'
' from %s', remote_addr)
request.app[KEY_FAILED_LOGIN_ATTEMPTS].pop(remote_addr)


class IpBan:
"""Represents banned IP address."""

def __init__(self, ip_ban: str, banned_at: datetime = None) -> None:
Expand Down
8 changes: 6 additions & 2 deletions homeassistant/components/http/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPInternalServerError

import homeassistant.remote as rem
from homeassistant.components.http.ban import process_success_login
from homeassistant.core import is_callback
from homeassistant.const import CONTENT_TYPE_JSON

Expand Down Expand Up @@ -91,8 +92,11 @@ async def handle(request):

authenticated = request.get(KEY_AUTHENTICATED, False)

if view.requires_auth and not authenticated:
raise HTTPUnauthorized()
if view.requires_auth:
if authenticated:
await process_success_login(request)
else:
raise HTTPUnauthorized()

_LOGGER.info('Serving %s to %s (auth: %s)',
request.path, request.get(KEY_REAL_IP), authenticated)
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/websocket_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
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
from homeassistant.components.http.ban import process_wrong_login, \
process_success_login

DOMAIN = 'websocket_api'

Expand Down Expand Up @@ -360,6 +361,7 @@ def handle_hass_stop(event):
return wsock

self.debug("Auth OK")
await process_success_login(request)
await self.wsock.send_json(auth_ok_message())

# ---------- AUTH PHASE OVER ----------
Expand Down
58 changes: 56 additions & 2 deletions tests/components/http/test_ban.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"""The tests for the Home Assistant HTTP component."""
# pylint: disable=protected-access
from unittest.mock import patch, mock_open
from ipaddress import ip_address
from unittest.mock import patch, mock_open, Mock

from aiohttp import web
from aiohttp.web_exceptions import HTTPUnauthorized
from aiohttp.web_middlewares import middleware

from homeassistant.components.http import KEY_AUTHENTICATED
from homeassistant.components.http.view import request_handler_factory
from homeassistant.setup import async_setup_component
import homeassistant.components.http as http
from homeassistant.components.http.ban import (
IpBan, IP_BANS_FILE, setup_bans, KEY_BANNED_IPS)
IpBan, IP_BANS_FILE, setup_bans, KEY_BANNED_IPS, KEY_FAILED_LOGIN_ATTEMPTS)

from . import mock_real_ip

Expand Down Expand Up @@ -88,3 +92,53 @@ async def unauth_handler(request):
resp = await client.get('/')
assert resp.status == 403
assert m.call_count == 1


async def test_failed_login_attempts_counter(hass, aiohttp_client):
"""Testing if failed login attempts counter increased."""
app = web.Application()
app['hass'] = hass

async def auth_handler(request):
"""Return 200 status code."""
return None, 200

app.router.add_get('/auth_true', request_handler_factory(
Mock(requires_auth=True), auth_handler))
app.router.add_get('/auth_false', request_handler_factory(
Mock(requires_auth=True), auth_handler))
app.router.add_get('/', request_handler_factory(
Mock(requires_auth=False), auth_handler))

setup_bans(hass, app, 5)
remote_ip = ip_address("200.201.202.204")
mock_real_ip(app)("200.201.202.204")

@middleware
async def mock_auth(request, handler):
"""Mock auth middleware."""
if 'auth_true' in request.path:
request[KEY_AUTHENTICATED] = True
else:
request[KEY_AUTHENTICATED] = False
return await handler(request)

app.middlewares.append(mock_auth)

client = await aiohttp_client(app)

resp = await client.get('/auth_false')
assert resp.status == 401
assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 1

resp = await client.get('/auth_false')
assert resp.status == 401
assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2

resp = await client.get('/')
assert resp.status == 200
assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2

resp = await client.get('/auth_true')
assert resp.status == 200
assert remote_ip not in app[KEY_FAILED_LOGIN_ATTEMPTS]

0 comments on commit b5aa5d8

Please sign in to comment.