Init ingress session boarder / lookup (#995)

* Init ingress session boarder / lookup

* Add session to API

* Add cokkie validate

* Do it without event bus

* Add logger

* Fix validation

* Add tests

* Update tests

* Mock json storage
This commit is contained in:
Pascal Vizeli 2019-04-05 17:36:07 +02:00 committed by GitHub
parent 1edec61133
commit 67f562a846
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 325 additions and 105 deletions

11
API.md
View File

@ -588,9 +588,20 @@ Write data to add-on stdin
### ingress ### ingress
- POST `/ingress/session`
Create a new Session for access to ingress service.
```json
{
"session": "token"
}
```
- VIEW `/ingress/{token}` - VIEW `/ingress/{token}`
Ingress WebUI for this Add-on. The addon need support HASS Auth! Ingress WebUI for this Add-on. The addon need support HASS Auth!
Need ingress session as cookie.
### discovery ### discovery

View File

@ -6,6 +6,7 @@ from ipaddress import IPv4Address, ip_address
import logging import logging
from pathlib import Path, PurePath from pathlib import Path, PurePath
import re import re
import secrets
import shutil import shutil
import tarfile import tarfile
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
@ -89,7 +90,6 @@ from ..exceptions import (
HostAppArmorError, HostAppArmorError,
JsonFileError, JsonFileError,
) )
from ..utils import create_token
from ..utils.apparmor import adjust_profile from ..utils.apparmor import adjust_profile
from ..utils.json import read_json_file, write_json_file from ..utils.json import read_json_file, write_json_file
from .utils import check_installed, remove_data from .utils import check_installed, remove_data
@ -896,7 +896,7 @@ class Addon(CoreSysAttributes):
return return
# Access Token # Access Token
self._data.user[self._id][ATTR_ACCESS_TOKEN] = create_token() self._data.user[self._id][ATTR_ACCESS_TOKEN] = secrets.token_hex(56)
self.save_data() self.save_data()
# Options # Options

View File

@ -194,6 +194,7 @@ class RestAPI(CoreSysAttributes):
api_ingress.coresys = self.coresys api_ingress.coresys = self.coresys
self.webapp.add_routes([ self.webapp.add_routes([
web.post('/ingress/session', api_ingress.create_session),
web.view('/ingress/{token}/{path:.*}', api_ingress.handler), web.view('/ingress/{token}/{path:.*}', api_ingress.handler),
]) ])

View File

@ -2,7 +2,7 @@
import asyncio import asyncio
from ipaddress import ip_address from ipaddress import ip_address
import logging import logging
from typing import Dict, Union from typing import Any, Dict, Union
import aiohttp import aiohttp
from aiohttp import hdrs, web from aiohttp import hdrs, web
@ -14,8 +14,9 @@ from aiohttp.web_exceptions import (
from multidict import CIMultiDict, istr from multidict import CIMultiDict, istr
from ..addons.addon import Addon from ..addons.addon import Addon
from ..const import HEADER_TOKEN, REQUEST_FROM from ..const import ATTR_SESSION, HEADER_TOKEN, REQUEST_FROM, COOKIE_INGRESS
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from .utils import api_process
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -28,34 +29,45 @@ class APIIngress(CoreSysAttributes):
token = request.match_info.get("token") token = request.match_info.get("token")
# Find correct add-on # Find correct add-on
for addon in self.sys_addons.list_installed: addon = self.sys_ingress.get(token)
if addon.ingress_token != token: if not addon:
continue _LOGGER.warning("Ingress for %s not available", token)
return addon raise HTTPServiceUnavailable()
_LOGGER.warning("Ingress for %s not available", token) return addon
raise HTTPServiceUnavailable()
def _check_ha_access(self, request: web.Request) -> None:
if request[REQUEST_FROM] != self.sys_homeassistant:
_LOGGER.warning("Ingress is only available behind Home Assistant")
raise HTTPUnauthorized()
def _create_url(self, addon: Addon, path: str) -> str: def _create_url(self, addon: Addon, path: str) -> str:
"""Create URL to container.""" """Create URL to container."""
return f"{addon.ingress_internal}/{path}" return f"{addon.ingress_internal}/{path}"
@api_process
async def create_session(self, request: web.Request) -> Dict[str, Any]:
"""Create a new session."""
self._check_ha_access(request)
session = self.sys_ingress.create_session()
return {ATTR_SESSION: session}
async def handler( async def handler(
self, request: web.Request self, request: web.Request
) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]:
"""Route data to Hass.io ingress service.""" """Route data to Hass.io ingress service."""
addon = self._extract_addon(request) self._check_ha_access(request)
path = request.match_info.get("path")
# Only Home Assistant call this # Check Ingress Session
if request[REQUEST_FROM] != self.sys_homeassistant: session = request.cookies.get(COOKIE_INGRESS)
_LOGGER.warning("Ingress is only available behind Home Assistant") if not self.sys_ingress.validate_session(session):
_LOGGER.warning("No valid ingress session %s", session)
raise HTTPUnauthorized() raise HTTPUnauthorized()
if not addon.with_ingress:
_LOGGER.warning("Add-on %s don't support ingress feature", addon.slug)
raise HTTPBadGateway()
# Process requests # Process requests
addon = self._extract_addon(request)
path = request.match_info.get("path")
try: try:
# Websocket # Websocket
if _is_websocket(request): if _is_websocket(request):

View File

@ -19,6 +19,7 @@ from .discovery import Discovery
from .hassos import HassOS from .hassos import HassOS
from .homeassistant import HomeAssistant from .homeassistant import HomeAssistant
from .host import HostManager from .host import HostManager
from .ingress import Ingress
from .services import ServiceManager from .services import ServiceManager
from .snapshots import SnapshotManager from .snapshots import SnapshotManager
from .supervisor import Supervisor from .supervisor import Supervisor
@ -49,6 +50,7 @@ async def initialize_coresys():
coresys.addons = AddonManager(coresys) coresys.addons = AddonManager(coresys)
coresys.snapshots = SnapshotManager(coresys) coresys.snapshots = SnapshotManager(coresys)
coresys.host = HostManager(coresys) coresys.host = HostManager(coresys)
coresys.ingress = Ingress(coresys)
coresys.tasks = Tasks(coresys) coresys.tasks = Tasks(coresys)
coresys.services = ServiceManager(coresys) coresys.services = ServiceManager(coresys)
coresys.discovery = Discovery(coresys) coresys.discovery = Discovery(coresys)
@ -71,8 +73,9 @@ def initialize_system_data(coresys):
# Home Assistant configuration folder # Home Assistant configuration folder
if not config.path_homeassistant.is_dir(): if not config.path_homeassistant.is_dir():
_LOGGER.info("Create Home Assistant configuration folder %s", _LOGGER.info(
config.path_homeassistant) "Create Home Assistant configuration folder %s", config.path_homeassistant
)
config.path_homeassistant.mkdir() config.path_homeassistant.mkdir()
# hassio ssl folder # hassio ssl folder
@ -82,18 +85,19 @@ def initialize_system_data(coresys):
# hassio addon data folder # hassio addon data folder
if not config.path_addons_data.is_dir(): if not config.path_addons_data.is_dir():
_LOGGER.info("Create Hass.io Add-on data folder %s", _LOGGER.info("Create Hass.io Add-on data folder %s", config.path_addons_data)
config.path_addons_data)
config.path_addons_data.mkdir(parents=True) config.path_addons_data.mkdir(parents=True)
if not config.path_addons_local.is_dir(): if not config.path_addons_local.is_dir():
_LOGGER.info("Create Hass.io Add-on local repository folder %s", _LOGGER.info(
config.path_addons_local) "Create Hass.io Add-on local repository folder %s", config.path_addons_local
)
config.path_addons_local.mkdir(parents=True) config.path_addons_local.mkdir(parents=True)
if not config.path_addons_git.is_dir(): if not config.path_addons_git.is_dir():
_LOGGER.info("Create Hass.io Add-on git repositories folder %s", _LOGGER.info(
config.path_addons_git) "Create Hass.io Add-on git repositories folder %s", config.path_addons_git
)
config.path_addons_git.mkdir(parents=True) config.path_addons_git.mkdir(parents=True)
# hassio tmp folder # hassio tmp folder
@ -154,7 +158,8 @@ def initialize_logging():
"ERROR": "red", "ERROR": "red",
"CRITICAL": "red", "CRITICAL": "red",
}, },
)) )
)
def check_environment(): def check_environment():
@ -188,19 +193,16 @@ def check_environment():
def reg_signal(loop): def reg_signal(loop):
"""Register SIGTERM and SIGKILL to stop system.""" """Register SIGTERM and SIGKILL to stop system."""
try: try:
loop.add_signal_handler(signal.SIGTERM, loop.add_signal_handler(signal.SIGTERM, lambda: loop.call_soon(loop.stop))
lambda: loop.call_soon(loop.stop))
except (ValueError, RuntimeError): except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGTERM") _LOGGER.warning("Could not bind to SIGTERM")
try: try:
loop.add_signal_handler(signal.SIGHUP, loop.add_signal_handler(signal.SIGHUP, lambda: loop.call_soon(loop.stop))
lambda: loop.call_soon(loop.stop))
except (ValueError, RuntimeError): except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGHUP") _LOGGER.warning("Could not bind to SIGHUP")
try: try:
loop.add_signal_handler(signal.SIGINT, loop.add_signal_handler(signal.SIGINT, lambda: loop.call_soon(loop.stop))
lambda: loop.call_soon(loop.stop))
except (ValueError, RuntimeError): except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGINT") _LOGGER.warning("Could not bind to SIGINT")

View File

@ -23,6 +23,7 @@ FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json") FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json")
FILE_HASSIO_SERVICES = Path(HASSIO_DATA, "services.json") FILE_HASSIO_SERVICES = Path(HASSIO_DATA, "services.json")
FILE_HASSIO_DISCOVERY = Path(HASSIO_DATA, "discovery.json") FILE_HASSIO_DISCOVERY = Path(HASSIO_DATA, "discovery.json")
FILE_HASSIO_INGRESS = Path(HASSIO_DATA, "ingress.json")
SOCKET_DOCKER = Path("/var/run/docker.sock") SOCKET_DOCKER = Path("/var/run/docker.sock")
@ -54,6 +55,7 @@ CONTENT_TYPE_TAR = "application/tar"
CONTENT_TYPE_URL = "application/x-www-form-urlencoded" CONTENT_TYPE_URL = "application/x-www-form-urlencoded"
HEADER_HA_ACCESS = "X-Ha-Access" HEADER_HA_ACCESS = "X-Ha-Access"
HEADER_TOKEN = "X-Hassio-Key" HEADER_TOKEN = "X-Hassio-Key"
COOKIE_INGRESS = "ingress_session"
ENV_TOKEN = "HASSIO_TOKEN" ENV_TOKEN = "HASSIO_TOKEN"
ENV_TIME = "TZ" ENV_TIME = "TZ"
@ -194,6 +196,7 @@ ATTR_INGRESS_ENTRY = "ingress_entry"
ATTR_INGRESS_TOKEN = "ingress_token" ATTR_INGRESS_TOKEN = "ingress_token"
ATTR_INGRESS_URL = "ingress_url" ATTR_INGRESS_URL = "ingress_url"
ATTR_IP_ADDRESS = "ip_address" ATTR_IP_ADDRESS = "ip_address"
ATTR_SESSION = "session"
PROVIDE_SERVICE = "provide" PROVIDE_SERVICE = "provide"
NEED_SERVICE = "need" NEED_SERVICE = "need"

View File

@ -62,6 +62,9 @@ class HassIO(CoreSysAttributes):
# Load discovery # Load discovery
await self.sys_discovery.load() await self.sys_discovery.load()
# Load ingress
await self.sys_ingress.load()
# start dns forwarding # start dns forwarding
self.sys_create_task(self.sys_dns.start()) self.sys_create_task(self.sys_dns.start())
@ -131,6 +134,7 @@ class HassIO(CoreSysAttributes):
self.sys_dns.stop(), self.sys_dns.stop(),
self.sys_websession.close(), self.sys_websession.close(),
self.sys_websession_ssl.close(), self.sys_websession_ssl.close(),
self.sys_ingress.unload(),
] ]
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:

View File

@ -23,6 +23,7 @@ if TYPE_CHECKING:
from .hassos import HassOS from .hassos import HassOS
from .homeassistant import HomeAssistant from .homeassistant import HomeAssistant
from .host import HostManager from .host import HostManager
from .ingress import Ingress
from .services import ServiceManager from .services import ServiceManager
from .snapshots import SnapshotManager from .snapshots import SnapshotManager
from .supervisor import Supervisor from .supervisor import Supervisor
@ -63,6 +64,7 @@ class CoreSys:
self._snapshots: SnapshotManager = None self._snapshots: SnapshotManager = None
self._tasks: Tasks = None self._tasks: Tasks = None
self._host: HostManager = None self._host: HostManager = None
self._ingress: Ingress = None
self._dbus: DBusManager = None self._dbus: DBusManager = None
self._hassos: HassOS = None self._hassos: HassOS = None
self._services: ServiceManager = None self._services: ServiceManager = None
@ -293,6 +295,18 @@ class CoreSys:
raise RuntimeError("HostManager already set!") raise RuntimeError("HostManager already set!")
self._host = value self._host = value
@property
def ingress(self) -> Ingress:
"""Return Ingress object."""
return self._ingress
@ingress.setter
def ingress(self, value: Ingress):
"""Set a Ingress object."""
if self._ingress:
raise RuntimeError("Ingress already set!")
self._ingress = value
@property @property
def hassos(self) -> HassOS: def hassos(self) -> HassOS:
"""Return HassOS object.""" """Return HassOS object."""
@ -441,6 +455,11 @@ class CoreSysAttributes:
"""Return HostManager object.""" """Return HostManager object."""
return self.coresys.host return self.coresys.host
@property
def sys_ingress(self) -> Ingress:
"""Return Ingress object."""
return self.coresys.ingress
@property @property
def sys_hassos(self) -> HassOS: def sys_hassos(self) -> HassOS:
"""Return HassOS object.""" """Return HassOS object."""

View File

@ -7,6 +7,7 @@ import logging
import os import os
from pathlib import Path from pathlib import Path
import re import re
import secrets
import socket import socket
import time import time
from typing import Any, AsyncContextManager, Awaitable, Dict, Optional from typing import Any, AsyncContextManager, Awaitable, Dict, Optional
@ -35,13 +36,13 @@ from .coresys import CoreSys, CoreSysAttributes
from .docker.homeassistant import DockerHomeAssistant from .docker.homeassistant import DockerHomeAssistant
from .docker.stats import DockerStats from .docker.stats import DockerStats
from .exceptions import ( from .exceptions import (
DockerAPIError,
HomeAssistantAPIError, HomeAssistantAPIError,
HomeAssistantAuthError, HomeAssistantAuthError,
HomeAssistantError, HomeAssistantError,
HomeAssistantUpdateError, HomeAssistantUpdateError,
DockerAPIError
) )
from .utils import convert_to_ascii, create_token, process_lock from .utils import convert_to_ascii, process_lock
from .utils.json import JsonConfig from .utils.json import JsonConfig
from .validate import SCHEMA_HASS_CONFIG from .validate import SCHEMA_HASS_CONFIG
@ -314,7 +315,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
return return
# Create new API token # Create new API token
self._data[ATTR_ACCESS_TOKEN] = create_token() self._data[ATTR_ACCESS_TOKEN] = secrets.token_hex(56)
self.save_data() self.save_data()
try: try:

103
hassio/ingress.py Normal file
View File

@ -0,0 +1,103 @@
"""Fetch last versions from webserver."""
from datetime import timedelta
import logging
from typing import Dict, Optional
import secrets
from .addons.addon import Addon
from .const import ATTR_SESSION, FILE_HASSIO_INGRESS
from .coresys import CoreSys, CoreSysAttributes
from .utils.json import JsonConfig
from .utils.dt import utcnow, utc_from_timestamp
from .validate import SCHEMA_INGRESS_CONFIG
_LOGGER = logging.getLogger(__name__)
class Ingress(JsonConfig, CoreSysAttributes):
"""Fetch last versions from version.json."""
def __init__(self, coresys: CoreSys):
"""Initialize updater."""
super().__init__(FILE_HASSIO_INGRESS, SCHEMA_INGRESS_CONFIG)
self.coresys: CoreSys = coresys
self.tokens: Dict[str, str] = {}
def get(self, token: str) -> Optional[Addon]:
"""Return addon they have this ingress token."""
if token not in self.tokens:
self._update_token_list()
return self.sys_addons.get(self.tokens.get(token))
@property
def sessions(self) -> Dict[str, float]:
"""Return sessions."""
return self._data[ATTR_SESSION]
async def load(self) -> None:
"""Update internal data."""
self._update_token_list()
self._cleanup_sessions()
_LOGGER.info("Load %d ingress session", len(self.sessions))
async def reload(self) -> None:
"""Reload/Validate sessions."""
self._cleanup_sessions()
async def unload(self) -> None:
"""Shutdown sessions."""
self.save_data()
def _cleanup_sessions(self) -> None:
"""Remove not used sessions."""
now = utcnow()
sessions = {}
for session, valid in self.sessions.items():
valid_dt = utc_from_timestamp(valid)
if valid_dt < now:
continue
# Is valid
sessions[session] = valid
# Write back
self.sessions.clear()
self.sessions.update(sessions)
def _update_token_list(self) -> None:
"""Regenerate token <-> Add-on map."""
self.tokens.clear()
# Read all ingress token and build a map
for addon in self.sys_addons.list_installed:
if not addon.with_ingress:
continue
self.tokens[addon.ingress_token] = addon.slug
def create_session(self) -> str:
"""Create new session."""
session = secrets.token_hex(64)
valid = utcnow() + timedelta(minutes=15)
self.sessions[session] = valid.timestamp()
self.save_data()
return session
def validate_session(self, session: str) -> bool:
"""Return True if session valid and make it longer valid."""
if session not in self.sessions:
return False
valid_until = utc_from_timestamp(self.sessions[session])
# Is still valid?
if valid_until < utcnow():
return False
# Update time
valid_until = valid_until + timedelta(minutes=15)
self.sessions[session] = valid_until.timestamp()
return True

View File

@ -7,7 +7,7 @@ from .exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
HASS_WATCHDOG_API = 'HASS_WATCHDOG_API' HASS_WATCHDOG_API = "HASS_WATCHDOG_API"
RUN_UPDATE_SUPERVISOR = 29100 RUN_UPDATE_SUPERVISOR = 29100
RUN_UPDATE_ADDONS = 57600 RUN_UPDATE_ADDONS = 57600
@ -17,6 +17,7 @@ RUN_RELOAD_ADDONS = 21600
RUN_RELOAD_SNAPSHOTS = 72000 RUN_RELOAD_SNAPSHOTS = 72000
RUN_RELOAD_HOST = 72000 RUN_RELOAD_HOST = 72000
RUN_RELOAD_UPDATER = 21600 RUN_RELOAD_UPDATER = 21600
RUN_RELOAD_INGRESS = 930
RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15 RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15
RUN_WATCHDOG_HOMEASSISTANT_API = 300 RUN_WATCHDOG_HOMEASSISTANT_API = 300
@ -33,28 +34,55 @@ class Tasks(CoreSysAttributes):
async def load(self): async def load(self):
"""Add Tasks to scheduler.""" """Add Tasks to scheduler."""
self.jobs.add(self.sys_scheduler.register_task( # Update
self._update_addons, RUN_UPDATE_ADDONS)) self.jobs.add(
self.jobs.add(self.sys_scheduler.register_task( self.sys_scheduler.register_task(self._update_addons, RUN_UPDATE_ADDONS)
self._update_supervisor, RUN_UPDATE_SUPERVISOR)) )
self.jobs.add(self.sys_scheduler.register_task( self.jobs.add(
self._update_hassos_cli, RUN_UPDATE_HASSOSCLI)) self.sys_scheduler.register_task(
self._update_supervisor, RUN_UPDATE_SUPERVISOR
)
)
self.jobs.add(
self.sys_scheduler.register_task(
self._update_hassos_cli, RUN_UPDATE_HASSOSCLI
)
)
self.jobs.add(self.sys_scheduler.register_task( # Reload
self.sys_addons.reload, RUN_RELOAD_ADDONS)) self.jobs.add(
self.jobs.add(self.sys_scheduler.register_task( self.sys_scheduler.register_task(self.sys_addons.reload, RUN_RELOAD_ADDONS)
self.sys_updater.reload, RUN_RELOAD_UPDATER)) )
self.jobs.add(self.sys_scheduler.register_task( self.jobs.add(
self.sys_snapshots.reload, RUN_RELOAD_SNAPSHOTS)) self.sys_scheduler.register_task(
self.jobs.add(self.sys_scheduler.register_task( self.sys_updater.reload, RUN_RELOAD_UPDATER
self.sys_host.reload, RUN_RELOAD_HOST)) )
)
self.jobs.add(
self.sys_scheduler.register_task(
self.sys_snapshots.reload, RUN_RELOAD_SNAPSHOTS
)
)
self.jobs.add(
self.sys_scheduler.register_task(self.sys_host.reload, RUN_RELOAD_HOST)
)
self.jobs.add(
self.sys_scheduler.register_task(
self.sys_ingress.reload, RUN_RELOAD_INGRESS
)
)
self.jobs.add(self.sys_scheduler.register_task( # Watchdog
self._watchdog_homeassistant_docker, self.jobs.add(
RUN_WATCHDOG_HOMEASSISTANT_DOCKER)) self.sys_scheduler.register_task(
self.jobs.add(self.sys_scheduler.register_task( self._watchdog_homeassistant_docker, RUN_WATCHDOG_HOMEASSISTANT_DOCKER
self._watchdog_homeassistant_api, )
RUN_WATCHDOG_HOMEASSISTANT_API)) )
self.jobs.add(
self.sys_scheduler.register_task(
self._watchdog_homeassistant_api, RUN_WATCHDOG_HOMEASSISTANT_API
)
)
_LOGGER.info("All core tasks are scheduled") _LOGGER.info("All core tasks are scheduled")
@ -72,7 +100,8 @@ class Tasks(CoreSysAttributes):
tasks.append(addon.update()) tasks.append(addon.update())
else: else:
_LOGGER.warning( _LOGGER.warning(
"Add-on %s will be ignored, schema tests fails", addon.slug) "Add-on %s will be ignored, schema tests fails", addon.slug
)
if tasks: if tasks:
_LOGGER.info("Add-on auto update process %d tasks", len(tasks)) _LOGGER.info("Add-on auto update process %d tasks", len(tasks))
@ -94,14 +123,18 @@ class Tasks(CoreSysAttributes):
async def _watchdog_homeassistant_docker(self): async def _watchdog_homeassistant_docker(self):
"""Check running state of Docker and start if they is close.""" """Check running state of Docker and start if they is close."""
# if Home Assistant is active # if Home Assistant is active
if not await self.sys_homeassistant.is_fails() or \ if (
not self.sys_homeassistant.watchdog or \ not await self.sys_homeassistant.is_fails()
self.sys_homeassistant.error_state: or not self.sys_homeassistant.watchdog
or self.sys_homeassistant.error_state
):
return return
# if Home Assistant is running # if Home Assistant is running
if self.sys_homeassistant.in_progress or \ if (
await self.sys_homeassistant.is_running(): self.sys_homeassistant.in_progress
or await self.sys_homeassistant.is_running()
):
return return
_LOGGER.warning("Watchdog found a problem with Home Assistant Docker!") _LOGGER.warning("Watchdog found a problem with Home Assistant Docker!")
@ -117,17 +150,21 @@ class Tasks(CoreSysAttributes):
a delay in our system. a delay in our system.
""" """
# If Home-Assistant is active # If Home-Assistant is active
if not await self.sys_homeassistant.is_fails() or \ if (
not self.sys_homeassistant.watchdog or \ not await self.sys_homeassistant.is_fails()
self.sys_homeassistant.error_state: or not self.sys_homeassistant.watchdog
or self.sys_homeassistant.error_state
):
return return
# Init cache data # Init cache data
retry_scan = self._cache.get(HASS_WATCHDOG_API, 0) retry_scan = self._cache.get(HASS_WATCHDOG_API, 0)
# If Home-Assistant API is up # If Home-Assistant API is up
if self.sys_homeassistant.in_progress or \ if (
await self.sys_homeassistant.check_api_state(): self.sys_homeassistant.in_progress
or await self.sys_homeassistant.check_api_state()
):
return return
# Look like we run into a problem # Look like we run into a problem

View File

@ -1,8 +1,6 @@
"""Tools file for Hass.io.""" """Tools file for Hass.io."""
import hashlib
import logging import logging
import re import re
import uuid
from datetime import datetime from datetime import datetime
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -14,11 +12,6 @@ def convert_to_ascii(raw) -> str:
return RE_STRING.sub("", raw.decode()) return RE_STRING.sub("", raw.decode())
def create_token() -> str:
"""Create token for API access."""
return hashlib.sha256(uuid.uuid4().bytes).hexdigest()
def process_lock(method): def process_lock(method):
"""Wrap function with only run once.""" """Wrap function with only run once."""

View File

@ -58,6 +58,11 @@ def parse_datetime(dt_str):
return datetime(**kws) return datetime(**kws)
def utcnow(): def utcnow() -> datetime:
"""Return the current timestamp including timezone.""" """Return the current timestamp including timezone."""
return datetime.now(UTC) return datetime.now(UTC)
def utc_from_timestamp(timestamp: float) -> datetime:
"""Return a UTC time from a timestamp."""
return UTC.localize(datetime.utcfromtimestamp(timestamp))

View File

@ -1,36 +1,36 @@
"""Validate functions.""" """Validate functions."""
import uuid
import re import re
import uuid
import voluptuous as vol import voluptuous as vol
from .const import ( from .const import (
ATTR_IMAGE,
ATTR_LAST_VERSION,
ATTR_CHANNEL,
ATTR_TIMEZONE,
ATTR_HASSOS,
ATTR_ADDONS_CUSTOM_LIST,
ATTR_PASSWORD,
ATTR_HOMEASSISTANT,
ATTR_HASSIO,
ATTR_BOOT,
ATTR_LAST_BOOT,
ATTR_SSL,
ATTR_PORT,
ATTR_WATCHDOG,
ATTR_WAIT_BOOT,
ATTR_UUID,
ATTR_REFRESH_TOKEN,
ATTR_HASSOS_CLI,
ATTR_ACCESS_TOKEN, ATTR_ACCESS_TOKEN,
CHANNEL_STABLE, ATTR_ADDONS_CUSTOM_LIST,
ATTR_BOOT,
ATTR_CHANNEL,
ATTR_HASSIO,
ATTR_HASSOS,
ATTR_HASSOS_CLI,
ATTR_HOMEASSISTANT,
ATTR_IMAGE,
ATTR_LAST_BOOT,
ATTR_LAST_VERSION,
ATTR_PASSWORD,
ATTR_PORT,
ATTR_REFRESH_TOKEN,
ATTR_SESSION,
ATTR_SSL,
ATTR_TIMEZONE,
ATTR_UUID,
ATTR_WAIT_BOOT,
ATTR_WATCHDOG,
CHANNEL_BETA, CHANNEL_BETA,
CHANNEL_DEV, CHANNEL_DEV,
CHANNEL_STABLE,
) )
from .utils.validate import validate_timezone from .utils.validate import validate_timezone
RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$") RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
@ -139,3 +139,12 @@ SCHEMA_HASSIO_CONFIG = vol.Schema(
SCHEMA_AUTH_CONFIG = vol.Schema({SHA256: SHA256}) SCHEMA_AUTH_CONFIG = vol.Schema({SHA256: SHA256})
SCHEMA_INGRESS_CONFIG = vol.Schema(
{
vol.Required(ATTR_SESSION, default=dict): vol.Schema(
{vol.Coerce(str): vol.Coerce(float)}
)
}
)

View File

@ -11,32 +11,30 @@ from hassio.bootstrap import initialize_coresys
@pytest.fixture @pytest.fixture
def docker(): def docker():
"""Mock Docker API.""" """Mock Docker API."""
with patch('hassio.coresys.DockerAPI') as mock: with patch("hassio.coresys.DockerAPI") as mock:
yield mock yield mock
@pytest.fixture @pytest.fixture
async def coresys(loop, docker): async def coresys(loop, docker):
"""Create a CoreSys Mock.""" """Create a CoreSys Mock."""
with patch('hassio.bootstrap.initialize_system_data'): with patch("hassio.bootstrap.initialize_system_data"):
coresys_obj = await initialize_coresys() coresys_obj = await initialize_coresys()
coresys_obj.ingress.save_data = MagicMock()
yield coresys_obj yield coresys_obj
@pytest.fixture @pytest.fixture
def sys_machine(): def sys_machine():
"""Mock sys_machine.""" """Mock sys_machine."""
with patch( with patch("hassio.coresys.CoreSys.machine", new_callable=PropertyMock) as mock:
'hassio.coresys.CoreSys.machine',
new_callable=PropertyMock) as mock:
yield mock yield mock
@pytest.fixture @pytest.fixture
def sys_supervisor(): def sys_supervisor():
with patch( with patch("hassio.coresys.CoreSys.supervisor", new_callable=PropertyMock) as mock:
'hassio.coresys.CoreSys.supervisor',
new_callable=PropertyMock) as mock:
mock.return_value = MagicMock() mock.return_value = MagicMock()
yield MagicMock yield MagicMock

22
tests/test_ingress.py Normal file
View File

@ -0,0 +1,22 @@
"""Test ingress."""
from datetime import timedelta
from hassio.utils.dt import utc_from_timestamp
def test_session_handling(coresys):
"""Create and test session."""
session = coresys.ingress.create_session()
validate = coresys.ingress.sessions[session]
assert coresys.ingress.save_data.called
assert session
assert validate
assert coresys.ingress.validate_session(session)
assert coresys.ingress.sessions[session] != validate
not_valid = utc_from_timestamp(validate) - timedelta(minutes=20)
coresys.ingress.sessions[session] = not_valid.timestamp()
assert not coresys.ingress.validate_session(session)
assert not coresys.ingress.validate_session("invalid session")