mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Remove deprecated GCM API from html5 (#83229)
* Remove deprecated GCM API * Cleanup code after removing GCM * Make vapid required * Use dict[key] instead of dict.get(key) for vapid config
This commit is contained in:
parent
1577f6ea50
commit
cefdce5002
@ -17,7 +17,6 @@ import voluptuous as vol
|
|||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
from homeassistant.components.frontend import add_manifest_json_key
|
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.components.notify import (
|
from homeassistant.components.notify import (
|
||||||
ATTR_DATA,
|
ATTR_DATA,
|
||||||
@ -40,8 +39,6 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
REGISTRATIONS_FILE = "html5_push_registrations.conf"
|
REGISTRATIONS_FILE = "html5_push_registrations.conf"
|
||||||
|
|
||||||
ATTR_GCM_SENDER_ID = "gcm_sender_id"
|
|
||||||
ATTR_GCM_API_KEY = "gcm_api_key"
|
|
||||||
ATTR_VAPID_PUB_KEY = "vapid_pub_key"
|
ATTR_VAPID_PUB_KEY = "vapid_pub_key"
|
||||||
ATTR_VAPID_PRV_KEY = "vapid_prv_key"
|
ATTR_VAPID_PRV_KEY = "vapid_prv_key"
|
||||||
ATTR_VAPID_EMAIL = "vapid_email"
|
ATTR_VAPID_EMAIL = "vapid_email"
|
||||||
@ -52,7 +49,7 @@ def gcm_api_deprecated(value):
|
|||||||
if value:
|
if value:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Configuring html5_push_notifications via the GCM api"
|
"Configuring html5_push_notifications via the GCM api"
|
||||||
" has been deprecated and will stop working after April 11,"
|
" has been deprecated and stopped working since May 29,"
|
||||||
" 2019. Use the VAPID configuration instead. For instructions,"
|
" 2019. Use the VAPID configuration instead. For instructions,"
|
||||||
" see https://www.home-assistant.io/integrations/html5/"
|
" see https://www.home-assistant.io/integrations/html5/"
|
||||||
)
|
)
|
||||||
@ -61,11 +58,11 @@ def gcm_api_deprecated(value):
|
|||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_GCM_SENDER_ID): vol.All(cv.string, gcm_api_deprecated),
|
vol.Optional("gcm_sender_id"): vol.All(cv.string, gcm_api_deprecated),
|
||||||
vol.Optional(ATTR_GCM_API_KEY): cv.string,
|
vol.Optional("gcm_api_key"): cv.string,
|
||||||
vol.Optional(ATTR_VAPID_PUB_KEY): cv.string,
|
vol.Required(ATTR_VAPID_PUB_KEY): cv.string,
|
||||||
vol.Optional(ATTR_VAPID_PRV_KEY): cv.string,
|
vol.Required(ATTR_VAPID_PRV_KEY): cv.string,
|
||||||
vol.Optional(ATTR_VAPID_EMAIL): cv.string,
|
vol.Required(ATTR_VAPID_EMAIL): cv.string,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -173,9 +170,9 @@ def get_service(hass, config, discovery_info=None):
|
|||||||
if registrations is None:
|
if registrations is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
vapid_pub_key = config.get(ATTR_VAPID_PUB_KEY)
|
vapid_pub_key = config[ATTR_VAPID_PUB_KEY]
|
||||||
vapid_prv_key = config.get(ATTR_VAPID_PRV_KEY)
|
vapid_prv_key = config[ATTR_VAPID_PRV_KEY]
|
||||||
vapid_email = config.get(ATTR_VAPID_EMAIL)
|
vapid_email = config[ATTR_VAPID_EMAIL]
|
||||||
|
|
||||||
def websocket_appkey(hass, connection, msg):
|
def websocket_appkey(hass, connection, msg):
|
||||||
connection.send_message(websocket_api.result_message(msg["id"], vapid_pub_key))
|
connection.send_message(websocket_api.result_message(msg["id"], vapid_pub_key))
|
||||||
@ -187,13 +184,8 @@ def get_service(hass, config, discovery_info=None):
|
|||||||
hass.http.register_view(HTML5PushRegistrationView(registrations, json_path))
|
hass.http.register_view(HTML5PushRegistrationView(registrations, json_path))
|
||||||
hass.http.register_view(HTML5PushCallbackView(registrations))
|
hass.http.register_view(HTML5PushCallbackView(registrations))
|
||||||
|
|
||||||
gcm_api_key = config.get(ATTR_GCM_API_KEY)
|
|
||||||
|
|
||||||
if config.get(ATTR_GCM_SENDER_ID) is not None:
|
|
||||||
add_manifest_json_key(ATTR_GCM_SENDER_ID, config.get(ATTR_GCM_SENDER_ID))
|
|
||||||
|
|
||||||
return HTML5NotificationService(
|
return HTML5NotificationService(
|
||||||
hass, gcm_api_key, vapid_prv_key, vapid_email, registrations, json_path
|
hass, vapid_prv_key, vapid_email, registrations, json_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -399,9 +391,8 @@ class HTML5PushCallbackView(HomeAssistantView):
|
|||||||
class HTML5NotificationService(BaseNotificationService):
|
class HTML5NotificationService(BaseNotificationService):
|
||||||
"""Implement the notification service for HTML5."""
|
"""Implement the notification service for HTML5."""
|
||||||
|
|
||||||
def __init__(self, hass, gcm_key, vapid_prv, vapid_email, registrations, json_path):
|
def __init__(self, hass, vapid_prv, vapid_email, registrations, json_path):
|
||||||
"""Initialize the service."""
|
"""Initialize the service."""
|
||||||
self._gcm_key = gcm_key
|
|
||||||
self._vapid_prv = vapid_prv
|
self._vapid_prv = vapid_prv
|
||||||
self._vapid_email = vapid_email
|
self._vapid_email = vapid_email
|
||||||
self.registrations = registrations
|
self.registrations = registrations
|
||||||
@ -506,33 +497,26 @@ class HTML5NotificationService(BaseNotificationService):
|
|||||||
"%s is not a valid HTML5 push notification target", target
|
"%s is not a valid HTML5 push notification target", target
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
subscription = info[ATTR_SUBSCRIPTION]
|
||||||
payload[ATTR_DATA][ATTR_JWT] = add_jwt(
|
payload[ATTR_DATA][ATTR_JWT] = add_jwt(
|
||||||
timestamp,
|
timestamp,
|
||||||
target,
|
target,
|
||||||
payload[ATTR_TAG],
|
payload[ATTR_TAG],
|
||||||
info[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH],
|
subscription[ATTR_KEYS][ATTR_AUTH],
|
||||||
)
|
)
|
||||||
webpusher = WebPusher(info[ATTR_SUBSCRIPTION])
|
webpusher = WebPusher(info[ATTR_SUBSCRIPTION])
|
||||||
if self._vapid_prv and self._vapid_email:
|
|
||||||
vapid_headers = create_vapid_headers(
|
endpoint = urlparse(subscription[ATTR_ENDPOINT])
|
||||||
self._vapid_email,
|
vapid_claims = {
|
||||||
info[ATTR_SUBSCRIPTION],
|
"sub": f"mailto:{self._vapid_email}",
|
||||||
self._vapid_prv,
|
"aud": f"{endpoint.scheme}://{endpoint.netloc}",
|
||||||
timestamp,
|
"exp": timestamp + (VAPID_CLAIM_VALID_HOURS * 60 * 60),
|
||||||
)
|
}
|
||||||
|
vapid_headers = Vapid.from_string(self._vapid_prv).sign(vapid_claims)
|
||||||
vapid_headers.update({"urgency": priority, "priority": priority})
|
vapid_headers.update({"urgency": priority, "priority": priority})
|
||||||
response = webpusher.send(
|
response = webpusher.send(
|
||||||
data=json.dumps(payload), headers=vapid_headers, ttl=ttl
|
data=json.dumps(payload), headers=vapid_headers, ttl=ttl
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
# Only pass the gcm key if we're actually using GCM
|
|
||||||
# If we don't, notifications break on FireFox
|
|
||||||
gcm_key = (
|
|
||||||
self._gcm_key
|
|
||||||
if "googleapis.com" in info[ATTR_SUBSCRIPTION][ATTR_ENDPOINT]
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
response = webpusher.send(json.dumps(payload), gcm_key=gcm_key, ttl=ttl)
|
|
||||||
|
|
||||||
if response.status_code == 410:
|
if response.status_code == 410:
|
||||||
_LOGGER.info("Notification channel has expired")
|
_LOGGER.info("Notification channel has expired")
|
||||||
@ -564,26 +548,3 @@ def add_jwt(timestamp, target, tag, jwt_secret):
|
|||||||
ATTR_TAG: tag,
|
ATTR_TAG: tag,
|
||||||
}
|
}
|
||||||
return jwt.encode(jwt_claims, jwt_secret)
|
return jwt.encode(jwt_claims, jwt_secret)
|
||||||
|
|
||||||
|
|
||||||
def create_vapid_headers(vapid_email, subscription_info, vapid_private_key, timestamp):
|
|
||||||
"""Create encrypted headers to send to WebPusher."""
|
|
||||||
|
|
||||||
if (
|
|
||||||
vapid_email
|
|
||||||
and vapid_private_key
|
|
||||||
and ATTR_ENDPOINT in subscription_info
|
|
||||||
and timestamp
|
|
||||||
):
|
|
||||||
vapid_exp = datetime.fromtimestamp(timestamp) + timedelta(
|
|
||||||
hours=VAPID_CLAIM_VALID_HOURS
|
|
||||||
)
|
|
||||||
url = urlparse(subscription_info.get(ATTR_ENDPOINT))
|
|
||||||
vapid_claims = {
|
|
||||||
"sub": f"mailto:{vapid_email}",
|
|
||||||
"aud": f"{url.scheme}://{url.netloc}",
|
|
||||||
"exp": int(vapid_exp.timestamp()),
|
|
||||||
}
|
|
||||||
vapid = Vapid.from_string(private_key=vapid_private_key)
|
|
||||||
return vapid.sign(vapid_claims)
|
|
||||||
return None
|
|
||||||
|
@ -12,6 +12,7 @@ from homeassistant.setup import async_setup_component
|
|||||||
CONFIG_FILE = "file.conf"
|
CONFIG_FILE = "file.conf"
|
||||||
|
|
||||||
VAPID_CONF = {
|
VAPID_CONF = {
|
||||||
|
"platform": "html5",
|
||||||
"vapid_pub_key": "BJMA2gDZEkHaXRhf1fhY_"
|
"vapid_pub_key": "BJMA2gDZEkHaXRhf1fhY_"
|
||||||
+ "QbKbhVIHlSJXI0bFyo0eJXnUPOjdgycCAbj-2bMKMKNKs"
|
+ "QbKbhVIHlSJXI0bFyo0eJXnUPOjdgycCAbj-2bMKMKNKs"
|
||||||
+ "_rM8JoSnyKGCXAY2dbONI",
|
+ "_rM8JoSnyKGCXAY2dbONI",
|
||||||
@ -70,7 +71,7 @@ async def mock_client(hass, hass_client, registrations=None):
|
|||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.html5.notify._load_config", return_value=registrations
|
"homeassistant.components.html5.notify._load_config", return_value=registrations
|
||||||
):
|
):
|
||||||
await async_setup_component(hass, "notify", {"notify": {"platform": "html5"}})
|
await async_setup_component(hass, "notify", {"notify": VAPID_CONF})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
return await hass_client()
|
return await hass_client()
|
||||||
@ -85,7 +86,7 @@ class TestHtml5Notify:
|
|||||||
|
|
||||||
m = mock_open()
|
m = mock_open()
|
||||||
with patch("homeassistant.util.json.open", m, create=True):
|
with patch("homeassistant.util.json.open", m, create=True):
|
||||||
service = html5.get_service(hass, {})
|
service = html5.get_service(hass, VAPID_CONF)
|
||||||
|
|
||||||
assert service is not None
|
assert service is not None
|
||||||
|
|
||||||
@ -99,7 +100,7 @@ class TestHtml5Notify:
|
|||||||
|
|
||||||
m = mock_open(read_data=json.dumps(data))
|
m = mock_open(read_data=json.dumps(data))
|
||||||
with patch("homeassistant.util.json.open", m, create=True):
|
with patch("homeassistant.util.json.open", m, create=True):
|
||||||
service = html5.get_service(hass, {"gcm_sender_id": "100"})
|
service = html5.get_service(hass, VAPID_CONF)
|
||||||
|
|
||||||
assert service is not None
|
assert service is not None
|
||||||
|
|
||||||
@ -111,7 +112,7 @@ class TestHtml5Notify:
|
|||||||
assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"]
|
assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"]
|
||||||
|
|
||||||
# Call to send
|
# Call to send
|
||||||
payload = json.loads(mock_wp.mock_calls[3][1][0])
|
payload = json.loads(mock_wp.mock_calls[3][2]["data"])
|
||||||
|
|
||||||
assert payload["dismiss"] is True
|
assert payload["dismiss"] is True
|
||||||
assert payload["tag"] == "test"
|
assert payload["tag"] == "test"
|
||||||
@ -126,7 +127,7 @@ class TestHtml5Notify:
|
|||||||
|
|
||||||
m = mock_open(read_data=json.dumps(data))
|
m = mock_open(read_data=json.dumps(data))
|
||||||
with patch("homeassistant.util.json.open", m, create=True):
|
with patch("homeassistant.util.json.open", m, create=True):
|
||||||
service = html5.get_service(hass, {"gcm_sender_id": "100"})
|
service = html5.get_service(hass, VAPID_CONF)
|
||||||
|
|
||||||
assert service is not None
|
assert service is not None
|
||||||
|
|
||||||
@ -140,39 +141,11 @@ class TestHtml5Notify:
|
|||||||
assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"]
|
assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"]
|
||||||
|
|
||||||
# Call to send
|
# Call to send
|
||||||
payload = json.loads(mock_wp.mock_calls[3][1][0])
|
payload = json.loads(mock_wp.mock_calls[3][2]["data"])
|
||||||
|
|
||||||
assert payload["body"] == "Hello"
|
assert payload["body"] == "Hello"
|
||||||
assert payload["icon"] == "beer.png"
|
assert payload["icon"] == "beer.png"
|
||||||
|
|
||||||
@patch("homeassistant.components.html5.notify.WebPusher")
|
|
||||||
def test_gcm_key_include(self, mock_wp):
|
|
||||||
"""Test if the gcm_key is only included for GCM endpoints."""
|
|
||||||
hass = MagicMock()
|
|
||||||
mock_wp().send().status_code = 201
|
|
||||||
|
|
||||||
data = {"chrome": SUBSCRIPTION_1, "firefox": SUBSCRIPTION_2}
|
|
||||||
|
|
||||||
m = mock_open(read_data=json.dumps(data))
|
|
||||||
with patch("homeassistant.util.json.open", m, create=True):
|
|
||||||
service = html5.get_service(
|
|
||||||
hass, {"gcm_sender_id": "100", "gcm_api_key": "Y6i0JdZ0mj9LOaSI"}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert service is not None
|
|
||||||
|
|
||||||
service.send_message("Hello", target=["chrome", "firefox"])
|
|
||||||
|
|
||||||
assert len(mock_wp.mock_calls) == 6
|
|
||||||
|
|
||||||
# WebPusher constructor
|
|
||||||
assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"]
|
|
||||||
assert mock_wp.mock_calls[4][1][0] == SUBSCRIPTION_2["subscription"]
|
|
||||||
|
|
||||||
# Get the keys passed to the WebPusher's send method
|
|
||||||
assert mock_wp.mock_calls[3][2]["gcm_key"] is not None
|
|
||||||
assert mock_wp.mock_calls[5][2]["gcm_key"] is None
|
|
||||||
|
|
||||||
@patch("homeassistant.components.html5.notify.WebPusher")
|
@patch("homeassistant.components.html5.notify.WebPusher")
|
||||||
def test_fcm_key_include(self, mock_wp):
|
def test_fcm_key_include(self, mock_wp):
|
||||||
"""Test if the FCM header is included."""
|
"""Test if the FCM header is included."""
|
||||||
@ -266,14 +239,6 @@ class TestHtml5Notify:
|
|||||||
assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal"
|
assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal"
|
||||||
|
|
||||||
|
|
||||||
def test_create_vapid_withoutvapid():
|
|
||||||
"""Test creating empty vapid."""
|
|
||||||
resp = html5.create_vapid_headers(
|
|
||||||
vapid_email=None, vapid_private_key=None, subscription_info=None, timestamp=None
|
|
||||||
)
|
|
||||||
assert resp is None
|
|
||||||
|
|
||||||
|
|
||||||
async def test_registering_new_device_view(hass, hass_client):
|
async def test_registering_new_device_view(hass, hass_client):
|
||||||
"""Test that the HTML view works."""
|
"""Test that the HTML view works."""
|
||||||
client = await mock_client(hass, hass_client)
|
client = await mock_client(hass, hass_client)
|
||||||
@ -479,7 +444,7 @@ async def test_callback_view_with_jwt(hass, hass_client):
|
|||||||
assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"]
|
assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"]
|
||||||
|
|
||||||
# Call to send
|
# Call to send
|
||||||
push_payload = json.loads(mock_wp.mock_calls[3][1][0])
|
push_payload = json.loads(mock_wp.mock_calls[3][2]["data"])
|
||||||
|
|
||||||
assert push_payload["body"] == "Hello"
|
assert push_payload["body"] == "Hello"
|
||||||
assert push_payload["icon"] == "beer.png"
|
assert push_payload["icon"] == "beer.png"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user