168 lines
5.1 KiB
Python
168 lines
5.1 KiB
Python
import base64
|
|
from homeassistant.components.frontend import EVENT_PANELS_UPDATED # type: ignore
|
|
import os
|
|
import re
|
|
import time
|
|
from typing import TYPE_CHECKING
|
|
|
|
from .const import LOGGER as _LOGGER
|
|
|
|
if TYPE_CHECKING:
|
|
from homeassistant.core import HomeAssistant
|
|
from typing import TypedDict
|
|
from yarl import URL
|
|
|
|
from .const import WorkMode, RewriteMode
|
|
|
|
class Token(TypedDict):
|
|
value: str
|
|
expire: int
|
|
|
|
class UserInfo(TypedDict):
|
|
id: str
|
|
name: str | None
|
|
username: str | None
|
|
|
|
|
|
class RewriteCfg:
|
|
mode: "RewriteMode"
|
|
path = ""
|
|
name = ""
|
|
match: str
|
|
replace: str
|
|
|
|
def __init__(self, **kwargs):
|
|
self.__dict__.update(
|
|
(k, v) for k, v in kwargs.items() if v is not None and v != getattr(self, k, None)
|
|
)
|
|
|
|
|
|
class IngressCfg:
|
|
mode: "WorkMode"
|
|
name: str
|
|
entry: str
|
|
origin: "URL"
|
|
sub_path = ""
|
|
headers = {}
|
|
cookie_name = "ingress_token"
|
|
_cookie_name_re = None
|
|
expire_time = 3600
|
|
static_token = ""
|
|
rewrites: list[RewriteCfg] = []
|
|
sub_apps: list["IngressCfg"] = []
|
|
|
|
def __init__(self, **kwargs):
|
|
self.__dict__.update(
|
|
(k, v) for k, v in kwargs.items() if v is not None and v != getattr(self, k, None)
|
|
)
|
|
self.token: "Token" = {"value": "", "expire": 0}
|
|
|
|
def remove_token_from_cookie(self, cookie):
|
|
if (cookie_re := self._cookie_name_re) is None:
|
|
cookie_re = _init_cookie_name_re(self.cookie_name)
|
|
self._cookie_name_re = cookie_re
|
|
cookie = cookie_re.sub("", cookie).strip("; ")
|
|
return USER_TOKEN_COOKIE_RE.sub("", cookie).strip("; ")
|
|
|
|
|
|
class IngressStore:
|
|
"""ingress configs."""
|
|
|
|
def __init__(self):
|
|
self._configs: dict[str, IngressCfg] = {}
|
|
self._tokens: dict[str, IngressCfg] = {}
|
|
self._user_tokens: dict[str, dict] = {}
|
|
|
|
def clear(self):
|
|
self._configs.clear()
|
|
self._tokens.clear()
|
|
self._user_tokens.clear()
|
|
|
|
def get(self, name: str) -> IngressCfg | None:
|
|
return self._configs.get(name)
|
|
|
|
def del_ingress(self, name: str) -> IngressCfg | None:
|
|
if cfg := self._configs.pop(name, None):
|
|
self._tokens.pop(cfg.token["value"], None)
|
|
return cfg
|
|
|
|
def add_ingress(self, cfg: IngressCfg, now: int):
|
|
self.del_ingress(cfg.name)
|
|
cfg.token["value"] = ""
|
|
self.new_token(cfg, now)
|
|
self._configs[cfg.name] = cfg
|
|
|
|
def new_token(self, cfg: IngressCfg, now: int) -> str:
|
|
if cfg.static_token:
|
|
token = f"t-{cfg.static_token}"
|
|
old = self._tokens.get(token)
|
|
if old and old is not cfg:
|
|
_LOGGER.error(
|
|
"static_token conflict with %s, use dynamic for %s!", old.name, cfg.name
|
|
)
|
|
del cfg.static_token
|
|
while not cfg.static_token:
|
|
token = base64.urlsafe_b64encode(os.urandom(33)).decode()
|
|
if token not in self._tokens:
|
|
break
|
|
tkcfg = cfg.token
|
|
self._tokens.pop(tkcfg.get("value"), None)
|
|
self._tokens[token] = cfg
|
|
tkcfg["value"] = token
|
|
tkcfg["expire"] = now + cfg.expire_time
|
|
return token
|
|
|
|
def check_token(
|
|
self, hass: "HomeAssistant", token: str, refresh=True
|
|
) -> tuple[IngressCfg | None, str]:
|
|
cfg = self._tokens.get(token)
|
|
if cfg and refresh:
|
|
# token valid, check refresh
|
|
now = int(time.time())
|
|
if now >= cfg.token["expire"]:
|
|
token = self.new_token(cfg, now)
|
|
hass.bus.async_fire(EVENT_PANELS_UPDATED)
|
|
return cfg, token
|
|
|
|
def generate_user_token(self, user_info: dict) -> str:
|
|
"""Generate a token associated with user info."""
|
|
now = int(time.time())
|
|
for token in [k for k, v in self._user_tokens.items() if now >= v["expire"]]:
|
|
self._user_tokens.pop(token, None)
|
|
while True:
|
|
token = base64.urlsafe_b64encode(os.urandom(33)).decode()
|
|
if token not in self._user_tokens:
|
|
break
|
|
self._user_tokens[token] = {"user_info": user_info, "expire": now + USER_TOKEN_VALIDITY}
|
|
return token
|
|
|
|
def check_user_token(self, token: str) -> "UserInfo | None":
|
|
"""Check if a token is valid and return associated user info."""
|
|
entry = self._user_tokens.get(token)
|
|
if not entry:
|
|
return None
|
|
now = int(time.time())
|
|
if now >= entry["expire"]:
|
|
# Token expired, remove it
|
|
self._user_tokens.pop(token, None)
|
|
return None
|
|
# Refresh expiration
|
|
entry["expire"] = now + USER_TOKEN_VALIDITY
|
|
return entry["user_info"]
|
|
|
|
def user_cookie_name(self) -> str:
|
|
return USER_TOKEN_COOKIE_NAME
|
|
|
|
def cookie_name(self, name: str) -> str:
|
|
cfg = self._configs.get(name)
|
|
return cfg.cookie_name if cfg else IngressCfg.cookie_name
|
|
|
|
|
|
def _init_cookie_name_re(cookie_name):
|
|
return re.compile(rf"(?:^|;\s*){re.escape(cookie_name)}=[^;]*(?=;|$)")
|
|
|
|
|
|
USER_TOKEN_VALIDITY = 300
|
|
USER_TOKEN_COOKIE_NAME = "ha_ingress_session"
|
|
USER_TOKEN_COOKIE_RE = _init_cookie_name_re(USER_TOKEN_COOKIE_NAME)
|