Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup http #12424

Merged
merged 5 commits into from Feb 15, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions homeassistant/components/emulated_hue/__init__.py
Expand Up @@ -14,7 +14,7 @@
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.components.http import REQUIREMENTS # NOQA
from homeassistant.components.http import HomeAssistantWSGI
from homeassistant.components.http import HomeAssistantHTTP
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.deprecation import get_deprecated
import homeassistant.helpers.config_validation as cv
Expand Down Expand Up @@ -86,7 +86,7 @@ def setup(hass, yaml_config):
"""Activate the emulated_hue component."""
config = Config(hass, yaml_config.get(DOMAIN, {}))

server = HomeAssistantWSGI(
server = HomeAssistantHTTP(
hass,
server_host=config.host_ip_addr,
server_port=config.listen_port,
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/frontend/__init__.py
Expand Up @@ -17,7 +17,7 @@

import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.auth import is_trusted_ip
from homeassistant.components.http.const import KEY_AUTHENTICATED
from homeassistant.config import find_config_file, load_yaml_config_file
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
Expand Down Expand Up @@ -490,7 +490,7 @@ def get(self, request, extra=None):
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5

no_auth = '1'
if hass.config.api.api_password and not is_trusted_ip(request):
if hass.config.api.api_password and not request[KEY_AUTHENTICATED]:
# do not try to auto connect on load
no_auth = '0'

Expand Down
74 changes: 26 additions & 48 deletions homeassistant/components/http/__init__.py
Expand Up @@ -12,35 +12,28 @@
import ssl

from aiohttp import web
from aiohttp.hdrs import ACCEPT, ORIGIN, CONTENT_TYPE
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently
import voluptuous as vol

from homeassistant.const import (
SERVER_PORT, CONTENT_TYPE_JSON, HTTP_HEADER_HA_AUTH,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
HTTP_HEADER_X_REQUESTED_WITH)
SERVER_PORT, CONTENT_TYPE_JSON,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,)
from homeassistant.core import is_callback
import homeassistant.helpers.config_validation as cv
import homeassistant.remote as rem
import homeassistant.util as hass_util
from homeassistant.util.logging import HideSensitiveDataFilter

from .auth import auth_middleware
from .ban import ban_middleware
from .const import (
KEY_BANS_ENABLED, KEY_AUTHENTICATED, KEY_LOGIN_THRESHOLD,
KEY_TRUSTED_NETWORKS, KEY_USE_X_FORWARDED_FOR)
from .auth import setup_auth
from .ban import setup_bans
from .cors import setup_cors
from .real_ip import setup_real_ip
from .const import KEY_AUTHENTICATED, KEY_REAL_IP
from .static import (
CachingFileResponse, CachingStaticResource, staticresource_middleware)
from .util import get_real_ip

REQUIREMENTS = ['aiohttp_cors==0.6.0']

ALLOWED_CORS_HEADERS = [
ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE,
HTTP_HEADER_HA_AUTH]

DOMAIN = 'http'

CONF_API_PASSWORD = 'api_password'
Expand Down Expand Up @@ -127,7 +120,7 @@ def async_setup(hass, config):
logging.getLogger('aiohttp.access').addFilter(
HideSensitiveDataFilter(api_password))

server = HomeAssistantWSGI(
server = HomeAssistantHTTP(
hass,
server_host=server_host,
server_port=server_port,
Expand Down Expand Up @@ -173,47 +166,40 @@ def start_server(event):
return True


class HomeAssistantWSGI(object):
"""WSGI server for Home Assistant."""
class HomeAssistantHTTP(object):
"""HTTP server for Home Assistant."""

def __init__(self, hass, api_password, ssl_certificate,
ssl_key, server_host, server_port, cors_origins,
use_x_forwarded_for, trusted_networks,
login_threshold, is_ban_enabled):
"""Initialize the WSGI Home Assistant server."""
middlewares = [auth_middleware, staticresource_middleware]
"""Initialize the HTTP Home Assistant server."""
app = self.app = web.Application(
middlewares=[staticresource_middleware])

# This order matters
setup_real_ip(app, use_x_forwarded_for)

if is_ban_enabled:
middlewares.insert(0, ban_middleware)
setup_bans(hass, app, login_threshold)

setup_auth(app, trusted_networks, api_password)

if cors_origins:
setup_cors(app, cors_origins)

self.app = web.Application(middlewares=middlewares)
self.app['hass'] = hass
self.app[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for
self.app[KEY_TRUSTED_NETWORKS] = trusted_networks
self.app[KEY_BANS_ENABLED] = is_ban_enabled
self.app[KEY_LOGIN_THRESHOLD] = login_threshold
app['hass'] = hass

self.hass = hass
self.api_password = api_password
self.ssl_certificate = ssl_certificate
self.ssl_key = ssl_key
self.server_host = server_host
self.server_port = server_port
self.is_ban_enabled = is_ban_enabled
self._handler = None
self.server = None

if cors_origins:
import aiohttp_cors

self.cors = aiohttp_cors.setup(self.app, defaults={
host: aiohttp_cors.ResourceOptions(
allow_headers=ALLOWED_CORS_HEADERS,
allow_methods='*',
) for host in cors_origins
})
else:
self.cors = None

def register_view(self, view):
"""Register a view with the WSGI server.

Expand Down Expand Up @@ -292,15 +278,7 @@ def serve_file(request):
@asyncio.coroutine
def start(self):
"""Start the WSGI server."""
cors_added = set()
if self.cors is not None:
for route in list(self.app.router.routes()):
if hasattr(route, 'resource'):
route = route.resource
if route in cors_added:
continue
self.cors.add(route)
cors_added.add(route)
yield from self.app.startup()

if self.ssl_certificate:
try:
Expand Down Expand Up @@ -420,7 +398,7 @@ def handle(request):
raise HTTPUnauthorized()

_LOGGER.info('Serving %s to %s (auth: %s)',
request.path, get_real_ip(request), authenticated)
request.path, request.get(KEY_REAL_IP), authenticated)

result = handler(request, **request.match_info)

Expand Down
83 changes: 47 additions & 36 deletions homeassistant/components/http/auth.py
Expand Up @@ -7,55 +7,66 @@
from aiohttp import hdrs
from aiohttp.web import middleware

from homeassistant.core import callback
from homeassistant.const import HTTP_HEADER_HA_AUTH
from .util import get_real_ip
from .const import KEY_TRUSTED_NETWORKS, KEY_AUTHENTICATED
from .const import KEY_AUTHENTICATED, KEY_REAL_IP

DATA_API_PASSWORD = 'api_password'

_LOGGER = logging.getLogger(__name__)


@middleware
@asyncio.coroutine
def auth_middleware(request, handler):
"""Authenticate as middleware."""
# If no password set, just always set authenticated=True
if request.app['hass'].http.api_password is None:
request[KEY_AUTHENTICATED] = True
@callback
def setup_auth(app, trusted_networks, api_password):
"""Create auth middleware for the app."""
@middleware
@asyncio.coroutine
def auth_middleware(request, handler):
"""Authenticate as middleware."""
# If no password set, just always set authenticated=True
if api_password is None:
request[KEY_AUTHENTICATED] = True
return (yield from handler(request))

# Check authentication
authenticated = False

if (HTTP_HEADER_HA_AUTH in request.headers and
hmac.compare_digest(
api_password, request.headers[HTTP_HEADER_HA_AUTH])):
# A valid auth header has been set
authenticated = True

elif (DATA_API_PASSWORD in request.query and
hmac.compare_digest(api_password,
request.query[DATA_API_PASSWORD])):
authenticated = True

elif (hdrs.AUTHORIZATION in request.headers and
validate_authorization_header(api_password, request)):
authenticated = True

elif _is_trusted_ip(request, trusted_networks):
authenticated = True

request[KEY_AUTHENTICATED] = authenticated
return (yield from handler(request))

# Check authentication
authenticated = False
@asyncio.coroutine
def auth_startup(app):
"""Initialize auth middleware when app starts up."""
app.middlewares.append(auth_middleware)

if (HTTP_HEADER_HA_AUTH in request.headers and
validate_password(
request, request.headers[HTTP_HEADER_HA_AUTH])):
# A valid auth header has been set
authenticated = True
app.on_startup.append(auth_startup)

elif (DATA_API_PASSWORD in request.query and
validate_password(request, request.query[DATA_API_PASSWORD])):
authenticated = True

elif (hdrs.AUTHORIZATION in request.headers and
validate_authorization_header(request)):
authenticated = True

elif is_trusted_ip(request):
authenticated = True

request[KEY_AUTHENTICATED] = authenticated
return (yield from handler(request))


def is_trusted_ip(request):
def _is_trusted_ip(request, trusted_networks):
"""Test if request is from a trusted ip."""
ip_addr = get_real_ip(request)
ip_addr = request[KEY_REAL_IP]

return ip_addr and any(
return any(
ip_addr in trusted_network for trusted_network
in request.app[KEY_TRUSTED_NETWORKS])
in trusted_networks)


def validate_password(request, api_password):
Expand All @@ -64,7 +75,7 @@ def validate_password(request, api_password):
api_password, request.app['hass'].http.api_password)


def validate_authorization_header(request):
def validate_authorization_header(api_password, request):
"""Test an authorization header if valid password."""
if hdrs.AUTHORIZATION not in request.headers:
return False
Expand All @@ -80,4 +91,4 @@ def validate_authorization_header(request):
if username != 'homeassistant':
return False

return validate_password(request, password)
return hmac.compare_digest(api_password, password)
44 changes: 27 additions & 17 deletions homeassistant/components/http/ban.py
Expand Up @@ -10,18 +10,20 @@
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
import voluptuous as vol

from homeassistant.core import callback
from homeassistant.components import persistent_notification
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.util.yaml import dump
from .const import (
KEY_BANS_ENABLED, KEY_BANNED_IPS, KEY_LOGIN_THRESHOLD,
KEY_FAILED_LOGIN_ATTEMPTS)
from .util import get_real_ip
from .const import KEY_REAL_IP

_LOGGER = logging.getLogger(__name__)

KEY_BANNED_IPS = 'ha_banned_ips'
KEY_FAILED_LOGIN_ATTEMPTS = 'ha_failed_login_attempts'
KEY_LOGIN_THRESHOLD = 'ha_login_threshold'

NOTIFICATION_ID_BAN = 'ip-ban'
NOTIFICATION_ID_LOGIN = 'http-login'

Expand All @@ -33,21 +35,31 @@
})


@callback
def setup_bans(hass, app, login_threshold):
"""Create IP Ban middleware for the app."""
@asyncio.coroutine
def ban_startup(app):
"""Initialize bans when app starts up."""
app.middlewares.append(ban_middleware)
app[KEY_BANNED_IPS] = yield from hass.async_add_job(
load_ip_bans_config, hass.config.path(IP_BANS_FILE))
app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int)
app[KEY_LOGIN_THRESHOLD] = login_threshold

app.on_startup.append(ban_startup)


@middleware
@asyncio.coroutine
def ban_middleware(request, handler):
"""IP Ban middleware."""
if not request.app[KEY_BANS_ENABLED]:
return (yield from handler(request))

if KEY_BANNED_IPS not in request.app:
hass = request.app['hass']
request.app[KEY_BANNED_IPS] = yield from hass.async_add_job(
load_ip_bans_config, hass.config.path(IP_BANS_FILE))
_LOGGER.error('IP Ban middleware loaded but banned IPs not loaded')
return (yield from handler(request))

# Verify if IP is not banned
ip_address_ = get_real_ip(request)

ip_address_ = request[KEY_REAL_IP]
is_banned = any(ip_ban.ip_address == ip_address_
for ip_ban in request.app[KEY_BANNED_IPS])

Expand All @@ -64,7 +76,7 @@ def ban_middleware(request, handler):
@asyncio.coroutine
def process_wrong_login(request):
"""Process a wrong login attempt."""
remote_addr = get_real_ip(request)
remote_addr = request[KEY_REAL_IP]

msg = ('Login attempt or request with invalid authentication '
'from {}'.format(remote_addr))
Expand All @@ -73,13 +85,11 @@ def process_wrong_login(request):
request.app['hass'], msg, 'Login attempt failed',
NOTIFICATION_ID_LOGIN)

if (not request.app[KEY_BANS_ENABLED] or
# Check if ban middleware is loaded
if (KEY_BANNED_IPS not in request.app 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)

request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1

if (request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >
Expand Down
8 changes: 0 additions & 8 deletions homeassistant/components/http/const.py
@@ -1,11 +1,3 @@
"""HTTP specific constants."""
KEY_AUTHENTICATED = 'ha_authenticated'
KEY_USE_X_FORWARDED_FOR = 'ha_use_x_forwarded_for'
KEY_TRUSTED_NETWORKS = 'ha_trusted_networks'
KEY_REAL_IP = 'ha_real_ip'
KEY_BANS_ENABLED = 'ha_bans_enabled'
KEY_BANNED_IPS = 'ha_banned_ips'
KEY_FAILED_LOGIN_ATTEMPTS = 'ha_failed_login_attempts'
KEY_LOGIN_THRESHOLD = 'ha_login_threshold'

HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For'