Compare commits

...

56 Commits
118 ... 130

Author SHA1 Message Date
Pascal Vizeli
ac824d3af6 Merge pull request #691 from home-assistant/dev
Release 130
2018-09-10 00:00:56 +02:00
Pascal Vizeli
dd25c29544 Bugfix Proxy with new token (#690)
* Update proxy.py

* Update security.py
2018-09-09 23:47:35 +02:00
Pascal Vizeli
5cbdbffbb2 Bump version to 130 2018-09-08 00:17:05 +02:00
Pascal Vizeli
bb81f14c2c Merge pull request #688 from home-assistant/dev
Release 129
2018-09-08 00:16:17 +02:00
Pascal Vizeli
cecefd6972 Change access to API (#686)
* Update API.md

* Update API.md

* Update API.md

* Update addons.py

* Update addons.py

* Update addons.py

* Update addons.py

* Update __init__.py

* Update security.py

* Update security.py

* Update const.py

* Update validate.py

* Update __init__.py

* Update validate.py

* Update homeassistant.py

* Update homeassistant.py

* Update homeassistant.py

* Update addon.py

* Update addon.py

* Update homeassistant.py

* Fix lint

* Fix lint

* Backward combatibility

* Make token more robust

* Fix bug

* Logic error

* Fix access

* fix valid
2018-09-07 22:59:31 +02:00
Pascal Vizeli
ff7f6a0b4c Bump version 129 2018-08-29 10:16:04 +02:00
Pascal Vizeli
1dc9f35e12 Merge pull request #674 from home-assistant/dev
Release 128
2018-08-29 10:13:57 +02:00
Pascal Vizeli
051b63c7cc Fix access token property (#673)
* Fix access token property

* revert
2018-08-28 17:04:39 +02:00
Pascal Vizeli
aac4b9b24a Snapshot/Restore Home-Assistant token (#672)
* Snapshot/Restore Home-Assistant token

* Encrypt token & check api

* fix lint
2018-08-28 16:32:17 +02:00
Paulus Schoutsen
1a208a20b6 Handle access token expiration (#671) 2018-08-28 12:14:40 +02:00
Pascal Vizeli
b1e8722ead Update: pycryptodome to 3.6.6 (#670) 2018-08-28 12:04:32 +02:00
Pascal Vizeli
a66af6e903 Update aiohttp to 3.4.0 (#668)
Update: aiohttp to 3.4.0
2018-08-28 01:18:38 +02:00
Pascal Vizeli
0c345fc615 Bump version 128 2018-08-19 22:05:42 +02:00
Pascal Vizeli
087b082a6b Merge pull request #660 from home-assistant/dev
Release 127
2018-08-19 22:03:49 +02:00
Pascal Vizeli
0b85209eae Detect running record migration (#659)
* Detect running record migration

* Fix order

* Change order second one
2018-08-19 21:58:19 +02:00
Pascal Vizeli
d81bc7de46 Change rating 1-6 (#658) 2018-08-19 18:17:14 +02:00
Pascal Vizeli
e3a99b9f89 Fix /share inside whitelist (#657) 2018-08-18 15:05:18 +02:00
Pascal Vizeli
5d319b37ea Bump verison 127 2018-08-16 23:38:57 +02:00
Pascal Vizeli
9f25606986 Merge pull request #653 from home-assistant/dev
Release 126
2018-08-16 23:38:24 +02:00
Pascal Vizeli
ecd12732ee New generation of security and access (#652)
* New generation of security and access

* Update const.py

* Update validate.py

* Update addon.py

* Update validate.py

* Fix name

* Allow access

* Fix

* add logs

* change message

* add rating

* fix lint

* fix lint

* fix

* Fix
2018-08-16 22:49:08 +02:00
Pascal Vizeli
85fbde8e36 Fix Dockerfile 2018-08-16 01:42:56 +02:00
Pascal Vizeli
6e6c2c3efb Change timezone handling (#641)
* Change timezone handling

* Update dt.py

* Update homeassistant.py

* fix

* Use new timezone

* fix handling

* fix regex

* fix regex

* Rename old config

* fix lint

* simplify

* fix regex

* fix

* cleanup

* cleanup

* fix

* fix find

* mm
2018-08-16 01:40:20 +02:00
Pascal Vizeli
0d4a808449 Improve docker build cache for supervisor (#651) 2018-08-15 23:52:52 +02:00
Pascal Vizeli
087f746647 update docker API to 3.5.0 (#650) 2018-08-15 22:05:13 +02:00
Pascal Vizeli
640d66ad1a Update uvloop 0.11.2 (#648) 2018-08-15 21:38:57 +02:00
Pascal Vizeli
f5f5ed83af Bump version 126 2018-08-09 14:38:34 +02:00
Pascal Vizeli
95f01a1161 Merge pull request #640 from home-assistant/dev
Release 125
2018-08-09 14:37:56 +02:00
Pascal Vizeli
b84e7e7d94 Allow to reset token (#639)
* Allow to reset token

* Update homeassistant.py
2018-08-09 14:37:00 +02:00
Pascal Vizeli
5d7018f3f0 Bump version 125 2018-08-09 01:05:21 +02:00
Pascal Vizeli
d87a85ceb5 Merge pull request #636 from home-assistant/dev
Release 124
2018-08-09 01:03:47 +02:00
Pascal Vizeli
9ab6e80b6f Cleanup logging (#637)
* Cleanup logging

* simplify
2018-08-09 01:03:00 +02:00
Pascal Vizeli
78e91e859e Add add-on support for docker sock ro (#635)
* Add add-on support for docker sock ro

* fix
2018-08-09 00:42:33 +02:00
Pascal Vizeli
9eee8eade6 Fix gpio mapping on amd64 systems (#634) 2018-08-09 00:29:20 +02:00
Pascal Vizeli
124ce0b8b7 Update voluptuous 0.11.5 (#622) 2018-08-09 00:06:49 +02:00
Pascal Vizeli
00e7d96472 Fix new auth system (#633)
* Fix new auth system

* Update exceptions.py

* Update exceptions.py

* Update homeassistant.py

* Update homeassistant.py

* Update homeassistant.py

* Fix some API Errors

* fix lint
2018-08-09 00:05:08 +02:00
Pascal Vizeli
398815efd8 Bump version to 124 2018-08-08 19:22:12 +02:00
Pascal Vizeli
bdc2bdcf56 Merge pull request #631 from ndarilek/dev
Add SYS_RESOURCE to list of valid privileges
2018-08-07 17:06:15 +02:00
Nolan Darilek
68eafb0a7d Add SYS_RESOURCE to list of valid privileges 2018-08-07 03:21:21 +00:00
Pascal Vizeli
7ca2fd7193 Merge pull request #618 from home-assistant/dev
Release 123
2018-08-04 01:25:55 +02:00
Pascal Vizeli
ec823edd8f Cleanup docker image (#617) 2018-08-04 00:41:14 +02:00
Pascal Vizeli
858c7a1fa7 Bump version 123 2018-08-02 23:40:52 +02:00
Pascal Vizeli
6ac45a24fc Merge pull request #615 from home-assistant/dev
Release 122
2018-08-02 23:39:43 +02:00
Pascal Vizeli
9430b39042 Update uvloop version 0.11.1 (#614) 2018-08-02 23:18:40 +02:00
Pascal Vizeli
ae7466ccfe Fix UnicodeDecodeError with read json file (#613)
* Update json.py

* Update data.py
2018-08-02 21:48:50 +02:00
Simon Holzmayer
2c17fe5da8 Adapt regex validation to allow docker images from other registries (#608)
* Adapt regex validation to allow images from other registries than dockerhub

Issue #564

* Update validate.py
2018-07-30 12:34:42 +02:00
Pascal Vizeli
a0fb91af29 Use requirements.txt (#607)
* Create requirements.txt

* Update setup.py

* Update Dockerfile

* Update Dockerfile

* Update requirements.txt

* Update requirements.txt

* Update Dockerfile

* Update tox.ini
2018-07-27 16:34:47 +02:00
Pascal Vizeli
f626e31fd3 Bump version 122 2018-07-25 01:52:24 +02:00
Pascal Vizeli
0151a149fd Merge pull request #604 from home-assistant/dev
Release 121
2018-07-25 01:47:36 +02:00
Pascal Vizeli
9dea93142b Timeout shutdown (#603)
* Don't wait too long for shutdown

* Update log message

* Fix timeout

* Fast shudown
2018-07-25 01:46:54 +02:00
Pascal Vizeli
7f878bfac0 Bump version to 121 2018-07-25 01:39:33 +02:00
Pascal Vizeli
ebe9ae2341 Merge pull request #600 from home-assistant/dev
Release 120
2018-07-24 22:11:10 +02:00
Pascal Vizeli
e777bbd024 Fix bug with proxy (#599) 2018-07-24 22:00:46 +02:00
Pascal Vizeli
2116d56124 Bump version 120 2018-07-24 16:32:46 +02:00
Pascal Vizeli
0b6a82b018 Merge pull request #598 from home-assistant/dev
Release 119
2018-07-24 16:21:06 +02:00
Pascal Vizeli
b4ea28af4e Update uvloop to 0.11.0 (#597) 2018-07-24 16:16:26 +02:00
Pascal Vizeli
22f59712df Bump version 119 2018-07-23 13:01:51 +02:00
33 changed files with 495 additions and 148 deletions

21
API.md
View File

@@ -1,4 +1,4 @@
# Hass.io Server # Hass.io
## Hass.io RESTful API ## Hass.io RESTful API
@@ -27,6 +27,9 @@ For access to API you need set the `X-HASSIO-KEY` they will be available for Add
### Hass.io ### Hass.io
- GET `/supervisor/ping` - GET `/supervisor/ping`
This API call don't need a token.
- GET `/supervisor/info` - GET `/supervisor/info`
The addons from `addons` are only installed one. The addons from `addons` are only installed one.
@@ -412,6 +415,8 @@ Proxy to real websocket instance.
### RESTful for API addons ### RESTful for API addons
If a add-on will call itself, you can use `/addons/self/...`.
- GET `/addons` - GET `/addons`
Get all available addons. Get all available addons.
@@ -478,10 +483,14 @@ Get all available addons.
"changelog": "bool", "changelog": "bool",
"hassio_api": "bool", "hassio_api": "bool",
"homeassistant_api": "bool", "homeassistant_api": "bool",
"full_access": "bool",
"protected": "bool",
"rating": "1-6",
"stdin": "bool", "stdin": "bool",
"webui": "null|http(s)://[HOST]:port/xy/zx", "webui": "null|http(s)://[HOST]:port/xy/zx",
"gpio": "bool", "gpio": "bool",
"devicetree": "bool", "devicetree": "bool",
"docker_api": "bool",
"audio": "bool", "audio": "bool",
"audio_input": "null|0,0", "audio_input": "null|0,0",
"audio_output": "null|0,0", "audio_output": "null|0,0",
@@ -513,6 +522,16 @@ Get all available addons.
Reset custom network/audio/options, set it `null`. Reset custom network/audio/options, set it `null`.
- POST `/addons/{addon}/security`
This function is not callable by itself.
```json
{
"protected": "bool",
}
```
- POST `/addons/{addon}/start` - POST `/addons/{addon}/start`
- POST `/addons/{addon}/stop` - POST `/addons/{addon}/stop`

View File

@@ -1,24 +1,22 @@
ARG BUILD_FROM ARG BUILD_FROM
FROM $BUILD_FROM FROM $BUILD_FROM
# Add env # Install base
ENV LANG C.UTF-8
# Setup base
RUN apk add --no-cache \ RUN apk add --no-cache \
git \ git \
socat \ socat \
glib \ glib \
libstdc++ \ libstdc++ \
eudev-libs \ eudev-libs
&& apk add --no-cache --virtual .build-dependencies \
# Install requirements
COPY requirements.txt /usr/src/
RUN apk add --no-cache --virtual .build-dependencies \
make \ make \
g++ \ g++ \
&& pip3 install --no-cache-dir \ && pip3 install --no-cache-dir -r /usr/src/requirements.txt \
uvloop==0.10.2 \ && apk del .build-dependencies \
cchardet==2.1.1 \ && rm -f /usr/src/requirements.txt
pycryptodome==3.6.4 \
&& apk del .build-dependencies
# Install HassIO # Install HassIO
COPY . /usr/src/hassio COPY . /usr/src/hassio

View File

@@ -50,6 +50,13 @@ class AddonManager(CoreSysAttributes):
return addon return addon
return None return None
def from_token(self, token):
"""Return an add-on from hassio token."""
for addon in self.list_addons:
if addon.is_installed and token == addon.hassio_token:
return addon
return None
async def load(self): async def load(self):
"""Startup addon management.""" """Startup addon management."""
self.data.reload() self.data.reload()

View File

@@ -25,10 +25,12 @@ from ..const import (
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT, ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC,
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES,
ATTR_APPARMOR, ATTR_DEVICETREE, SECURITY_PROFILE, SECURITY_DISABLE, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS,
SECURITY_DEFAULT) ATTR_PROTECTED, ATTR_ACCESS_TOKEN,
SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..docker.addon import DockerAddon from ..docker.addon import DockerAddon
from ..utils import create_token
from ..utils.json import write_json_file, read_json_file from ..utils.json import write_json_file, read_json_file
from ..utils.apparmor import adjust_profile from ..utils.apparmor import adjust_profile
from ..exceptions import HostAppArmorError from ..exceptions import HostAppArmorError
@@ -171,6 +173,13 @@ class Addon(CoreSysAttributes):
return self._data.user[self._id][ATTR_UUID] return self._data.user[self._id][ATTR_UUID]
return None return None
@property
def hassio_token(self):
"""Return access token for hass.io API."""
if self.is_installed:
return self._data.user[self._id].get(ATTR_ACCESS_TOKEN)
return None
@property @property
def description(self): def description(self):
"""Return description of addon.""" """Return description of addon."""
@@ -201,6 +210,18 @@ class Addon(CoreSysAttributes):
return self._data.cache[self._id][ATTR_VERSION] return self._data.cache[self._id][ATTR_VERSION]
return self.version_installed return self.version_installed
@property
def protected(self):
"""Return if addon is in protected mode."""
if self.is_installed:
return self._data.user[self._id][ATTR_PROTECTED]
return True
@protected.setter
def protected(self, value):
"""Set addon in protected mode."""
self._data.user[self._id][ATTR_PROTECTED] = value
@property @property
def startup(self): def startup(self):
"""Return startup type of addon.""" """Return startup type of addon."""
@@ -335,6 +356,11 @@ class Addon(CoreSysAttributes):
"""Return if the add-on don't support hass labels.""" """Return if the add-on don't support hass labels."""
return self._mesh.get(ATTR_LEGACY) return self._mesh.get(ATTR_LEGACY)
@property
def access_docker_api(self):
"""Return if the add-on need read-only docker API access."""
return self._mesh.get(ATTR_DOCKER_API)
@property @property
def access_hassio_api(self): def access_hassio_api(self):
"""Return True if the add-on access to hassio api.""" """Return True if the add-on access to hassio api."""
@@ -355,6 +381,11 @@ class Addon(CoreSysAttributes):
"""Return True if the add-on access to gpio interface.""" """Return True if the add-on access to gpio interface."""
return self._mesh[ATTR_GPIO] return self._mesh[ATTR_GPIO]
@property
def with_full_access(self):
"""Return True if the add-on want full access to hardware."""
return self._mesh[ATTR_FULL_ACCESS]
@property @property
def with_devicetree(self): def with_devicetree(self):
"""Return True if the add-on read access to devicetree.""" """Return True if the add-on read access to devicetree."""
@@ -663,6 +694,14 @@ class Addon(CoreSysAttributes):
@check_installed @check_installed
async def start(self): async def start(self):
"""Set options and start addon.""" """Set options and start addon."""
if await self.instance.is_running():
_LOGGER.warning("%s allready running!", self.slug)
return
# Access Token
self._data.user[self._id][ATTR_ACCESS_TOKEN] = create_token()
self._data.save_data()
# Options # Options
if not self.write_options(): if not self.write_options():
return False return False

View File

@@ -80,7 +80,7 @@ class AddonsData(JsonConfig, CoreSysAttributes):
read_json_file(repository_file) read_json_file(repository_file)
) )
except (OSError, json.JSONDecodeError): except (OSError, json.JSONDecodeError, UnicodeDecodeError):
_LOGGER.warning("Can't read repository information from %s", _LOGGER.warning("Can't read repository information from %s",
repository_file) repository_file)
return return

View File

@@ -4,11 +4,53 @@ import hashlib
import logging import logging
import re import re
from ..const import (
SECURITY_DISABLE, SECURITY_PROFILE, PRIVILEGED_NET_ADMIN,
PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO)
RE_SHA1 = re.compile(r"[a-f0-9]{8}") RE_SHA1 = re.compile(r"[a-f0-9]{8}")
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def rating_security(addon):
"""Return 1-5 for security rating.
1 = not secure
5 = high secure
"""
rating = 5
# AppArmor
if addon.apparmor == SECURITY_DISABLE:
rating += -1
elif addon.apparmor == SECURITY_PROFILE:
rating += 1
# API Access
if addon.access_hassio_api or addon.access_homeassistant_api:
rating += -1
# Privileged options
if addon.privileged in (PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN,
PRIVILEGED_SYS_RAWIO):
rating += -1
# Not secure Networking
if addon.host_network:
rating += -1
# Full Access
if addon.with_full_access:
rating += -2
# Docker Access
if addon.access_docker_api:
rating = 1
return max(min(6, rating), 1)
def get_hash_from_repository(name): def get_hash_from_repository(name):
"""Generate a hash from repository.""" """Generate a hash from repository."""
key = name.lower().encode() key = name.lower().encode()

View File

@@ -18,7 +18,11 @@ from ..const import (
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH,
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY,
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
ATTR_APPARMOR, ATTR_DEVICETREE) ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED,
ATTR_FULL_ACCESS, ATTR_ACCESS_TOKEN,
PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO,
PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE,
PRIVILEGED_SYS_RESOURCE)
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -58,12 +62,13 @@ STARTUP_ALL = [
] ]
PRIVILEGED_ALL = [ PRIVILEGED_ALL = [
"NET_ADMIN", PRIVILEGED_NET_ADMIN,
"SYS_ADMIN", PRIVILEGED_SYS_ADMIN,
"SYS_RAWIO", PRIVILEGED_SYS_RAWIO,
"IPC_LOCK", PRIVILEGED_IPC_LOCK,
"SYS_TIME", PRIVILEGED_SYS_TIME,
"SYS_NICE" PRIVILEGED_SYS_NICE,
PRIVILEGED_SYS_RESOURCE,
] ]
BASE_IMAGE = { BASE_IMAGE = {
@@ -109,6 +114,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)}, vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)], vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)],
vol.Optional(ATTR_APPARMOR, default=True): vol.Boolean(), vol.Optional(ATTR_APPARMOR, default=True): vol.Boolean(),
vol.Optional(ATTR_FULL_ACCESS, default=False): vol.Boolean(),
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(), vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(), vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(), vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
@@ -116,6 +122,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(), vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(), vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(), vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(),
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)], vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
vol.Optional(ATTR_DISCOVERY): [vol.Match(RE_DISCOVERY)], vol.Optional(ATTR_DISCOVERY): [vol.Match(RE_DISCOVERY)],
vol.Required(ATTR_OPTIONS): dict, vol.Required(ATTR_OPTIONS): dict,
@@ -129,7 +136,8 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT]) vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])
})) }))
}), False), }), False),
vol.Optional(ATTR_IMAGE): vol.Match(r"^[\w{}]+/[\-\w{}]+$"), vol.Optional(ATTR_IMAGE):
vol.Match(r"^([a-zA-Z.:\d{}]+/)*?([\w{}]+)/([\-\w{}]+)$"),
vol.Optional(ATTR_TIMEOUT, default=10): vol.Optional(ATTR_TIMEOUT, default=10):
vol.All(vol.Coerce(int), vol.Range(min=10, max=120)), vol.All(vol.Coerce(int), vol.Range(min=10, max=120)),
}, extra=vol.REMOVE_EXTRA) }, extra=vol.REMOVE_EXTRA)
@@ -160,6 +168,7 @@ SCHEMA_ADDON_USER = vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str), vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex):
vol.Match(r"^[0-9a-f]{32}$"), vol.Match(r"^[0-9a-f]{32}$"),
vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"),
vol.Optional(ATTR_OPTIONS, default=dict): dict, vol.Optional(ATTR_OPTIONS, default=dict): dict,
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(), vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
vol.Optional(ATTR_BOOT): vol.Optional(ATTR_BOOT):
@@ -167,6 +176,7 @@ SCHEMA_ADDON_USER = vol.Schema({
vol.Optional(ATTR_NETWORK): DOCKER_PORTS, vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_DEVICE, vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_DEVICE,
vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE, vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE,
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
}, extra=vol.REMOVE_EXTRA) }, extra=vol.REMOVE_EXTRA)

View File

@@ -158,6 +158,7 @@ class RestAPI(CoreSysAttributes):
web.get('/addons/{addon}/logo', api_addons.logo), web.get('/addons/{addon}/logo', api_addons.logo),
web.get('/addons/{addon}/changelog', api_addons.changelog), web.get('/addons/{addon}/changelog', api_addons.changelog),
web.post('/addons/{addon}/stdin', api_addons.stdin), web.post('/addons/{addon}/stdin', api_addons.stdin),
web.post('/addons/{addon}/security', api_addons.security),
web.get('/addons/{addon}/stats', api_addons.stats), web.get('/addons/{addon}/stats', api_addons.stats),
]) ])
@@ -235,13 +236,16 @@ class RestAPI(CoreSysAttributes):
async def start(self): async def start(self):
"""Run rest api webserver.""" """Run rest api webserver."""
await self._runner.setup() await self._runner.setup()
self._site = web.TCPSite(self._runner, "0.0.0.0", 80) self._site = web.TCPSite(
self._runner, host="0.0.0.0", port=80, shutdown_timeout=5)
try: try:
await self._site.start() await self._site.start()
except OSError as err: except OSError as err:
_LOGGER.fatal( _LOGGER.fatal(
"Failed to create HTTP server at 0.0.0.0:80 -> %s", err) "Failed to create HTTP server at 0.0.0.0:80 -> %s", err)
else:
_LOGGER.info("Start API on %s", self.sys_docker.network.supervisor)
async def stop(self): async def stop(self):
"""Stop rest api webserver.""" """Stop rest api webserver."""
@@ -251,3 +255,5 @@ class RestAPI(CoreSysAttributes):
# Shutdown running API # Shutdown running API
await self._site.stop() await self._site.stop()
await self._runner.cleanup() await self._runner.cleanup()
_LOGGER.info("Stop API on %s", self.sys_docker.network.supervisor)

View File

@@ -6,6 +6,7 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from .utils import api_process, api_process_raw, api_validate from .utils import api_process, api_process_raw, api_validate
from ..addons.utils import rating_security
from ..const import ( from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
ATTR_URL, ATTR_DESCRIPTON, ATTR_DETACHED, ATTR_NAME, ATTR_REPOSITORY, ATTR_URL, ATTR_DESCRIPTON, ATTR_DETACHED, ATTR_NAME, ATTR_REPOSITORY,
@@ -17,10 +18,13 @@ from ..const import (
ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION, ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION,
ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX, ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX,
ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES, ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES,
ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT) ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT,
REQUEST_FROM)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import DOCKER_PORTS, ALSA_DEVICE from ..validate import DOCKER_PORTS, ALSA_DEVICE
from ..exceptions import APINotSupportedError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -37,13 +41,24 @@ SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE, vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE,
}) })
# pylint: disable=no-value-for-parameter
SCHEMA_SECURITY = vol.Schema({
vol.Optional(ATTR_PROTECTED): vol.Boolean(),
})
class APIAddons(CoreSysAttributes): class APIAddons(CoreSysAttributes):
"""Handle rest api for addons functions.""" """Handle rest api for addons functions."""
def _extract_addon(self, request, check_installed=True): def _extract_addon(self, request, check_installed=True):
"""Return addon, throw an exception it it doesn't exist.""" """Return addon, throw an exception it it doesn't exist."""
addon = self.sys_addons.get(request.match_info.get('addon')) addon_slug = request.match_info.get('addon')
# Lookup itself
if addon_slug == 'self':
addon_slug = request.get(REQUEST_FROM)
addon = self.sys_addons.get(addon_slug)
if not addon: if not addon:
raise RuntimeError("Addon does not exist") raise RuntimeError("Addon does not exist")
@@ -116,6 +131,8 @@ class APIAddons(CoreSysAttributes):
ATTR_REPOSITORY: addon.repository, ATTR_REPOSITORY: addon.repository,
ATTR_LAST_VERSION: addon.last_version, ATTR_LAST_VERSION: addon.last_version,
ATTR_STATE: await addon.state(), ATTR_STATE: await addon.state(),
ATTR_PROTECTED: addon.protected,
ATTR_RATING: rating_security(addon),
ATTR_BOOT: addon.boot, ATTR_BOOT: addon.boot,
ATTR_OPTIONS: addon.options, ATTR_OPTIONS: addon.options,
ATTR_URL: addon.url, ATTR_URL: addon.url,
@@ -126,6 +143,7 @@ class APIAddons(CoreSysAttributes):
ATTR_HOST_IPC: addon.host_ipc, ATTR_HOST_IPC: addon.host_ipc,
ATTR_HOST_DBUS: addon.host_dbus, ATTR_HOST_DBUS: addon.host_dbus,
ATTR_PRIVILEGED: addon.privileged, ATTR_PRIVILEGED: addon.privileged,
ATTR_FULL_ACCESS: addon.with_full_access,
ATTR_APPARMOR: addon.apparmor, ATTR_APPARMOR: addon.apparmor,
ATTR_DEVICES: self._pretty_devices(addon), ATTR_DEVICES: self._pretty_devices(addon),
ATTR_ICON: addon.with_icon, ATTR_ICON: addon.with_icon,
@@ -137,6 +155,7 @@ class APIAddons(CoreSysAttributes):
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api, ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
ATTR_GPIO: addon.with_gpio, ATTR_GPIO: addon.with_gpio,
ATTR_DEVICETREE: addon.with_devicetree, ATTR_DEVICETREE: addon.with_devicetree,
ATTR_DOCKER_API: addon.access_docker_api,
ATTR_AUDIO: addon.with_audio, ATTR_AUDIO: addon.with_audio,
ATTR_AUDIO_INPUT: addon.audio_input, ATTR_AUDIO_INPUT: addon.audio_input,
ATTR_AUDIO_OUTPUT: addon.audio_output, ATTR_AUDIO_OUTPUT: addon.audio_output,
@@ -171,6 +190,25 @@ class APIAddons(CoreSysAttributes):
addon.save_data() addon.save_data()
return True return True
@api_process
async def security(self, request):
"""Store security options for addon."""
addon = self._extract_addon(request)
# Have Access
if addon.slug == request[REQUEST_FROM]:
_LOGGER.error("Can't self modify his security!")
raise APINotSupportedError()
body = await api_validate(SCHEMA_SECURITY, request)
if ATTR_PROTECTED in body:
_LOGGER.warning("Protected flag changing for %s!", addon.slug)
addon.protected = body[ATTR_PROTECTED]
addon.save_data()
return True
@api_process @api_process
async def stats(self, request): async def stats(self, request):
"""Return resource information.""" """Return resource information."""

View File

@@ -21,17 +21,16 @@ _LOGGER = logging.getLogger(__name__)
SCHEMA_OPTIONS = vol.Schema({ SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_BOOT): vol.Boolean(), vol.Optional(ATTR_BOOT): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Inclusive(ATTR_IMAGE, 'custom_hass'):
vol.Any(None, vol.Coerce(str)), vol.Maybe(vol.Coerce(str)),
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'):
vol.Any(None, DOCKER_IMAGE), vol.Any(None, DOCKER_IMAGE),
vol.Optional(ATTR_PORT): NETWORK_PORT, vol.Optional(ATTR_PORT): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)), vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_SSL): vol.Boolean(), vol.Optional(ATTR_SSL): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG): vol.Boolean(), vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT): vol.Optional(ATTR_WAIT_BOOT):
vol.All(vol.Coerce(int), vol.Range(min=60)), vol.All(vol.Coerce(int), vol.Range(min=60)),
# Required once we enforce user system vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_REFRESH_TOKEN): str,
}) })
SCHEMA_VERSION = vol.Schema({ SCHEMA_VERSION = vol.Schema({

View File

@@ -23,7 +23,11 @@ class APIProxy(CoreSysAttributes):
def _check_access(self, request): def _check_access(self, request):
"""Check the Hass.io token.""" """Check the Hass.io token."""
hassio_token = request.headers.get(HEADER_HA_ACCESS) hassio_token = request.headers.get(HEADER_HA_ACCESS)
addon = self.sys_addons.from_uuid(hassio_token) addon = self.sys_addons.from_token(hassio_token)
# Need removed with 131
if not addon:
addon = self.sys_addons.from_uuid(hassio_token)
if not addon: if not addon:
_LOGGER.warning("Unknown HomeAssistant API access!") _LOGGER.warning("Unknown HomeAssistant API access!")
@@ -59,6 +63,8 @@ class APIProxy(CoreSysAttributes):
except HomeAssistantAuthError: except HomeAssistantAuthError:
_LOGGER.error("Authenticate error on API for request %s", path) _LOGGER.error("Authenticate error on API for request %s", path)
except HomeAssistantAPIError:
_LOGGER.error("Error on API for request %s", path)
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
_LOGGER.error("Client error on API %s request %s", path, err) _LOGGER.error("Client error on API %s request %s", path, err)
except asyncio.TimeoutError: except asyncio.TimeoutError:
@@ -148,11 +154,12 @@ class APIProxy(CoreSysAttributes):
self.sys_homeassistant.access_token = None self.sys_homeassistant.access_token = None
return await self._websocket_client() return await self._websocket_client()
_LOGGER.error( raise HomeAssistantAuthError()
"Failed authentication to Home-Assistant websocket: %s", data)
except (RuntimeError, HomeAssistantAPIError) as err: except (RuntimeError, ValueError) as err:
_LOGGER.error("Client error on websocket API %s.", err) _LOGGER.error("Client error on websocket API %s.", err)
except HomeAssistantAuthError as err:
_LOGGER.error("Failed authentication to HomeAssistant websocket")
raise HTTPBadGateway() raise HTTPBadGateway()
@@ -175,7 +182,11 @@ class APIProxy(CoreSysAttributes):
response = await server.receive_json() response = await server.receive_json()
hassio_token = (response.get('api_password') or hassio_token = (response.get('api_password') or
response.get('access_token')) response.get('access_token'))
addon = self.sys_addons.from_uuid(hassio_token) addon = self.sys_addons.from_token(hassio_token)
# Need removed with 131
if not addon:
addon = self.sys_addons.from_uuid(hassio_token)
if not addon or not addon.access_homeassistant_api: if not addon or not addon.access_homeassistant_api:
_LOGGER.warning("Unauthorized websocket access!") _LOGGER.warning("Unauthorized websocket access!")

View File

@@ -3,18 +3,28 @@ import logging
import re import re
from aiohttp.web import middleware from aiohttp.web import middleware
from aiohttp.web_exceptions import HTTPUnauthorized from aiohttp.web_exceptions import HTTPUnauthorized, HTTPForbidden
from ..const import HEADER_TOKEN, REQUEST_FROM from ..const import HEADER_TOKEN, REQUEST_FROM
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NO_SECURITY_CHECK = set(( NO_SECURITY_CHECK = re.compile(
re.compile(r"^/homeassistant/api/.*$"), r"^(?:"
re.compile(r"^/homeassistant/websocket$"), r"|/homeassistant/api/.*$"
re.compile(r"^/supervisor/ping$"), r"|/homeassistant/websocket$"
)) r"|/supervisor/ping$"
r")$"
)
ADDONS_API_BYPASS = re.compile(
r"^(?:"
r"|/homeassistant/info$"
r"|/supervisor/info$"
r"|/addons(?:/self/[^/]+)?$"
r")$"
)
class SecurityMiddleware(CoreSysAttributes): class SecurityMiddleware(CoreSysAttributes):
@@ -27,33 +37,50 @@ class SecurityMiddleware(CoreSysAttributes):
@middleware @middleware
async def token_validation(self, request, handler): async def token_validation(self, request, handler):
"""Check security access of this layer.""" """Check security access of this layer."""
request_from = None
hassio_token = request.headers.get(HEADER_TOKEN) hassio_token = request.headers.get(HEADER_TOKEN)
# Ignore security check # Ignore security check
for rule in NO_SECURITY_CHECK: if NO_SECURITY_CHECK.match(request.path):
if rule.match(request.path): _LOGGER.debug("Passthrough %s", request.path)
_LOGGER.debug("Passthrough %s", request.path) return await handler(request)
return await handler(request)
# Not token
if not hassio_token:
_LOGGER.warning("No API token provided for %s", request.path)
raise HTTPUnauthorized()
# Home-Assistant # Home-Assistant
if hassio_token == self.sys_homeassistant.uuid: # UUID check need removed with 131
if hassio_token in (self.sys_homeassistant.uuid,
self.sys_homeassistant.hassio_token):
_LOGGER.debug("%s access from Home-Assistant", request.path) _LOGGER.debug("%s access from Home-Assistant", request.path)
request[REQUEST_FROM] = 'homeassistant' request_from = 'homeassistant'
# Host # Host
if hassio_token == self.sys_machine_id: if hassio_token == self.sys_machine_id:
_LOGGER.debug("%s access from Host", request.path) _LOGGER.debug("%s access from Host", request.path)
request[REQUEST_FROM] = 'host' request_from = 'host'
# Add-on # Add-on
addon = self.sys_addons.from_uuid(hassio_token) \ addon = None
if hassio_token else None if hassio_token and not request_from:
if addon: addon = self.sys_addons.from_token(hassio_token)
_LOGGER.info("%s access from %s", request.path, addon.slug) # Need removed with 131
request[REQUEST_FROM] = addon.slug if not addon:
addon = self.sys_addons.from_uuid(hassio_token)
if request.get(REQUEST_FROM): # Check Add-on API access
if addon and addon.access_hassio_api:
_LOGGER.info("%s access from %s", request.path, addon.slug)
request_from = addon.slug
elif addon and ADDONS_API_BYPASS.match(request.path):
_LOGGER.debug("Passthrough %s from %s", request.path, addon.slug)
request_from = addon.slug
if request_from:
request[REQUEST_FROM] = request_from
return await handler(request) return await handler(request)
_LOGGER.warning("Invalid token for access %s", request.path) _LOGGER.warning("Invalid token for access %s", request.path)
raise HTTPUnauthorized() raise HTTPForbidden()

View File

@@ -30,10 +30,10 @@ def api_process(method):
"""Return api information.""" """Return api information."""
try: try:
answer = await method(api, *args, **kwargs) answer = await method(api, *args, **kwargs)
except RuntimeError as err:
return api_return_error(message=str(err))
except HassioError: except HassioError:
return api_return_error() return api_return_error()
except RuntimeError as err:
return api_return_error(message=str(err))
if isinstance(answer, dict): if isinstance(answer, dict):
return api_return_ok(data=answer) return api_return_ok(data=answer)

View File

@@ -66,10 +66,11 @@ def initialize_system_data(coresys):
config = coresys.config config = coresys.config
# homeassistant config folder # homeassistant config folder
if not config.path_config.is_dir(): if not config.path_homeassistant.is_dir():
_LOGGER.info( _LOGGER.info(
"Create Home-Assistant config folder %s", config.path_config) "Create Home-Assistant config folder %s",
config.path_config.mkdir() config.path_homeassistant)
config.path_homeassistant.mkdir()
# hassio ssl folder # hassio ssl folder
if not config.path_ssl.is_dir(): if not config.path_ssl.is_dir():

View File

@@ -2,8 +2,11 @@
from datetime import datetime from datetime import datetime
import logging import logging
import os import os
import re
from pathlib import Path, PurePath from pathlib import Path, PurePath
import pytz
from .const import ( from .const import (
FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_TIMEZONE, ATTR_ADDONS_CUSTOM_LIST, FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_TIMEZONE, ATTR_ADDONS_CUSTOM_LIST,
ATTR_LAST_BOOT, ATTR_WAIT_BOOT) ATTR_LAST_BOOT, ATTR_WAIT_BOOT)
@@ -29,6 +32,8 @@ APPARMOR_DATA = PurePath("apparmor")
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat() DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
RE_TIMEZONE = re.compile(r"time_zone: (?P<timezone>[\w/\-+]+)")
class CoreConfig(JsonConfig): class CoreConfig(JsonConfig):
"""Hold all core config data.""" """Hold all core config data."""
@@ -40,7 +45,21 @@ class CoreConfig(JsonConfig):
@property @property
def timezone(self): def timezone(self):
"""Return system timezone.""" """Return system timezone."""
return self._data[ATTR_TIMEZONE] config_file = Path(self.path_homeassistant, 'configuration.yaml')
try:
assert config_file.exists()
configuration = config_file.read_text()
data = RE_TIMEZONE.search(configuration)
assert data
timezone = data.group('timezone')
pytz.timezone(timezone)
except (pytz.exceptions.UnknownTimeZoneError, OSError, AssertionError):
_LOGGER.debug("Can't parse HomeAssistant timezone")
return self._data[ATTR_TIMEZONE]
return timezone
@timezone.setter @timezone.setter
def timezone(self, value): def timezone(self, value):
@@ -83,12 +102,12 @@ class CoreConfig(JsonConfig):
return PurePath(os.environ['SUPERVISOR_SHARE']) return PurePath(os.environ['SUPERVISOR_SHARE'])
@property @property
def path_extern_config(self): def path_extern_homeassistant(self):
"""Return config path extern for docker.""" """Return config path extern for docker."""
return str(PurePath(self.path_extern_hassio, HOMEASSISTANT_CONFIG)) return str(PurePath(self.path_extern_hassio, HOMEASSISTANT_CONFIG))
@property @property
def path_config(self): def path_homeassistant(self):
"""Return config path inside supervisor.""" """Return config path inside supervisor."""
return Path(HASSIO_DATA, HOMEASSISTANT_CONFIG) return Path(HASSIO_DATA, HOMEASSISTANT_CONFIG)

View File

@@ -2,7 +2,7 @@
from pathlib import Path from pathlib import Path
from ipaddress import ip_network from ipaddress import ip_network
HASSIO_VERSION = '118' HASSIO_VERSION = '130'
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
URL_HASSIO_VERSION = \ URL_HASSIO_VERSION = \
@@ -178,6 +178,11 @@ ATTR_HASSOS_CLI = 'hassos_cli'
ATTR_VERSION_CLI = 'version_cli' ATTR_VERSION_CLI = 'version_cli'
ATTR_VERSION_CLI_LATEST = 'version_cli_latest' ATTR_VERSION_CLI_LATEST = 'version_cli_latest'
ATTR_REFRESH_TOKEN = 'refresh_token' ATTR_REFRESH_TOKEN = 'refresh_token'
ATTR_ACCESS_TOKEN = 'access_token'
ATTR_DOCKER_API = 'docker_api'
ATTR_FULL_ACCESS = 'full_access'
ATTR_PROTECTED = 'protected'
ATTR_RATING = 'rating'
SERVICE_MQTT = 'mqtt' SERVICE_MQTT = 'mqtt'
@@ -226,6 +231,14 @@ SECURITY_PROFILE = 'profile'
SECURITY_DEFAULT = 'default' SECURITY_DEFAULT = 'default'
SECURITY_DISABLE = 'disable' SECURITY_DISABLE = 'disable'
PRIVILEGED_NET_ADMIN = 'NET_ADMIN'
PRIVILEGED_SYS_ADMIN = 'SYS_ADMIN'
PRIVILEGED_SYS_RAWIO = 'SYS_RAWIO'
PRIVILEGED_IPC_LOCK = 'IPC_LOCK'
PRIVILEGED_SYS_TIME = 'SYS_TIME'
PRIVILEGED_SYS_NICE = 'SYS_NICE'
PRIVILEGED_SYS_RESOURCE = 'SYS_RESOURCE'
FEATURES_SHUTDOWN = 'shutdown' FEATURES_SHUTDOWN = 'shutdown'
FEATURES_REBOOT = 'reboot' FEATURES_REBOOT = 'reboot'
FEATURES_HASSOS = 'hassos' FEATURES_HASSOS = 'hassos'

View File

@@ -3,6 +3,8 @@ from contextlib import suppress
import asyncio import asyncio
import logging import logging
import async_timeout
from .coresys import CoreSysAttributes from .coresys import CoreSysAttributes
from .const import ( from .const import (
STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION, STARTUP_INITIALIZE) STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION, STARTUP_INITIALIZE)
@@ -65,7 +67,6 @@ class HassIO(CoreSysAttributes):
# start api # start api
await self.sys_api.start() await self.sys_api.start()
_LOGGER.info("Start API on %s", self.sys_docker.network.supervisor)
# start addon mark as initialize # start addon mark as initialize
await self.sys_addons.boot(STARTUP_INITIALIZE) await self.sys_addons.boot(STARTUP_INITIALIZE)
@@ -113,12 +114,18 @@ class HassIO(CoreSysAttributes):
self.sys_scheduler.suspend = True self.sys_scheduler.suspend = True
# process async stop tasks # process async stop tasks
await asyncio.wait([ try:
self.sys_api.stop(), with async_timeout.timeout(10):
self.sys_dns.stop(), await asyncio.wait([
self.sys_websession.close(), self.sys_api.stop(),
self.sys_websession_ssl.close() self.sys_dns.stop(),
]) self.sys_websession.close(),
self.sys_websession_ssl.close()
])
except asyncio.TimeoutError:
_LOGGER.warning("Force Shutdown!")
_LOGGER.info("Hass.io is down")
async def shutdown(self): async def shutdown(self):
"""Shutdown all running containers in correct order.""" """Shutdown all running containers in correct order."""

View File

@@ -66,6 +66,11 @@ class CoreSys:
"""Return True if we run dev modus.""" """Return True if we run dev modus."""
return self._updater.channel == CHANNEL_DEV return self._updater.channel == CHANNEL_DEV
@property
def timezone(self):
"""Return timezone."""
return self._config.timezone
@property @property
def loop(self): def loop(self):
"""Return loop object.""" """Return loop object."""

View File

@@ -1,6 +1,7 @@
"""Init file for HassIO addon docker object.""" """Init file for HassIO addon docker object."""
import logging import logging
import os import os
from pathlib import Path
import docker import docker
import requests import requests
@@ -66,6 +67,11 @@ class DockerAddon(DockerInterface):
return 'host' return 'host'
return None return None
@property
def full_access(self):
"""Return True if full access is enabled."""
return not self.addon.protected and self.addon.with_full_access
@property @property
def hostname(self): def hostname(self):
"""Return slug/id of addon.""" """Return slug/id of addon."""
@@ -85,8 +91,8 @@ class DockerAddon(DockerInterface):
return { return {
**addon_env, **addon_env,
ENV_TIME: self.sys_config.timezone, ENV_TIME: self.sys_timezone,
ENV_TOKEN: self.addon.uuid, ENV_TOKEN: self.addon.hassio_token,
} }
@property @property
@@ -172,7 +178,7 @@ class DockerAddon(DockerInterface):
# setup config mappings # setup config mappings
if MAP_CONFIG in addon_mapping: if MAP_CONFIG in addon_mapping:
volumes.update({ volumes.update({
str(self.sys_config.path_extern_config): { str(self.sys_config.path_extern_homeassistant): {
'bind': "/config", 'mode': addon_mapping[MAP_CONFIG] 'bind': "/config", 'mode': addon_mapping[MAP_CONFIG]
}}) }})
@@ -204,14 +210,14 @@ class DockerAddon(DockerInterface):
# GPIO support # GPIO support
if self.addon.with_gpio: if self.addon.with_gpio:
volumes.update({ for gpio_path in ("/sys/class/gpio", "/sys/devices/platform/soc"):
"/sys/class/gpio": { if not Path(gpio_path).exists():
'bind': "/sys/class/gpio", 'mode': 'rw' continue
}, volumes.update({
"/sys/devices/platform/soc": { gpio_path: {
'bind': "/sys/devices/platform/soc", 'mode': 'rw' 'bind': gpio_path, 'mode': 'rw'
}, },
}) })
# DeviceTree support # DeviceTree support
if self.addon.with_devicetree: if self.addon.with_devicetree:
@@ -221,6 +227,14 @@ class DockerAddon(DockerInterface):
}, },
}) })
# Docker API support
if not self.addon.protected and self.addon.access_docker_api:
volumes.update({
"/var/run/docker.sock": {
'bind': "/var/run/docker.sock", 'mode': 'ro'
},
})
# Host dbus system # Host dbus system
if self.addon.host_dbus: if self.addon.host_dbus:
volumes.update({ volumes.update({
@@ -245,6 +259,11 @@ class DockerAddon(DockerInterface):
if self._is_running(): if self._is_running():
return True return True
# Security check
if not self.addon.protected:
_LOGGER.warning(
"%s run with disabled proteced mode!", self.addon.name)
# cleanup # cleanup
self._stop() self._stop()
@@ -254,6 +273,7 @@ class DockerAddon(DockerInterface):
hostname=self.hostname, hostname=self.hostname,
detach=True, detach=True,
init=True, init=True,
privileged=self.full_access,
ipc_mode=self.ipc, ipc_mode=self.ipc,
stdin_open=self.addon.with_stdin, stdin_open=self.addon.with_stdin,
network_mode=self.network_mode, network_mode=self.network_mode,

View File

@@ -61,11 +61,11 @@ class DockerHomeAssistant(DockerInterface):
network_mode='host', network_mode='host',
environment={ environment={
'HASSIO': self.sys_docker.network.supervisor, 'HASSIO': self.sys_docker.network.supervisor,
ENV_TIME: self.sys_config.timezone, ENV_TIME: self.sys_timezone,
ENV_TOKEN: self.sys_homeassistant.uuid, ENV_TOKEN: self.sys_homeassistant.hassio_token,
}, },
volumes={ volumes={
str(self.sys_config.path_extern_config): str(self.sys_config.path_extern_homeassistant):
{'bind': '/config', 'mode': 'rw'}, {'bind': '/config', 'mode': 'rw'},
str(self.sys_config.path_extern_ssl): str(self.sys_config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'ro'}, {'bind': '/ssl', 'mode': 'ro'},
@@ -95,13 +95,15 @@ class DockerHomeAssistant(DockerInterface):
stdout=True, stdout=True,
stderr=True, stderr=True,
environment={ environment={
ENV_TIME: self.sys_config.timezone, ENV_TIME: self.sys_timezone,
}, },
volumes={ volumes={
str(self.sys_config.path_extern_config): str(self.sys_config.path_extern_homeassistant):
{'bind': '/config', 'mode': 'rw'}, {'bind': '/config', 'mode': 'rw'},
str(self.sys_config.path_extern_ssl): str(self.sys_config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'ro'}, {'bind': '/ssl', 'mode': 'ro'},
str(self.sys_config.path_extern_share):
{'bind': '/share', 'mode': 'ro'},
} }
) )

View File

@@ -1,7 +1,4 @@
"""Core Exceptions.""" """Core Exceptions."""
import asyncio
import aiohttp
class HassioError(Exception): class HassioError(Exception):
@@ -26,14 +23,13 @@ class HomeAssistantUpdateError(HomeAssistantError):
pass pass
class HomeAssistantAuthError(HomeAssistantError): class HomeAssistantAPIError(HomeAssistantError):
"""Home Assistant Auth API exception.""" """Home Assistant API exception."""
pass pass
class HomeAssistantAPIError( class HomeAssistantAuthError(HomeAssistantAPIError):
HomeAssistantAuthError, asyncio.TimeoutError, aiohttp.ClientError): """Home Assistant Auth API exception."""
"""Home Assistant API exception."""
pass pass
@@ -80,6 +76,19 @@ class HostServiceError(HostError):
class HostAppArmorError(HostError): class HostAppArmorError(HostError):
"""Host apparmor functions fails.""" """Host apparmor functions fails."""
pass
# API
class APIError(HassioError):
"""API errors."""
pass
class APINotSupportedError(HassioNotSupportedError):
"""API not supported error."""
pass
# utils/gdbus # utils/gdbus

View File

@@ -1,9 +1,11 @@
"""HomeAssistant control object.""" """HomeAssistant control object."""
import asyncio import asyncio
from contextlib import asynccontextmanager, suppress from contextlib import asynccontextmanager, suppress
from datetime import datetime, timedelta
import logging import logging
import os import os
import re import re
from pathlib import Path
import socket import socket
import time import time
@@ -14,14 +16,14 @@ import attr
from .const import ( from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID, FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID,
ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG,
ATTR_WAIT_BOOT, ATTR_REFRESH_TOKEN, ATTR_WAIT_BOOT, ATTR_REFRESH_TOKEN, ATTR_ACCESS_TOKEN,
HEADER_HA_ACCESS) HEADER_HA_ACCESS)
from .coresys import CoreSysAttributes from .coresys import CoreSysAttributes
from .docker.homeassistant import DockerHomeAssistant from .docker.homeassistant import DockerHomeAssistant
from .exceptions import ( from .exceptions import (
HomeAssistantUpdateError, HomeAssistantError, HomeAssistantAPIError, HomeAssistantUpdateError, HomeAssistantError, HomeAssistantAPIError,
HomeAssistantAuthError) HomeAssistantAuthError)
from .utils import convert_to_ascii, process_lock from .utils import convert_to_ascii, process_lock, create_token
from .utils.json import JsonConfig from .utils.json import JsonConfig
from .validate import SCHEMA_HASS_CONFIG from .validate import SCHEMA_HASS_CONFIG
@@ -45,6 +47,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
self._error_state = False self._error_state = False
# We don't persist access tokens. Instead we fetch new ones when needed # We don't persist access tokens. Instead we fetch new ones when needed
self.access_token = None self.access_token = None
self._access_token_expires = None
async def load(self): async def load(self):
"""Prepare HomeAssistant object.""" """Prepare HomeAssistant object."""
@@ -182,6 +185,11 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
"""Return a UUID of this HomeAssistant.""" """Return a UUID of this HomeAssistant."""
return self._data[ATTR_UUID] return self._data[ATTR_UUID]
@property
def hassio_token(self):
"""Return a access token for Hass.io API."""
return self._data.get(ATTR_ACCESS_TOKEN)
@property @property
def refresh_token(self): def refresh_token(self):
"""Return the refresh token to authenticate with HomeAssistant.""" """Return the refresh token to authenticate with HomeAssistant."""
@@ -274,6 +282,14 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async def _start(self): async def _start(self):
"""Start HomeAssistant docker & wait.""" """Start HomeAssistant docker & wait."""
if await self.instance.is_running():
_LOGGER.warning("HomeAssistant allready running!")
return
# Create new API token
self._data[ATTR_ACCESS_TOKEN] = create_token()
self.save_data()
if not await self.instance.run(): if not await self.instance.run():
raise HomeAssistantError() raise HomeAssistantError()
await self._block_till_run() await self._block_till_run()
@@ -351,11 +367,12 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async def ensure_access_token(self): async def ensure_access_token(self):
"""Ensures there is an access token.""" """Ensures there is an access token."""
if self.access_token is not None: if (self.access_token is not None and
self._access_token_expires > datetime.utcnow()):
return return
with suppress(asyncio.TimeoutError, aiohttp.ClientError): with suppress(asyncio.TimeoutError, aiohttp.ClientError):
async with self.sys_websession_ssl.get( async with self.sys_websession_ssl.post(
f"{self.api_url}/auth/token", f"{self.api_url}/auth/token",
timeout=30, timeout=30,
data={ data={
@@ -364,14 +381,14 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
} }
) as resp: ) as resp:
if resp.status != 200: if resp.status != 200:
_LOGGER.error("Authenticate problem with HomeAssistant!") _LOGGER.error("Can't update HomeAssistant access token!")
raise HomeAssistantAuthError() raise HomeAssistantAuthError()
_LOGGER.info("Updated HomeAssistant API token")
tokens = await resp.json() tokens = await resp.json()
self.access_token = tokens['access_token'] self.access_token = tokens['access_token']
return self._access_token_expires = \
datetime.utcnow() + timedelta(seconds=tokens['expires_in'])
_LOGGER.error("Can't update HomeAssistant access token!")
raise HomeAssistantAPIError()
@asynccontextmanager @asynccontextmanager
async def make_request(self, method, path, json=None, content_type=None, async def make_request(self, method, path, json=None, content_type=None,
@@ -380,10 +397,12 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
url = f"{self.api_url}/{path}" url = f"{self.api_url}/{path}"
headers = {} headers = {}
# Passthrough content type
if content_type is not None: if content_type is not None:
headers[hdrs.CONTENT_TYPE] = content_type headers[hdrs.CONTENT_TYPE] = content_type
elif self.api_password: # Set old API Password
if self.api_password:
headers[HEADER_HA_ACCESS] = self.api_password headers[HEADER_HA_ACCESS] = self.api_password
for _ in (1, 2): for _ in (1, 2):
@@ -392,15 +411,20 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
await self.ensure_access_token() await self.ensure_access_token()
headers[hdrs.AUTHORIZATION] = f'Bearer {self.access_token}' headers[hdrs.AUTHORIZATION] = f'Bearer {self.access_token}'
async with getattr(self.sys_websession_ssl, method)( try:
url, data=data, timeout=timeout, json=json, headers=headers async with getattr(self.sys_websession_ssl, method)(
) as resp: url, data=data, timeout=timeout, json=json,
# Access token expired headers=headers
if resp.status == 401 and self.refresh_token: ) as resp:
self.access_token = None # Access token expired
continue if resp.status == 401 and self.refresh_token:
yield resp self.access_token = None
return continue
yield resp
return
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error on call %s: %s", url, err)
break
raise HomeAssistantAPIError() raise HomeAssistantAPIError()
@@ -431,6 +455,9 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async def _block_till_run(self): async def _block_till_run(self):
"""Block until Home-Assistant is booting up or startup timeout.""" """Block until Home-Assistant is booting up or startup timeout."""
start_time = time.monotonic() start_time = time.monotonic()
migration_progress = False
migration_file = Path(
self.sys_config.path_homeassistant, '.migration_progress')
def check_port(): def check_port():
"""Check if port is mapped.""" """Check if port is mapped."""
@@ -446,21 +473,39 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
pass pass
return False return False
while time.monotonic() - start_time < self.wait_boot: while True:
await asyncio.sleep(10)
# 1
# Check if Container is is_running
if not await self.instance.is_running():
_LOGGER.error("HomeAssistant is crashed!")
break
# 2
# Check if API response # Check if API response
if await self.sys_run_in_executor(check_port): if await self.sys_run_in_executor(check_port):
_LOGGER.info("Detect a running HomeAssistant instance") _LOGGER.info("Detect a running HomeAssistant instance")
self._error_state = False self._error_state = False
return return
# wait and don't hit the system # 3
await asyncio.sleep(10) # Running DB Migration
if migration_file.exists():
if not migration_progress:
migration_progress = True
_LOGGER.info("HomeAssistant record migration in progress")
continue
elif migration_progress:
migration_progress = False # Reset start time
start_time = time.monotonic()
_LOGGER.info("HomeAssistant record migration done")
# Check if Container is is_running # 4
if not await self.instance.is_running(): # Timeout
_LOGGER.error("Home Assistant is crashed!") if time.monotonic() - start_time > self.wait_boot:
_LOGGER.warning("Don't wait anymore of HomeAssistant startup!")
break break
_LOGGER.warning("Don't wait anymore of HomeAssistant startup!")
self._error_state = True self._error_state = True
raise HomeAssistantError() raise HomeAssistantError()

View File

@@ -3,6 +3,8 @@ import asyncio
import logging import logging
import shlex import shlex
import async_timeout
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
COMMAND = "socat UDP-RECVFROM:53,fork UDP-SENDTO:127.0.0.11:53" COMMAND = "socat UDP-RECVFROM:53,fork UDP-SENDTO:127.0.0.11:53"
@@ -38,5 +40,10 @@ class DNSForward:
return return
self.proc.kill() self.proc.kill()
await self.proc.wait() try:
with async_timeout.timeout(5):
await self.proc.wait()
except asyncio.TimeoutError:
_LOGGER.warning("Stop waiting for DNS shutdown")
_LOGGER.info("Stop DNS forwarding") _LOGGER.info("Stop DNS forwarding")

View File

@@ -293,6 +293,7 @@ class SnapshotManager(CoreSysAttributes):
# Stop Home-Assistant if they will be restored later # Stop Home-Assistant if they will be restored later
if homeassistant and FOLDER_HOMEASSISTANT in folders: if homeassistant and FOLDER_HOMEASSISTANT in folders:
await self.sys_homeassistant.stop() await self.sys_homeassistant.stop()
snapshot.restore_homeassistant()
# Process folders # Process folders
if folders: if folders:
@@ -304,7 +305,6 @@ class SnapshotManager(CoreSysAttributes):
if homeassistant: if homeassistant:
_LOGGER.info("Restore %s run Home-Assistant", _LOGGER.info("Restore %s run Home-Assistant",
snapshot.slug) snapshot.slug)
snapshot.restore_homeassistant()
task_hass = self.sys_create_task( task_hass = self.sys_create_task(
self.sys_homeassistant.update( self.sys_homeassistant.update(
snapshot.homeassistant_version)) snapshot.homeassistant_version))
@@ -322,12 +322,20 @@ class SnapshotManager(CoreSysAttributes):
_LOGGER.info("Restore %s old add-ons", snapshot.slug) _LOGGER.info("Restore %s old add-ons", snapshot.slug)
await snapshot.restore_addons(addon_list) await snapshot.restore_addons(addon_list)
# make sure homeassistant run agen # Make sure homeassistant run agen
if task_hass: if task_hass:
_LOGGER.info("Restore %s wait for Home-Assistant", _LOGGER.info("Restore %s wait for Home-Assistant",
snapshot.slug) snapshot.slug)
await task_hass await task_hass
await self.sys_homeassistant.start()
# Do we need start HomeAssistant?
if not await self.sys_homeassistant.is_running():
await self.sys_homeassistant.start()
# Check If we can access to API / otherwise restart
if not await self.sys_homeassistant.check_api_state():
_LOGGER.warning("Need restart HomeAssistant for API")
await self.sys_homeassistant.restart()
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Restore %s error", snapshot.slug) _LOGGER.exception("Restore %s error", snapshot.slug)

View File

@@ -20,7 +20,7 @@ from ..const import (
ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_IMAGE, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_IMAGE,
ATTR_PORT, ATTR_SSL, ATTR_PASSWORD, ATTR_WATCHDOG, ATTR_BOOT, ATTR_CRYPTO, ATTR_PORT, ATTR_SSL, ATTR_PASSWORD, ATTR_WATCHDOG, ATTR_BOOT, ATTR_CRYPTO,
ATTR_LAST_VERSION, ATTR_PROTECTED, ATTR_WAIT_BOOT, ATTR_SIZE, ATTR_LAST_VERSION, ATTR_PROTECTED, ATTR_WAIT_BOOT, ATTR_SIZE,
CRYPTO_AES128) ATTR_REFRESH_TOKEN, CRYPTO_AES128)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..utils.json import write_json_file from ..utils.json import write_json_file
from ..utils.tar import SecureTarFile from ..utils.tar import SecureTarFile
@@ -387,6 +387,8 @@ class Snapshot(CoreSysAttributes):
# API/Proxy # API/Proxy
self.homeassistant[ATTR_PORT] = self.sys_homeassistant.api_port self.homeassistant[ATTR_PORT] = self.sys_homeassistant.api_port
self.homeassistant[ATTR_SSL] = self.sys_homeassistant.api_ssl self.homeassistant[ATTR_SSL] = self.sys_homeassistant.api_ssl
self.homeassistant[ATTR_REFRESH_TOKEN] = \
self._encrypt_data(self.sys_homeassistant.refresh_token)
self.homeassistant[ATTR_PASSWORD] = \ self.homeassistant[ATTR_PASSWORD] = \
self._encrypt_data(self.sys_homeassistant.api_password) self._encrypt_data(self.sys_homeassistant.api_password)
@@ -405,6 +407,8 @@ class Snapshot(CoreSysAttributes):
# API/Proxy # API/Proxy
self.sys_homeassistant.api_port = self.homeassistant[ATTR_PORT] self.sys_homeassistant.api_port = self.homeassistant[ATTR_PORT]
self.sys_homeassistant.api_ssl = self.homeassistant[ATTR_SSL] self.sys_homeassistant.api_ssl = self.homeassistant[ATTR_SSL]
self.sys_homeassistant.refresh_token = \
self._decrypt_data(self.homeassistant[ATTR_REFRESH_TOKEN])
self.sys_homeassistant.api_password = \ self.sys_homeassistant.api_password = \
self._decrypt_data(self.homeassistant[ATTR_PASSWORD]) self._decrypt_data(self.homeassistant[ATTR_PASSWORD])

View File

@@ -7,6 +7,7 @@ from ..const import (
ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, ATTR_IMAGE, ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, ATTR_IMAGE,
ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT, ATTR_SIZE, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT, ATTR_SIZE,
ATTR_LAST_VERSION, ATTR_WAIT_BOOT, ATTR_PROTECTED, ATTR_CRYPTO, ATTR_LAST_VERSION, ATTR_WAIT_BOOT, ATTR_PROTECTED, ATTR_CRYPTO,
ATTR_REFRESH_TOKEN,
FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL, FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
SNAPSHOT_FULL, SNAPSHOT_PARTIAL, CRYPTO_AES128) SNAPSHOT_FULL, SNAPSHOT_PARTIAL, CRYPTO_AES128)
from ..validate import NETWORK_PORT, REPOSITORIES, DOCKER_IMAGE from ..validate import NETWORK_PORT, REPOSITORIES, DOCKER_IMAGE
@@ -40,6 +41,7 @@ SCHEMA_SNAPSHOT = vol.Schema({
vol.Optional(ATTR_SSL, default=False): vol.Boolean(), vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)), vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(), vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT, default=600): vol.Optional(ATTR_WAIT_BOOT, default=600):
vol.All(vol.Coerce(int), vol.Range(min=60)), vol.All(vol.Coerce(int), vol.Range(min=60)),

View File

@@ -1,7 +1,9 @@
"""Tools file for HassIO.""" """Tools file for HassIO."""
from datetime import datetime from datetime import datetime
import hashlib
import logging import logging
import re import re
import uuid
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
RE_STRING = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))") RE_STRING = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
@@ -12,6 +14,11 @@ def convert_to_ascii(raw):
return RE_STRING.sub("", raw.decode()) return RE_STRING.sub("", raw.decode())
def create_token():
"""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."""
async def wrap_api(api, *args, **kwargs): async def wrap_api(api, *args, **kwargs):

View File

@@ -9,7 +9,6 @@ UTC = pytz.utc
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
FREEGEOIP_URL = "https://freegeoip.net/json/"
# Copyright (c) Django Software Foundation and individual contributors. # Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved. # All rights reserved.

View File

@@ -45,7 +45,7 @@ class JsonConfig:
if self._file.is_file(): if self._file.is_file():
try: try:
self._data = read_json_file(self._file) self._data = read_json_file(self._file)
except (OSError, json.JSONDecodeError): except (OSError, json.JSONDecodeError, UnicodeDecodeError):
_LOGGER.warning("Can't read %s", self._file) _LOGGER.warning("Can't read %s", self._file)
self._data = {} self._data = {}

View File

@@ -10,6 +10,7 @@ from .const import (
ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO,
ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG,
ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN, ATTR_HASSOS_CLI, ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN, ATTR_HASSOS_CLI,
ATTR_ACCESS_TOKEN,
CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV) CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV)
@@ -84,6 +85,7 @@ DOCKER_PORTS = vol.Schema({
SCHEMA_HASS_CONFIG = vol.Schema({ SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex):
vol.Match(r"^[0-9a-f]{32}$"), vol.Match(r"^[0-9a-f]{32}$"),
vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"),
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(), vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE, vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str), vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),

13
requirements.txt Normal file
View File

@@ -0,0 +1,13 @@
attr==0.3.1
async_timeout==3.0.0
aiohttp==3.4.0
docker==3.5.0
colorlog==3.1.2
voluptuous==0.11.5
gitpython==2.1.10
pytz==2018.4
pyudev==0.21.0
pycryptodome==3.6.6
cpe==1.2.1
uvloop==0.11.2
cchardet==2.1.1

View File

@@ -38,18 +38,5 @@ setup(
'hassio.utils', 'hassio.utils',
'hassio.snapshots' 'hassio.snapshots'
], ],
include_package_data=True, include_package_data=True
install_requires=[
'attr==0.3.1',
'async_timeout==3.0.0',
'aiohttp==3.3.2',
'docker==3.4.0',
'colorlog==3.1.2',
'voluptuous==0.11.1',
'gitpython==2.1.10',
'pytz==2018.4',
'pyudev==0.21.0',
'pycryptodome==3.6.4',
"cpe==1.2.1"
]
) )

View File

@@ -4,7 +4,8 @@ envlist = lint
[testenv] [testenv]
deps = deps =
flake8==3.5.0 flake8==3.5.0
pylint==2.0.0 pylint==2.1.1
-r{toxinidir}/requirements.txt
[testenv:lint] [testenv:lint]
basepython = python3 basepython = python3