2019-02-14 15:01:46 +00:00
""" Support to serve the Home Assistant API as WSGI application. """
2017-11-04 19:04:05 +00:00
from ipaddress import ip_network
2016-11-25 21:04:06 +00:00
import logging
2017-11-04 19:04:05 +00:00
import os
2016-11-25 21:04:06 +00:00
import ssl
2020-05-08 15:52:32 +00:00
from traceback import extract_stack
2020-06-02 18:54:11 +00:00
from typing import Dict , Optional , cast
2016-11-25 21:04:06 +00:00
from aiohttp import web
2018-03-09 01:51:49 +00:00
from aiohttp . web_exceptions import HTTPMovedPermanently
2017-11-04 19:04:05 +00:00
import voluptuous as vol
2016-11-25 21:04:06 +00:00
2017-11-04 19:04:05 +00:00
from homeassistant . const import (
2019-07-31 19:25:30 +00:00
EVENT_HOMEASSISTANT_START ,
EVENT_HOMEASSISTANT_STOP ,
SERVER_PORT ,
)
2020-06-02 18:54:11 +00:00
from homeassistant . core import Event , HomeAssistant
2020-01-14 21:03:02 +00:00
from homeassistant . helpers import storage
2016-11-25 21:04:06 +00:00
import homeassistant . helpers . config_validation as cv
2020-01-14 21:03:02 +00:00
from homeassistant . loader import bind_hass
2020-06-02 22:02:09 +00:00
from homeassistant . setup import ATTR_COMPONENT , EVENT_COMPONENT_LOADED
2016-12-18 20:56:07 +00:00
import homeassistant . util as hass_util
2018-07-16 08:32:07 +00:00
from homeassistant . util import ssl as ssl_util
2016-11-25 21:04:06 +00:00
2018-02-15 21:06:14 +00:00
from . auth import setup_auth
from . ban import setup_bans
2019-11-16 09:22:07 +00:00
from . const import KEY_AUTHENTICATED , KEY_HASS , KEY_HASS_USER , KEY_REAL_IP # noqa: F401
2018-02-15 21:06:14 +00:00
from . cors import setup_cors
from . real_ip import setup_real_ip
2019-02-15 17:31:54 +00:00
from . static import CACHE_HEADERS , CachingStaticResource
2019-11-16 09:22:07 +00:00
from . view import HomeAssistantView # noqa: F401
2018-03-09 01:51:49 +00:00
2019-08-12 03:38:18 +00:00
# mypy: allow-untyped-defs, no-check-untyped-defs
2019-07-31 19:25:30 +00:00
DOMAIN = " http "
CONF_SERVER_HOST = " server_host "
CONF_SERVER_PORT = " server_port "
CONF_BASE_URL = " base_url "
CONF_SSL_CERTIFICATE = " ssl_certificate "
CONF_SSL_PEER_CERTIFICATE = " ssl_peer_certificate "
CONF_SSL_KEY = " ssl_key "
CONF_CORS_ORIGINS = " cors_allowed_origins "
CONF_USE_X_FORWARDED_FOR = " use_x_forwarded_for "
CONF_TRUSTED_PROXIES = " trusted_proxies "
CONF_LOGIN_ATTEMPTS_THRESHOLD = " login_attempts_threshold "
CONF_IP_BAN_ENABLED = " ip_ban_enabled "
CONF_SSL_PROFILE = " ssl_profile "
SSL_MODERN = " modern "
SSL_INTERMEDIATE = " intermediate "
2016-11-25 21:04:06 +00:00
_LOGGER = logging . getLogger ( __name__ )
2019-07-31 19:25:30 +00:00
DEFAULT_SERVER_HOST = " 0.0.0.0 "
DEFAULT_DEVELOPMENT = " 0 "
2019-08-05 06:24:54 +00:00
# To be able to load custom cards.
DEFAULT_CORS = " https://cast.home-assistant.io "
2018-02-17 09:29:14 +00:00
NO_LOGIN_ATTEMPT_THRESHOLD = - 1
2016-11-25 21:04:06 +00:00
2020-01-09 10:09:34 +00:00
MAX_CLIENT_SIZE : int = 1024 * * 2 * 16
2020-01-14 21:03:02 +00:00
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
2019-02-26 22:42:48 +00:00
2020-05-08 00:29:47 +00:00
HTTP_SCHEMA = vol . All (
cv . deprecated ( CONF_BASE_URL ) ,
vol . Schema (
{
vol . Optional ( CONF_SERVER_HOST , default = DEFAULT_SERVER_HOST ) : cv . string ,
vol . Optional ( CONF_SERVER_PORT , default = SERVER_PORT ) : cv . port ,
vol . Optional ( CONF_BASE_URL ) : cv . string ,
vol . Optional ( CONF_SSL_CERTIFICATE ) : cv . isfile ,
vol . Optional ( CONF_SSL_PEER_CERTIFICATE ) : cv . isfile ,
vol . Optional ( CONF_SSL_KEY ) : cv . isfile ,
vol . Optional ( CONF_CORS_ORIGINS , default = [ DEFAULT_CORS ] ) : vol . All (
cv . ensure_list , [ cv . string ]
) ,
vol . Inclusive ( CONF_USE_X_FORWARDED_FOR , " proxy " ) : cv . boolean ,
vol . Inclusive ( CONF_TRUSTED_PROXIES , " proxy " ) : vol . All (
cv . ensure_list , [ ip_network ]
) ,
vol . Optional (
CONF_LOGIN_ATTEMPTS_THRESHOLD , default = NO_LOGIN_ATTEMPT_THRESHOLD
) : vol . Any ( cv . positive_int , NO_LOGIN_ATTEMPT_THRESHOLD ) ,
vol . Optional ( CONF_IP_BAN_ENABLED , default = True ) : cv . boolean ,
vol . Optional ( CONF_SSL_PROFILE , default = SSL_MODERN ) : vol . In (
[ SSL_INTERMEDIATE , SSL_MODERN ]
) ,
}
) ,
2019-07-31 19:25:30 +00:00
)
CONFIG_SCHEMA = vol . Schema ( { DOMAIN : HTTP_SCHEMA } , extra = vol . ALLOW_EXTRA )
2016-11-25 21:04:06 +00:00
2020-01-14 21:03:02 +00:00
@bind_hass
async def async_get_last_config ( hass : HomeAssistant ) - > Optional [ dict ] :
""" Return the last known working config. """
store = storage . Store ( hass , STORAGE_VERSION , STORAGE_KEY )
return cast ( Optional [ dict ] , await store . async_load ( ) )
2018-08-13 07:26:20 +00:00
class ApiConfig :
""" Configuration settings for API server. """
2019-07-31 19:25:30 +00:00
def __init__ (
2020-05-08 00:29:47 +00:00
self ,
local_ip : str ,
host : str ,
port : Optional [ int ] = SERVER_PORT ,
use_ssl : bool = False ,
2019-07-31 19:25:30 +00:00
) - > None :
2018-08-13 07:26:20 +00:00
""" Initialize a new API config object. """
2020-05-08 00:29:47 +00:00
self . local_ip = local_ip
2018-08-13 07:26:20 +00:00
self . host = host
self . port = port
2019-10-13 21:16:27 +00:00
self . use_ssl = use_ssl
2018-08-13 07:26:20 +00:00
2019-07-31 19:25:30 +00:00
host = host . rstrip ( " / " )
2018-08-13 07:26:20 +00:00
if host . startswith ( ( " http:// " , " https:// " ) ) :
2020-05-08 15:52:32 +00:00
self . deprecated_base_url = host
2018-08-13 07:26:20 +00:00
elif use_ssl :
2020-05-08 15:52:32 +00:00
self . deprecated_base_url = f " https:// { host } "
2018-08-13 07:26:20 +00:00
else :
2020-05-08 15:52:32 +00:00
self . deprecated_base_url = f " http:// { host } "
2018-08-13 07:26:20 +00:00
if port is not None :
2020-05-08 15:52:32 +00:00
self . deprecated_base_url + = f " : { port } "
@property
def base_url ( self ) - > str :
""" Proxy property to find caller of this deprecated property. """
found_frame = None
2020-06-01 18:44:45 +00:00
for frame in reversed ( extract_stack ( ) [ : - 1 ] ) :
2020-05-08 15:52:32 +00:00
for path in ( " custom_components/ " , " homeassistant/components/ " ) :
try :
index = frame . filename . index ( path )
# Skip webhook from the stack
if frame . filename [ index : ] . startswith (
" homeassistant/components/webhook/ "
) :
continue
found_frame = frame
break
except ValueError :
continue
if found_frame is not None :
break
# Did not source from an integration? Hard error.
if found_frame is None :
raise RuntimeError (
" Detected use of deprecated `base_url` property in the Home Assistant core. Please report this issue. "
)
# If a frame was found, it originated from an integration
if found_frame :
start = index + len ( path )
end = found_frame . filename . index ( " / " , start )
integration = found_frame . filename [ start : end ]
if path == " custom_components/ " :
extra = " to the custom component author "
else :
extra = " "
_LOGGER . warning (
2020-05-08 19:53:28 +00:00
" Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue %s for %s using this method at %s , line %s : %s " ,
2020-05-08 15:52:32 +00:00
extra ,
integration ,
found_frame . filename [ index : ] ,
found_frame . lineno ,
found_frame . line . strip ( ) ,
)
return self . deprecated_base_url
2018-08-13 07:26:20 +00:00
2018-03-09 01:51:49 +00:00
async def async_setup ( hass , config ) :
2016-11-25 21:04:06 +00:00
""" Set up the HTTP API and debug interface. """
conf = config . get ( DOMAIN )
if conf is None :
conf = HTTP_SCHEMA ( { } )
server_host = conf [ CONF_SERVER_HOST ]
server_port = conf [ CONF_SERVER_PORT ]
2018-02-17 09:29:14 +00:00
ssl_certificate = conf . get ( CONF_SSL_CERTIFICATE )
2018-06-26 15:44:08 +00:00
ssl_peer_certificate = conf . get ( CONF_SSL_PEER_CERTIFICATE )
2018-02-17 09:29:14 +00:00
ssl_key = conf . get ( CONF_SSL_KEY )
2016-11-25 21:04:06 +00:00
cors_origins = conf [ CONF_CORS_ORIGINS ]
2018-08-03 11:52:34 +00:00
use_x_forwarded_for = conf . get ( CONF_USE_X_FORWARDED_FOR , False )
trusted_proxies = conf . get ( CONF_TRUSTED_PROXIES , [ ] )
2016-11-25 21:04:06 +00:00
is_ban_enabled = conf [ CONF_IP_BAN_ENABLED ]
login_threshold = conf [ CONF_LOGIN_ATTEMPTS_THRESHOLD ]
2018-08-14 06:20:17 +00:00
ssl_profile = conf [ CONF_SSL_PROFILE ]
2016-11-25 21:04:06 +00:00
2018-02-15 21:06:14 +00:00
server = HomeAssistantHTTP (
2016-11-25 21:04:06 +00:00
hass ,
server_host = server_host ,
server_port = server_port ,
ssl_certificate = ssl_certificate ,
2018-06-26 15:44:08 +00:00
ssl_peer_certificate = ssl_peer_certificate ,
2016-11-25 21:04:06 +00:00
ssl_key = ssl_key ,
cors_origins = cors_origins ,
use_x_forwarded_for = use_x_forwarded_for ,
2018-06-29 20:27:06 +00:00
trusted_proxies = trusted_proxies ,
2016-11-25 21:04:06 +00:00
login_threshold = login_threshold ,
2018-08-14 06:20:17 +00:00
is_ban_enabled = is_ban_enabled ,
ssl_profile = ssl_profile ,
2016-11-25 21:04:06 +00:00
)
2020-06-02 18:54:11 +00:00
startup_listeners = [ ]
async def stop_server ( event : Event ) - > None :
2017-04-30 05:04:49 +00:00
""" Stop the server. """
2018-03-09 01:51:49 +00:00
await server . stop ( )
2016-11-25 21:04:06 +00:00
2020-06-02 18:54:11 +00:00
async def start_server ( event : Event ) - > None :
2017-04-30 05:04:49 +00:00
""" Start the server. """
2016-11-25 21:04:06 +00:00
2020-06-02 18:54:11 +00:00
for listener in startup_listeners :
listener ( )
2020-01-30 17:47:16 +00:00
2020-06-02 18:54:11 +00:00
hass . bus . async_listen_once ( EVENT_HOMEASSISTANT_STOP , stop_server )
2020-01-30 17:47:16 +00:00
2020-06-02 18:54:11 +00:00
await start_http_server_and_save_config ( hass , dict ( conf ) , server )
2020-01-14 21:03:02 +00:00
2020-06-02 22:02:09 +00:00
async def async_wait_frontend_load ( event : Event ) - > None :
""" Wait for the frontend to load. """
if event . data [ ATTR_COMPONENT ] != " frontend " :
return
await start_server ( event )
startup_listeners . append (
hass . bus . async_listen ( EVENT_COMPONENT_LOADED , async_wait_frontend_load )
)
2020-06-02 18:54:11 +00:00
startup_listeners . append (
hass . bus . async_listen ( EVENT_HOMEASSISTANT_START , start_server )
)
2016-11-25 21:04:06 +00:00
hass . http = server
2016-12-18 20:56:07 +00:00
host = conf . get ( CONF_BASE_URL )
2020-05-08 00:29:47 +00:00
local_ip = await hass . async_add_executor_job ( hass_util . get_local_ip )
2016-12-18 20:56:07 +00:00
if host :
2016-12-18 22:59:45 +00:00
port = None
2016-12-18 20:56:07 +00:00
elif server_host != DEFAULT_SERVER_HOST :
host = server_host
2016-12-18 22:59:45 +00:00
port = server_port
2016-12-18 20:56:07 +00:00
else :
2020-05-08 00:29:47 +00:00
host = local_ip
2016-12-18 22:59:45 +00:00
port = server_port
2016-12-18 20:56:07 +00:00
2020-05-08 00:29:47 +00:00
hass . config . api = ApiConfig ( local_ip , host , port , ssl_certificate is not None )
2016-11-25 21:04:06 +00:00
return True
2018-07-20 08:45:20 +00:00
class HomeAssistantHTTP :
2018-02-15 21:06:14 +00:00
""" HTTP server for Home Assistant. """
2016-11-25 21:04:06 +00:00
2019-07-31 19:25:30 +00:00
def __init__ (
self ,
hass ,
ssl_certificate ,
ssl_peer_certificate ,
ssl_key ,
server_host ,
server_port ,
cors_origins ,
use_x_forwarded_for ,
trusted_proxies ,
login_threshold ,
is_ban_enabled ,
ssl_profile ,
) :
2018-02-15 21:06:14 +00:00
""" Initialize the HTTP Home Assistant server. """
2020-01-09 10:09:34 +00:00
app = self . app = web . Application (
middlewares = [ ] , client_max_size = MAX_CLIENT_SIZE
)
2019-03-11 02:55:36 +00:00
app [ KEY_HASS ] = hass
2018-02-15 21:06:14 +00:00
# This order matters
2018-06-29 20:27:06 +00:00
setup_real_ip ( app , use_x_forwarded_for , trusted_proxies )
2016-11-25 21:04:06 +00:00
if is_ban_enabled :
2018-02-15 21:06:14 +00:00
setup_bans ( hass , app , login_threshold )
2019-03-11 02:55:36 +00:00
setup_auth ( hass , app )
2018-02-15 21:06:14 +00:00
2018-07-19 06:37:00 +00:00
setup_cors ( app , cors_origins )
2016-11-25 21:04:06 +00:00
self . hass = hass
self . ssl_certificate = ssl_certificate
2018-06-26 15:44:08 +00:00
self . ssl_peer_certificate = ssl_peer_certificate
2016-11-25 21:04:06 +00:00
self . ssl_key = ssl_key
self . server_host = server_host
self . server_port = server_port
2019-06-08 06:08:55 +00:00
self . trusted_proxies = trusted_proxies
2018-02-15 21:06:14 +00:00
self . is_ban_enabled = is_ban_enabled
2018-08-14 06:20:17 +00:00
self . ssl_profile = ssl_profile
2016-11-25 21:04:06 +00:00
self . _handler = None
2018-08-20 12:03:35 +00:00
self . runner = None
self . site = None
2016-11-25 21:04:06 +00:00
def register_view ( self , view ) :
""" Register a view with the WSGI server.
The view argument must be a class that inherits from HomeAssistantView .
It is optional to instantiate it before registering ; this method will
handle it either way .
"""
if isinstance ( view , type ) :
# Instantiate the view, if needed
view = view ( )
2019-07-31 19:25:30 +00:00
if not hasattr ( view , " url " ) :
2016-11-25 21:04:06 +00:00
class_name = view . __class__ . __name__
2019-08-23 16:53:33 +00:00
raise AttributeError ( f ' { class_name } missing required attribute " url " ' )
2016-11-25 21:04:06 +00:00
2019-07-31 19:25:30 +00:00
if not hasattr ( view , " name " ) :
2016-11-25 21:04:06 +00:00
class_name = view . __class__ . __name__
2019-08-23 16:53:33 +00:00
raise AttributeError ( f ' { class_name } missing required attribute " name " ' )
2016-11-25 21:04:06 +00:00
2018-07-19 06:37:00 +00:00
view . register ( self . app , self . app . router )
2016-11-25 21:04:06 +00:00
def register_redirect ( self , url , redirect_to ) :
""" Register a redirect with the server.
If given this must be either a string or callable . In case of a
callable it ' s called with the url adapter that triggered the match and
the values of the URL as keyword arguments and has to return the target
for the redirect , otherwise it has to be a string with placeholders in
rule syntax .
"""
2019-07-31 19:25:30 +00:00
2019-07-27 00:40:40 +00:00
async def redirect ( request ) :
2016-11-25 21:04:06 +00:00
""" Redirect to location. """
raise HTTPMovedPermanently ( redirect_to )
2019-07-31 19:25:30 +00:00
self . app . router . add_route ( " GET " , url , redirect )
2016-11-25 21:04:06 +00:00
2017-03-30 07:50:53 +00:00
def register_static_path ( self , url_path , path , cache_headers = True ) :
""" Register a folder or file to serve as a static path. """
2016-11-25 21:04:06 +00:00
if os . path . isdir ( path ) :
2017-03-30 07:50:53 +00:00
if cache_headers :
resource = CachingStaticResource
else :
resource = web . StaticResource
self . app . router . register_resource ( resource ( url_path , path ) )
return
2016-11-25 21:04:06 +00:00
2017-03-30 07:50:53 +00:00
if cache_headers :
2019-07-31 19:25:30 +00:00
2018-03-09 01:51:49 +00:00
async def serve_file ( request ) :
2017-03-30 07:50:53 +00:00
""" Serve file from disk. """
2019-02-15 17:31:54 +00:00
return web . FileResponse ( path , headers = CACHE_HEADERS )
2019-07-31 19:25:30 +00:00
2017-03-30 07:50:53 +00:00
else :
2019-07-31 19:25:30 +00:00
2018-03-09 01:51:49 +00:00
async def serve_file ( request ) :
2017-03-30 07:50:53 +00:00
""" Serve file from disk. """
return web . FileResponse ( path )
2016-11-25 21:04:06 +00:00
2019-07-31 19:25:30 +00:00
self . app . router . add_route ( " GET " , url_path , serve_file )
2016-11-25 21:04:06 +00:00
2018-03-09 01:51:49 +00:00
async def start ( self ) :
2018-08-20 12:03:35 +00:00
""" Start the aiohttp server. """
2016-11-25 21:04:06 +00:00
if self . ssl_certificate :
2016-12-13 06:02:24 +00:00
try :
2018-08-14 06:20:17 +00:00
if self . ssl_profile == SSL_INTERMEDIATE :
context = ssl_util . server_context_intermediate ( )
else :
context = ssl_util . server_context_modern ( )
2018-08-20 12:03:35 +00:00
await self . hass . async_add_executor_job (
2019-07-31 19:25:30 +00:00
context . load_cert_chain , self . ssl_certificate , self . ssl_key
)
2016-12-13 06:02:24 +00:00
except OSError as error :
2019-07-31 19:25:30 +00:00
_LOGGER . error (
" Could not read SSL certificate from %s : %s " ,
self . ssl_certificate ,
error ,
)
2016-12-13 06:02:24 +00:00
return
2018-06-26 15:44:08 +00:00
if self . ssl_peer_certificate :
context . verify_mode = ssl . CERT_REQUIRED
2018-08-20 12:03:35 +00:00
await self . hass . async_add_executor_job (
2019-07-31 19:25:30 +00:00
context . load_verify_locations , self . ssl_peer_certificate
)
2018-06-26 15:44:08 +00:00
2016-11-25 21:04:06 +00:00
else :
context = None
2016-11-27 22:01:12 +00:00
# Aiohttp freezes apps after start so that no changes can be made.
# However in Home Assistant components can be discovered after boot.
# This will now raise a RunTimeError.
2018-03-05 21:28:41 +00:00
# To work around this we now prevent the router from getting frozen
2018-11-21 19:55:21 +00:00
# pylint: disable=protected-access
2018-03-05 21:28:41 +00:00
self . app . _router . freeze = lambda : None
2016-11-27 22:01:12 +00:00
2018-08-20 12:03:35 +00:00
self . runner = web . AppRunner ( self . app )
await self . runner . setup ( )
2019-07-31 19:25:30 +00:00
self . site = web . TCPSite (
self . runner , self . server_host , self . server_port , ssl_context = context
)
2016-12-13 06:02:24 +00:00
try :
2018-08-20 12:03:35 +00:00
await self . site . start ( )
2016-12-13 06:02:24 +00:00
except OSError as error :
2019-07-31 19:25:30 +00:00
_LOGGER . error (
" Failed to create HTTP server at port %d : %s " , self . server_port , error
)
2016-11-25 21:04:06 +00:00
2018-03-09 01:51:49 +00:00
async def stop ( self ) :
2018-08-20 12:03:35 +00:00
""" Stop the aiohttp server. """
await self . site . stop ( )
await self . runner . cleanup ( )
2020-06-02 18:54:11 +00:00
async def start_http_server_and_save_config (
hass : HomeAssistant , conf : Dict , server : HomeAssistantHTTP
) - > None :
""" Startup the http server and save the config. """
await server . start ( ) # type: ignore
# If we are set up successful, we store the HTTP settings for safe mode.
store = storage . Store ( hass , STORAGE_VERSION , STORAGE_KEY )
if CONF_TRUSTED_PROXIES in conf :
conf [ CONF_TRUSTED_PROXIES ] = [
str ( ip . network_address ) for ip in conf [ CONF_TRUSTED_PROXIES ]
]
await store . async_save ( conf )