Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new cloud actions commands #219

Merged
merged 9 commits into from
Jan 20, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 24 additions & 30 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
{
"name": "Hass-NabuCasa Dev",
"context": "..",
"dockerFile": "Dockerfile",
"postCreateCommand": "pip3 install -e .",
"runArgs": [
"-e",
"GIT_EDTIOR=code --wait"
],
"extensions": [
"ms-python.python",
"visualstudioexptteam.vscodeintellicode",
"ms-azure-devops.azure-pipelines",
"esbenp.prettier-vscode"
],
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"python.formatting.blackArgs": [
"--target-version",
"py36"
],
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"terminal.integrated.shell.linux": "/bin/bash"
}
}
"name": "Hass-NabuCasa Dev",
"context": "..",
"dockerFile": "Dockerfile",
"postCreateCommand": "pip3 install -e .",
"runArgs": ["-e", "GIT_EDTIOR=code --wait"],
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode",
"esbenp.prettier-vscode"
],
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"python.formatting.blackArgs": ["--target-version", "py36"],
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"terminal.integrated.shell.linux": "/bin/bash"
}
}
21 changes: 21 additions & 0 deletions hass_nabucasa/iot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import pprint
import uuid
import random

from . import iot_base
from .utils import Registry
Expand Down Expand Up @@ -135,6 +136,26 @@ async def async_handle_cloud(cloud, payload):
_LOGGER.error(
"You have been logged out from Home Assistant cloud: %s", payload["reason"]
)
elif action == "disconnect_remote":
pvizeli marked this conversation as resolved.
Show resolved Hide resolved
# Disconect Remote connection
await cloud.remote.disconnect(clear_snitun_token=True)
elif action == "evaluate_remote_security":

async def _reconnect() -> None:
"""Reconnect after a random timeout."""
await asyncio.sleep(random.randint(60, 7200))
await cloud.remote.disconnect(clear_snitun_token=True)
await cloud.remote.connect()

# Reconnect to remote frontends
cloud.client.loop.create_task(_reconnect())
elif action in ("user_notification", "critical_user_notification"):
# Send user Notification
cloud.client.user_message(
"homeassistant_cloud_notification",
payload["title"],
payload["message"],
)
else:
_LOGGER.warning("Received unknown cloud action: %s", action)

Expand Down
25 changes: 20 additions & 5 deletions hass_nabucasa/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,10 @@ async def _refresh_snitun_token(self) -> None:
try:
async with async_timeout.timeout(30):
resp = await cloud_api.async_remote_token(self.cloud, aes_key, aes_iv)
if resp.status == 409:
raise RemoteInsecureVersion()
if resp.status != 200:
raise RemoteBackendError()
if resp.status == 409:
raise RemoteInsecureVersion()
if resp.status != 200:
raise RemoteBackendError()
except (asyncio.TimeoutError, aiohttp.ClientError):
raise RemoteBackendError() from None

Expand All @@ -281,6 +281,18 @@ async def _refresh_snitun_token(self) -> None:
data["throttling"],
)

async def check_version_security(self) -> bool:
pvizeli marked this conversation as resolved.
Show resolved Hide resolved
"""Evaluate core version security and return True if we are safe."""
try:
async with async_timeout.timeout(30):
resp = await cloud_api.async_remote_token(self.cloud, b"", b"")
if resp.status == 409:
return False
except (asyncio.TimeoutError, aiohttp.ClientError):
pass

return True

async def connect(self) -> None:
"""Connect to snitun server."""
if not self._snitun:
Expand Down Expand Up @@ -326,7 +338,7 @@ async def connect(self) -> None:
elif self._reconnect_task and insecure:
self.cloud.run_task(self.disconnect())

async def disconnect(self) -> None:
async def disconnect(self, clear_snitun_token=False) -> None:
"""Disconnect from snitun server."""
if not self._snitun:
_LOGGER.error("Can't handle request-connection without backend")
Expand All @@ -336,6 +348,9 @@ async def disconnect(self) -> None:
if self._reconnect_task:
self._reconnect_task.cancel()

if clear_snitun_token:
self._token = None

# Check if we already connected
if not self._snitun.is_connected:
return
Expand Down
48 changes: 48 additions & 0 deletions tests/test_iot.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,51 @@ async def test_send_message_answer(loop, cloud_mock_iot):
cloud_iot._response_handler[uuid].set_result({"response": True})
response = await send_task
assert response == {"response": True}


async def test_handling_core_messages_user_notifcation(cloud_mock_iot):
"""Test handling core messages."""
cloud_mock_iot.client.user_message = MagicMock()

await iot.async_handle_cloud(
cloud_mock_iot,
{"action": "user_notification", "title": "Test", "message": "My message"},
)
assert len(cloud_mock_iot.client.user_message.mock_calls) == 1


async def test_handling_core_messages_critical_user_notifcation(cloud_mock_iot):
"""Test handling core messages."""
cloud_mock_iot.client.user_message = MagicMock()

await iot.async_handle_cloud(
cloud_mock_iot,
{
"action": "critical_user_notification",
"title": "Test",
"message": "My message",
},
)
assert len(cloud_mock_iot.client.user_message.mock_calls) == 1


async def test_handling_core_messages_remote_disconnect(cloud_mock_iot):
"""Test handling core messages."""
cloud_mock_iot.remote.disconnect = AsyncMock()

await iot.async_handle_cloud(
cloud_mock_iot,
{"action": "disconnect_remote"},
)
assert len(cloud_mock_iot.remote.disconnect.mock_calls) == 1


async def test_handling_core_messages_evaluate_remote_security(cloud_mock_iot):
"""Test handling core messages."""
cloud_mock_iot.client = MagicMock()

await iot.async_handle_cloud(
cloud_mock_iot,
{"action": "evaluate_remote_security"},
)
assert len(cloud_mock_iot.client.loop.mock_calls) == 1
44 changes: 43 additions & 1 deletion tests/test_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

from .common import MockAcme, MockSnitun

# pylint: disable=protected-access


@pytest.fixture(autouse=True)
def ignore_context():
Expand Down Expand Up @@ -309,6 +311,7 @@ async def test_call_disconnect(
await remote.disconnect()
assert snitun_mock.call_disconnect
assert not remote.is_connected
assert remote._token
assert auth_cloud_mock.client.mock_dispatcher[-1][0] == DISPATCH_REMOTE_DISCONNECT


Expand Down Expand Up @@ -507,7 +510,7 @@ async def test_load_connect_insecure(
"valid": valid.timestamp(),
"throttling": 400,
},
status=409
status=409,
)

auth_cloud_mock.client.prop_remote_autostart = True
Expand All @@ -523,3 +526,42 @@ async def test_load_connect_insecure(

assert not snitun_mock.call_connect
assert auth_cloud_mock.client.mock_dispatcher[-1][0] == DISPATCH_REMOTE_BACKEND_UP


async def test_call_disconnect_clean_token(
auth_cloud_mock, acme_mock, mock_cognito, aioclient_mock, snitun_mock
):
"""Initialize backend."""
valid = utcnow() + timedelta(days=1)
auth_cloud_mock.remote_api_url = "https://test.local/api"
remote = RemoteUI(auth_cloud_mock)

aioclient_mock.post(
"https://test.local/api/register_instance",
json={
"domain": "test.dui.nabu.casa",
"email": "test@nabucasa.inc",
"server": "rest-remote.nabu.casa",
},
)
aioclient_mock.post(
"https://test.local/api/snitun_token",
json={
"token": "test-token",
"server": "rest-remote.nabu.casa",
"valid": valid.timestamp(),
"throttling": 400,
},
)

assert not remote.is_connected
await remote.load_backend()
await asyncio.sleep(0.1)
assert remote.is_connected
assert remote._token

await remote.disconnect(clear_snitun_token=True)
assert snitun_mock.call_disconnect
assert not remote.is_connected
assert remote._token is None
assert auth_cloud_mock.client.mock_dispatcher[-1][0] == DISPATCH_REMOTE_DISCONNECT