Compare commits

...

47 Commits
121 ... 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
31 changed files with 464 additions and 138 deletions

21
API.md
View File

@@ -1,4 +1,4 @@
# Hass.io Server
# Hass.io
## 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
- GET `/supervisor/ping`
This API call don't need a token.
- GET `/supervisor/info`
The addons from `addons` are only installed one.
@@ -412,6 +415,8 @@ Proxy to real websocket instance.
### RESTful for API addons
If a add-on will call itself, you can use `/addons/self/...`.
- GET `/addons`
Get all available addons.
@@ -478,10 +483,14 @@ Get all available addons.
"changelog": "bool",
"hassio_api": "bool",
"homeassistant_api": "bool",
"full_access": "bool",
"protected": "bool",
"rating": "1-6",
"stdin": "bool",
"webui": "null|http(s)://[HOST]:port/xy/zx",
"gpio": "bool",
"devicetree": "bool",
"docker_api": "bool",
"audio": "bool",
"audio_input": "null|0,0",
"audio_output": "null|0,0",
@@ -513,6 +522,16 @@ Get all available addons.
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}/stop`

View File

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

View File

@@ -50,6 +50,13 @@ class AddonManager(CoreSysAttributes):
return addon
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):
"""Startup addon management."""
self.data.reload()

View File

@@ -25,10 +25,12 @@ from ..const import (
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC,
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES,
ATTR_APPARMOR, ATTR_DEVICETREE, SECURITY_PROFILE, SECURITY_DISABLE,
SECURITY_DEFAULT)
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS,
ATTR_PROTECTED, ATTR_ACCESS_TOKEN,
SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT)
from ..coresys import CoreSysAttributes
from ..docker.addon import DockerAddon
from ..utils import create_token
from ..utils.json import write_json_file, read_json_file
from ..utils.apparmor import adjust_profile
from ..exceptions import HostAppArmorError
@@ -171,6 +173,13 @@ class Addon(CoreSysAttributes):
return self._data.user[self._id][ATTR_UUID]
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
def description(self):
"""Return description of addon."""
@@ -201,6 +210,18 @@ class Addon(CoreSysAttributes):
return self._data.cache[self._id][ATTR_VERSION]
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
def startup(self):
"""Return startup type of addon."""
@@ -335,6 +356,11 @@ class Addon(CoreSysAttributes):
"""Return if the add-on don't support hass labels."""
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
def access_hassio_api(self):
"""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 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
def with_devicetree(self):
"""Return True if the add-on read access to devicetree."""
@@ -663,6 +694,14 @@ class Addon(CoreSysAttributes):
@check_installed
async def start(self):
"""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
if not self.write_options():
return False

View File

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

View File

@@ -4,11 +4,53 @@ import hashlib
import logging
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}")
_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):
"""Generate a hash from repository."""
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_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY,
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
_LOGGER = logging.getLogger(__name__)
@@ -58,12 +62,13 @@ STARTUP_ALL = [
]
PRIVILEGED_ALL = [
"NET_ADMIN",
"SYS_ADMIN",
"SYS_RAWIO",
"IPC_LOCK",
"SYS_TIME",
"SYS_NICE"
PRIVILEGED_NET_ADMIN,
PRIVILEGED_SYS_ADMIN,
PRIVILEGED_SYS_RAWIO,
PRIVILEGED_IPC_LOCK,
PRIVILEGED_SYS_TIME,
PRIVILEGED_SYS_NICE,
PRIVILEGED_SYS_RESOURCE,
]
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_PRIVILEGED): [vol.In(PRIVILEGED_ALL)],
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_GPIO, 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_STDIN, 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_DISCOVERY): [vol.Match(RE_DISCOVERY)],
vol.Required(ATTR_OPTIONS): dict,
@@ -129,7 +136,8 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])
}))
}), 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.All(vol.Coerce(int), vol.Range(min=10, max=120)),
}, extra=vol.REMOVE_EXTRA)
@@ -160,6 +168,7 @@ SCHEMA_ADDON_USER = vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex):
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_AUTO_UPDATE, default=False): vol.Boolean(),
vol.Optional(ATTR_BOOT):
@@ -167,6 +176,7 @@ SCHEMA_ADDON_USER = vol.Schema({
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_DEVICE,
vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE,
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
}, 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}/changelog', api_addons.changelog),
web.post('/addons/{addon}/stdin', api_addons.stdin),
web.post('/addons/{addon}/security', api_addons.security),
web.get('/addons/{addon}/stats', api_addons.stats),
])

View File

@@ -6,6 +6,7 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error
from .utils import api_process, api_process_raw, api_validate
from ..addons.utils import rating_security
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
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_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_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT)
ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API,
ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT,
REQUEST_FROM)
from ..coresys import CoreSysAttributes
from ..validate import DOCKER_PORTS, ALSA_DEVICE
from ..exceptions import APINotSupportedError
_LOGGER = logging.getLogger(__name__)
@@ -37,13 +41,24 @@ SCHEMA_OPTIONS = vol.Schema({
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):
"""Handle rest api for addons functions."""
def _extract_addon(self, request, check_installed=True):
"""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:
raise RuntimeError("Addon does not exist")
@@ -116,6 +131,8 @@ class APIAddons(CoreSysAttributes):
ATTR_REPOSITORY: addon.repository,
ATTR_LAST_VERSION: addon.last_version,
ATTR_STATE: await addon.state(),
ATTR_PROTECTED: addon.protected,
ATTR_RATING: rating_security(addon),
ATTR_BOOT: addon.boot,
ATTR_OPTIONS: addon.options,
ATTR_URL: addon.url,
@@ -126,6 +143,7 @@ class APIAddons(CoreSysAttributes):
ATTR_HOST_IPC: addon.host_ipc,
ATTR_HOST_DBUS: addon.host_dbus,
ATTR_PRIVILEGED: addon.privileged,
ATTR_FULL_ACCESS: addon.with_full_access,
ATTR_APPARMOR: addon.apparmor,
ATTR_DEVICES: self._pretty_devices(addon),
ATTR_ICON: addon.with_icon,
@@ -137,6 +155,7 @@ class APIAddons(CoreSysAttributes):
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
ATTR_GPIO: addon.with_gpio,
ATTR_DEVICETREE: addon.with_devicetree,
ATTR_DOCKER_API: addon.access_docker_api,
ATTR_AUDIO: addon.with_audio,
ATTR_AUDIO_INPUT: addon.audio_input,
ATTR_AUDIO_OUTPUT: addon.audio_output,
@@ -171,6 +190,25 @@ class APIAddons(CoreSysAttributes):
addon.save_data()
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
async def stats(self, request):
"""Return resource information."""

View File

@@ -21,17 +21,16 @@ _LOGGER = logging.getLogger(__name__)
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_BOOT): vol.Boolean(),
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.Any(None, DOCKER_IMAGE),
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_WATCHDOG): vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT):
vol.All(vol.Coerce(int), vol.Range(min=60)),
# Required once we enforce user system
vol.Optional(ATTR_REFRESH_TOKEN): str,
vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)),
})
SCHEMA_VERSION = vol.Schema({

View File

@@ -23,7 +23,11 @@ class APIProxy(CoreSysAttributes):
def _check_access(self, request):
"""Check the Hass.io token."""
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:
_LOGGER.warning("Unknown HomeAssistant API access!")
@@ -59,6 +63,8 @@ class APIProxy(CoreSysAttributes):
except HomeAssistantAuthError:
_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:
_LOGGER.error("Client error on API %s request %s", path, err)
except asyncio.TimeoutError:
@@ -148,11 +154,12 @@ class APIProxy(CoreSysAttributes):
self.sys_homeassistant.access_token = None
return await self._websocket_client()
_LOGGER.error(
"Failed authentication to Home-Assistant websocket: %s", data)
raise HomeAssistantAuthError()
except (RuntimeError, HomeAssistantAPIError) as err:
except (RuntimeError, ValueError) as err:
_LOGGER.error("Client error on websocket API %s.", err)
except HomeAssistantAuthError as err:
_LOGGER.error("Failed authentication to HomeAssistant websocket")
raise HTTPBadGateway()
@@ -175,7 +182,11 @@ class APIProxy(CoreSysAttributes):
response = await server.receive_json()
hassio_token = (response.get('api_password') or
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:
_LOGGER.warning("Unauthorized websocket access!")

View File

@@ -3,18 +3,28 @@ import logging
import re
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 ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
NO_SECURITY_CHECK = set((
re.compile(r"^/homeassistant/api/.*$"),
re.compile(r"^/homeassistant/websocket$"),
re.compile(r"^/supervisor/ping$"),
))
NO_SECURITY_CHECK = re.compile(
r"^(?:"
r"|/homeassistant/api/.*$"
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):
@@ -27,33 +37,50 @@ class SecurityMiddleware(CoreSysAttributes):
@middleware
async def token_validation(self, request, handler):
"""Check security access of this layer."""
request_from = None
hassio_token = request.headers.get(HEADER_TOKEN)
# Ignore security check
for rule in NO_SECURITY_CHECK:
if rule.match(request.path):
_LOGGER.debug("Passthrough %s", request.path)
return await handler(request)
if NO_SECURITY_CHECK.match(request.path):
_LOGGER.debug("Passthrough %s", request.path)
return await handler(request)
# Not token
if not hassio_token:
_LOGGER.warning("No API token provided for %s", request.path)
raise HTTPUnauthorized()
# 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)
request[REQUEST_FROM] = 'homeassistant'
request_from = 'homeassistant'
# Host
if hassio_token == self.sys_machine_id:
_LOGGER.debug("%s access from Host", request.path)
request[REQUEST_FROM] = 'host'
request_from = 'host'
# Add-on
addon = self.sys_addons.from_uuid(hassio_token) \
if hassio_token else None
if addon:
_LOGGER.info("%s access from %s", request.path, addon.slug)
request[REQUEST_FROM] = addon.slug
addon = None
if hassio_token and not request_from:
addon = self.sys_addons.from_token(hassio_token)
# Need removed with 131
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)
_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."""
try:
answer = await method(api, *args, **kwargs)
except RuntimeError as err:
return api_return_error(message=str(err))
except HassioError:
return api_return_error()
except RuntimeError as err:
return api_return_error(message=str(err))
if isinstance(answer, dict):
return api_return_ok(data=answer)

View File

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

View File

@@ -2,8 +2,11 @@
from datetime import datetime
import logging
import os
import re
from pathlib import Path, PurePath
import pytz
from .const import (
FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_TIMEZONE, ATTR_ADDONS_CUSTOM_LIST,
ATTR_LAST_BOOT, ATTR_WAIT_BOOT)
@@ -29,6 +32,8 @@ APPARMOR_DATA = PurePath("apparmor")
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
RE_TIMEZONE = re.compile(r"time_zone: (?P<timezone>[\w/\-+]+)")
class CoreConfig(JsonConfig):
"""Hold all core config data."""
@@ -40,7 +45,21 @@ class CoreConfig(JsonConfig):
@property
def timezone(self):
"""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
def timezone(self, value):
@@ -83,12 +102,12 @@ class CoreConfig(JsonConfig):
return PurePath(os.environ['SUPERVISOR_SHARE'])
@property
def path_extern_config(self):
def path_extern_homeassistant(self):
"""Return config path extern for docker."""
return str(PurePath(self.path_extern_hassio, HOMEASSISTANT_CONFIG))
@property
def path_config(self):
def path_homeassistant(self):
"""Return config path inside supervisor."""
return Path(HASSIO_DATA, HOMEASSISTANT_CONFIG)

View File

@@ -2,7 +2,7 @@
from pathlib import Path
from ipaddress import ip_network
HASSIO_VERSION = '121'
HASSIO_VERSION = '130'
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
URL_HASSIO_VERSION = \
@@ -178,6 +178,11 @@ ATTR_HASSOS_CLI = 'hassos_cli'
ATTR_VERSION_CLI = 'version_cli'
ATTR_VERSION_CLI_LATEST = 'version_cli_latest'
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'
@@ -226,6 +231,14 @@ SECURITY_PROFILE = 'profile'
SECURITY_DEFAULT = 'default'
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_REBOOT = 'reboot'
FEATURES_HASSOS = 'hassos'

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
"""HomeAssistant control object."""
import asyncio
from contextlib import asynccontextmanager, suppress
from datetime import datetime, timedelta
import logging
import os
import re
from pathlib import Path
import socket
import time
@@ -14,14 +16,14 @@ import attr
from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID,
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)
from .coresys import CoreSysAttributes
from .docker.homeassistant import DockerHomeAssistant
from .exceptions import (
HomeAssistantUpdateError, HomeAssistantError, HomeAssistantAPIError,
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 .validate import SCHEMA_HASS_CONFIG
@@ -45,6 +47,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
self._error_state = False
# We don't persist access tokens. Instead we fetch new ones when needed
self.access_token = None
self._access_token_expires = None
async def load(self):
"""Prepare HomeAssistant object."""
@@ -182,6 +185,11 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
"""Return a UUID of this HomeAssistant."""
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
def refresh_token(self):
"""Return the refresh token to authenticate with HomeAssistant."""
@@ -274,6 +282,14 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async def _start(self):
"""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():
raise HomeAssistantError()
await self._block_till_run()
@@ -351,11 +367,12 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async def ensure_access_token(self):
"""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
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",
timeout=30,
data={
@@ -364,14 +381,14 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
}
) as resp:
if resp.status != 200:
_LOGGER.error("Authenticate problem with HomeAssistant!")
_LOGGER.error("Can't update HomeAssistant access token!")
raise HomeAssistantAuthError()
_LOGGER.info("Updated HomeAssistant API token")
tokens = await resp.json()
self.access_token = tokens['access_token']
return
_LOGGER.error("Can't update HomeAssistant access token!")
raise HomeAssistantAPIError()
self._access_token_expires = \
datetime.utcnow() + timedelta(seconds=tokens['expires_in'])
@asynccontextmanager
async def make_request(self, method, path, json=None, content_type=None,
@@ -394,15 +411,20 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
await self.ensure_access_token()
headers[hdrs.AUTHORIZATION] = f'Bearer {self.access_token}'
async with getattr(self.sys_websession_ssl, method)(
url, data=data, timeout=timeout, json=json, headers=headers
) as resp:
# Access token expired
if resp.status == 401 and self.refresh_token:
self.access_token = None
continue
yield resp
return
try:
async with getattr(self.sys_websession_ssl, method)(
url, data=data, timeout=timeout, json=json,
headers=headers
) as resp:
# Access token expired
if resp.status == 401 and self.refresh_token:
self.access_token = None
continue
yield resp
return
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error on call %s: %s", url, err)
break
raise HomeAssistantAPIError()
@@ -433,6 +455,9 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async def _block_till_run(self):
"""Block until Home-Assistant is booting up or startup timeout."""
start_time = time.monotonic()
migration_progress = False
migration_file = Path(
self.sys_config.path_homeassistant, '.migration_progress')
def check_port():
"""Check if port is mapped."""
@@ -448,21 +473,39 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
pass
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
if await self.sys_run_in_executor(check_port):
_LOGGER.info("Detect a running HomeAssistant instance")
self._error_state = False
return
# wait and don't hit the system
await asyncio.sleep(10)
# 3
# 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
if not await self.instance.is_running():
_LOGGER.error("Home Assistant is crashed!")
# 4
# Timeout
if time.monotonic() - start_time > self.wait_boot:
_LOGGER.warning("Don't wait anymore of HomeAssistant startup!")
break
_LOGGER.warning("Don't wait anymore of HomeAssistant startup!")
self._error_state = True
raise HomeAssistantError()

View File

@@ -293,6 +293,7 @@ class SnapshotManager(CoreSysAttributes):
# Stop Home-Assistant if they will be restored later
if homeassistant and FOLDER_HOMEASSISTANT in folders:
await self.sys_homeassistant.stop()
snapshot.restore_homeassistant()
# Process folders
if folders:
@@ -304,7 +305,6 @@ class SnapshotManager(CoreSysAttributes):
if homeassistant:
_LOGGER.info("Restore %s run Home-Assistant",
snapshot.slug)
snapshot.restore_homeassistant()
task_hass = self.sys_create_task(
self.sys_homeassistant.update(
snapshot.homeassistant_version))
@@ -322,12 +322,20 @@ class SnapshotManager(CoreSysAttributes):
_LOGGER.info("Restore %s old add-ons", snapshot.slug)
await snapshot.restore_addons(addon_list)
# make sure homeassistant run agen
# Make sure homeassistant run agen
if task_hass:
_LOGGER.info("Restore %s wait for Home-Assistant",
snapshot.slug)
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
_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_PORT, ATTR_SSL, ATTR_PASSWORD, ATTR_WATCHDOG, ATTR_BOOT, ATTR_CRYPTO,
ATTR_LAST_VERSION, ATTR_PROTECTED, ATTR_WAIT_BOOT, ATTR_SIZE,
CRYPTO_AES128)
ATTR_REFRESH_TOKEN, CRYPTO_AES128)
from ..coresys import CoreSysAttributes
from ..utils.json import write_json_file
from ..utils.tar import SecureTarFile
@@ -387,6 +387,8 @@ class Snapshot(CoreSysAttributes):
# API/Proxy
self.homeassistant[ATTR_PORT] = self.sys_homeassistant.api_port
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._encrypt_data(self.sys_homeassistant.api_password)
@@ -405,6 +407,8 @@ class Snapshot(CoreSysAttributes):
# API/Proxy
self.sys_homeassistant.api_port = self.homeassistant[ATTR_PORT]
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._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_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT, ATTR_SIZE,
ATTR_LAST_VERSION, ATTR_WAIT_BOOT, ATTR_PROTECTED, ATTR_CRYPTO,
ATTR_REFRESH_TOKEN,
FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
SNAPSHOT_FULL, SNAPSHOT_PARTIAL, CRYPTO_AES128)
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_PORT, default=8123): NETWORK_PORT,
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_WAIT_BOOT, default=600):
vol.All(vol.Coerce(int), vol.Range(min=60)),

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ from .const import (
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,
CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV)
@@ -84,6 +85,7 @@ DOCKER_PORTS = vol.Schema({
SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex):
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.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
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.snapshots'
],
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"
]
include_package_data=True
)

View File

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