1
mirror of https://github.com/home-assistant/core synced 2024-10-07 10:13:38 +02:00
ha-core/homeassistant/helpers/network.py
2020-11-27 08:55:34 +01:00

300 lines
9.4 KiB
Python

"""Network helpers."""
from ipaddress import ip_address
from typing import Optional, cast
import yarl
from homeassistant.components import http
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass
from homeassistant.util.network import (
is_ip_address,
is_local,
is_loopback,
is_private,
normalize_url,
)
TYPE_URL_INTERNAL = "internal_url"
TYPE_URL_EXTERNAL = "external_url"
class NoURLAvailableError(HomeAssistantError):
"""An URL to the Home Assistant instance is not available."""
@bind_hass
def is_internal_request(hass: HomeAssistant) -> bool:
"""Test if the current request is internal."""
try:
_get_internal_url(hass, require_current_request=True)
return True
except NoURLAvailableError:
return False
@bind_hass
def get_url(
hass: HomeAssistant,
*,
require_current_request: bool = False,
require_ssl: bool = False,
require_standard_port: bool = False,
allow_internal: bool = True,
allow_external: bool = True,
allow_cloud: bool = True,
allow_ip: bool = True,
prefer_external: bool = False,
prefer_cloud: bool = False,
) -> str:
"""Get a URL to this instance."""
if require_current_request and http.current_request.get() is None:
raise NoURLAvailableError
order = [TYPE_URL_INTERNAL, TYPE_URL_EXTERNAL]
if prefer_external:
order.reverse()
# Try finding an URL in the order specified
for url_type in order:
if allow_internal and url_type == TYPE_URL_INTERNAL:
try:
return _get_internal_url(
hass,
allow_ip=allow_ip,
require_current_request=require_current_request,
require_ssl=require_ssl,
require_standard_port=require_standard_port,
)
except NoURLAvailableError:
pass
if allow_external and url_type == TYPE_URL_EXTERNAL:
try:
return _get_external_url(
hass,
allow_cloud=allow_cloud,
allow_ip=allow_ip,
prefer_cloud=prefer_cloud,
require_current_request=require_current_request,
require_ssl=require_ssl,
require_standard_port=require_standard_port,
)
except NoURLAvailableError:
pass
# For current request, we accept loopback interfaces (e.g., 127.0.0.1),
# the Supervisor hostname and localhost transparently
request_host = _get_request_host()
if (
require_current_request
and request_host is not None
and hass.config.api is not None
):
scheme = "https" if hass.config.api.use_ssl else "http"
current_url = yarl.URL.build(
scheme=scheme, host=request_host, port=hass.config.api.port
)
known_hostnames = ["localhost"]
if hass.components.hassio.is_hassio():
host_info = hass.components.hassio.get_host_info()
known_hostnames.extend(
[host_info["hostname"], f"{host_info['hostname']}.local"]
)
if (
(
(
allow_ip
and is_ip_address(request_host)
and is_loopback(ip_address(request_host))
)
or request_host in known_hostnames
)
and (not require_ssl or current_url.scheme == "https")
and (not require_standard_port or current_url.is_default_port())
):
return normalize_url(str(current_url))
# We have to be honest now, we have no viable option available
raise NoURLAvailableError
def _get_request_host() -> Optional[str]:
"""Get the host address of the current request."""
request = http.current_request.get()
if request is None:
raise NoURLAvailableError
return yarl.URL(request.url).host
@bind_hass
def _get_internal_url(
hass: HomeAssistant,
*,
allow_ip: bool = True,
require_current_request: bool = False,
require_ssl: bool = False,
require_standard_port: bool = False,
) -> str:
"""Get internal URL of this instance."""
if hass.config.internal_url:
internal_url = yarl.URL(hass.config.internal_url)
if (
(not require_current_request or internal_url.host == _get_request_host())
and (not require_ssl or internal_url.scheme == "https")
and (not require_standard_port or internal_url.is_default_port())
and (allow_ip or not is_ip_address(str(internal_url.host)))
):
return normalize_url(str(internal_url))
# Fallback to old base_url
try:
return _get_deprecated_base_url(
hass,
internal=True,
allow_ip=allow_ip,
require_current_request=require_current_request,
require_ssl=require_ssl,
require_standard_port=require_standard_port,
)
except NoURLAvailableError:
pass
# Fallback to detected local IP
if allow_ip and not (
require_ssl or hass.config.api is None or hass.config.api.use_ssl
):
ip_url = yarl.URL.build(
scheme="http", host=hass.config.api.local_ip, port=hass.config.api.port
)
if (
not is_loopback(ip_address(ip_url.host))
and (not require_current_request or ip_url.host == _get_request_host())
and (not require_standard_port or ip_url.is_default_port())
):
return normalize_url(str(ip_url))
raise NoURLAvailableError
@bind_hass
def _get_external_url(
hass: HomeAssistant,
*,
allow_cloud: bool = True,
allow_ip: bool = True,
prefer_cloud: bool = False,
require_current_request: bool = False,
require_ssl: bool = False,
require_standard_port: bool = False,
) -> str:
"""Get external URL of this instance."""
if prefer_cloud and allow_cloud:
try:
return _get_cloud_url(hass)
except NoURLAvailableError:
pass
if hass.config.external_url:
external_url = yarl.URL(hass.config.external_url)
if (
(allow_ip or not is_ip_address(str(external_url.host)))
and (
not require_current_request or external_url.host == _get_request_host()
)
and (not require_standard_port or external_url.is_default_port())
and (
not require_ssl
or (
external_url.scheme == "https"
and not is_ip_address(str(external_url.host))
)
)
):
return normalize_url(str(external_url))
try:
return _get_deprecated_base_url(
hass,
allow_ip=allow_ip,
require_current_request=require_current_request,
require_ssl=require_ssl,
require_standard_port=require_standard_port,
)
except NoURLAvailableError:
pass
if allow_cloud:
try:
return _get_cloud_url(hass, require_current_request=require_current_request)
except NoURLAvailableError:
pass
raise NoURLAvailableError
@bind_hass
def _get_cloud_url(hass: HomeAssistant, require_current_request: bool = False) -> str:
"""Get external Home Assistant Cloud URL of this instance."""
if "cloud" in hass.config.components:
try:
cloud_url = yarl.URL(cast(str, hass.components.cloud.async_remote_ui_url()))
except hass.components.cloud.CloudNotAvailable as err:
raise NoURLAvailableError from err
if not require_current_request or cloud_url.host == _get_request_host():
return normalize_url(str(cloud_url))
raise NoURLAvailableError
@bind_hass
def _get_deprecated_base_url(
hass: HomeAssistant,
*,
internal: bool = False,
allow_ip: bool = True,
require_current_request: bool = False,
require_ssl: bool = False,
require_standard_port: bool = False,
) -> str:
"""Work with the deprecated `base_url`, used as fallback."""
if hass.config.api is None or not hass.config.api.deprecated_base_url:
raise NoURLAvailableError
base_url = yarl.URL(hass.config.api.deprecated_base_url)
# Rules that apply to both internal and external
if (
(allow_ip or not is_ip_address(str(base_url.host)))
and (not require_current_request or base_url.host == _get_request_host())
and (not require_ssl or base_url.scheme == "https")
and (not require_standard_port or base_url.is_default_port())
):
# Check to ensure an internal URL
if internal and (
str(base_url.host).endswith(".local")
or (
is_ip_address(str(base_url.host))
and not is_loopback(ip_address(base_url.host))
and is_private(ip_address(base_url.host))
)
):
return normalize_url(str(base_url))
# Check to ensure an external URL (a little)
if (
not internal
and not str(base_url.host).endswith(".local")
and not (
is_ip_address(str(base_url.host))
and is_local(ip_address(str(base_url.host)))
)
):
return normalize_url(str(base_url))
raise NoURLAvailableError