1 """Helpers to resolve client ID/secret."""
3 from __future__
import annotations
5 from html.parser
import HTMLParser
6 from ipaddress
import ip_address
8 from urllib.parse
import ParseResult, urljoin, urlparse
11 import aiohttp.client_exceptions
16 _LOGGER = logging.getLogger(__name__)
20 hass: HomeAssistant, client_id: str, redirect_uri: str
22 """Verify that the client and redirect uri match."""
32 client_id_parts.scheme == redirect_parts.scheme
33 and client_id_parts.netloc == redirect_parts.netloc
42 client_id ==
"https://home-assistant.io/iOS"
43 and redirect_uri ==
"homeassistant://auth-callback"
47 if client_id ==
"https://home-assistant.io/android" and redirect_uri
in (
48 "homeassistant://auth-callback",
49 "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android",
50 "https://wear.googleapis-cn.com/3p_auth/io.homeassistant.companion.android",
57 return redirect_uri
in redirect_uris
61 """Parser to find link tags."""
64 """Initialize a link tag parser."""
67 self.found: list[str |
None] = []
70 """Handle finding a start tag."""
74 attributes: dict[str, str |
None] =
dict(attrs)
76 if attributes.get(
"rel") == self.
relrel:
77 self.found.append(attributes.get(
"href"))
81 """Find link tag with redirect_uri values.
85 The client SHOULD publish one or more <link> tags or Link HTTP headers with
86 a rel attribute of redirect_uri at the client_id URL.
88 We limit to the first 10kB of the page.
90 We do not implement extracting redirect uris from headers.
96 aiohttp.ClientSession()
as session,
97 session.get(url, timeout=aiohttp.ClientTimeout(total=5))
as resp,
99 async
for data
in resp.content.iter_chunked(1024):
100 parser.feed(data.decode())
107 _LOGGER.error(
"Timeout while looking up redirect_uri %s", url)
108 except aiohttp.client_exceptions.ClientSSLError:
109 _LOGGER.error(
"SSL error while looking up redirect_uri %s", url)
110 except aiohttp.client_exceptions.ClientOSError
as ex:
111 _LOGGER.error(
"OS error while looking up redirect_uri %s: %s", url, ex.strerror)
112 except aiohttp.client_exceptions.ClientConnectionError:
114 "Low level connection error while looking up redirect_uri %s", url
116 except aiohttp.client_exceptions.ClientError:
117 _LOGGER.error(
"Unknown error while looking up redirect_uri %s", url)
123 return [urljoin(url, found)
for found
in parser.found]
127 """Verify that the client id is valid."""
136 """Parse a url in parts and canonicalize according to IndieAuth."""
137 parts = urlparse(url)
142 parts = parts._replace(netloc=parts.netloc.lower())
147 parts = parts._replace(path=
"/")
153 """Test if client id is a valid URL according to IndieAuth section 3.2.
155 https://indieauth.spec.indieweb.org/#client-identifier
161 if parts.scheme
not in (
"http",
"https"):
168 if any(segment
in (
".",
"..")
for segment
in parts.path.split(
"/")):
170 "Client ID cannot contain single-dot or double-dot path segments"
174 if parts.fragment !=
"":
175 raise ValueError(
"Client ID cannot contain a fragment")
178 if parts.username
is not None:
179 raise ValueError(
"Client ID cannot contain username")
181 if parts.password
is not None:
182 raise ValueError(
"Client ID cannot contain password")
188 except ValueError
as ex:
189 raise ValueError(
"Client ID contains invalid port")
from ex
202 netloc = parts.netloc
205 if netloc[0] ==
"[" and netloc[-1] ==
"]":
206 netloc = netloc[1:-1]
208 address = ip_address(netloc)
213 if address
is None or is_local(address):
216 raise ValueError(
"Hostname should be a domain name or local IP address")
None handle_starttag(self, str tag, list[tuple[str, str|None]] attrs)
None __init__(self, str rel)
ParseResult _parse_client_id(str client_id)
bool verify_redirect_uri(HomeAssistant hass, str client_id, str redirect_uri)
bool verify_client_id(str client_id)
ParseResult _parse_url(str url)
list[str] fetch_redirect_uris(HomeAssistant hass, str url)
bool is_local(ConfigEntry entry)