From cefdce50022a620d2c34534daaa1490e03a1a2bb Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sun, 4 Dec 2022 18:34:54 +0100 Subject: [PATCH] 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 --- homeassistant/components/html5/notify.py | 89 +++++++----------------- tests/components/html5/test_notify.py | 51 +++----------- 2 files changed, 33 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 30465c9bd81..30fa5b749ca 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -17,7 +17,6 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.components import websocket_api -from homeassistant.components.frontend import add_manifest_json_key from homeassistant.components.http import HomeAssistantView from homeassistant.components.notify import ( ATTR_DATA, @@ -40,8 +39,6 @@ _LOGGER = logging.getLogger(__name__) 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_PRV_KEY = "vapid_prv_key" ATTR_VAPID_EMAIL = "vapid_email" @@ -52,7 +49,7 @@ def gcm_api_deprecated(value): if value: _LOGGER.warning( "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," " see https://www.home-assistant.io/integrations/html5/" ) @@ -61,11 +58,11 @@ def gcm_api_deprecated(value): PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Optional(ATTR_GCM_SENDER_ID): vol.All(cv.string, gcm_api_deprecated), - vol.Optional(ATTR_GCM_API_KEY): cv.string, - vol.Optional(ATTR_VAPID_PUB_KEY): cv.string, - vol.Optional(ATTR_VAPID_PRV_KEY): cv.string, - vol.Optional(ATTR_VAPID_EMAIL): cv.string, + vol.Optional("gcm_sender_id"): vol.All(cv.string, gcm_api_deprecated), + vol.Optional("gcm_api_key"): cv.string, + vol.Required(ATTR_VAPID_PUB_KEY): cv.string, + vol.Required(ATTR_VAPID_PRV_KEY): 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: return None - vapid_pub_key = config.get(ATTR_VAPID_PUB_KEY) - vapid_prv_key = config.get(ATTR_VAPID_PRV_KEY) - vapid_email = config.get(ATTR_VAPID_EMAIL) + vapid_pub_key = config[ATTR_VAPID_PUB_KEY] + vapid_prv_key = config[ATTR_VAPID_PRV_KEY] + vapid_email = config[ATTR_VAPID_EMAIL] def websocket_appkey(hass, connection, msg): 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(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( - 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): """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.""" - self._gcm_key = gcm_key self._vapid_prv = vapid_prv self._vapid_email = vapid_email self.registrations = registrations @@ -506,33 +497,26 @@ class HTML5NotificationService(BaseNotificationService): "%s is not a valid HTML5 push notification target", target ) continue + subscription = info[ATTR_SUBSCRIPTION] payload[ATTR_DATA][ATTR_JWT] = add_jwt( timestamp, target, payload[ATTR_TAG], - info[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH], + subscription[ATTR_KEYS][ATTR_AUTH], ) webpusher = WebPusher(info[ATTR_SUBSCRIPTION]) - if self._vapid_prv and self._vapid_email: - vapid_headers = create_vapid_headers( - self._vapid_email, - info[ATTR_SUBSCRIPTION], - self._vapid_prv, - timestamp, - ) - vapid_headers.update({"urgency": priority, "priority": priority}) - response = webpusher.send( - 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) + + endpoint = urlparse(subscription[ATTR_ENDPOINT]) + vapid_claims = { + "sub": f"mailto:{self._vapid_email}", + "aud": f"{endpoint.scheme}://{endpoint.netloc}", + "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}) + response = webpusher.send( + data=json.dumps(payload), headers=vapid_headers, ttl=ttl + ) if response.status_code == 410: _LOGGER.info("Notification channel has expired") @@ -564,26 +548,3 @@ def add_jwt(timestamp, target, tag, jwt_secret): ATTR_TAG: tag, } 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 diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index b77986441ed..b51d31ebcdd 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -12,6 +12,7 @@ from homeassistant.setup import async_setup_component CONFIG_FILE = "file.conf" VAPID_CONF = { + "platform": "html5", "vapid_pub_key": "BJMA2gDZEkHaXRhf1fhY_" + "QbKbhVIHlSJXI0bFyo0eJXnUPOjdgycCAbj-2bMKMKNKs" + "_rM8JoSnyKGCXAY2dbONI", @@ -70,7 +71,7 @@ async def mock_client(hass, hass_client, registrations=None): with patch( "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() return await hass_client() @@ -85,7 +86,7 @@ class TestHtml5Notify: m = mock_open() 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 @@ -99,7 +100,7 @@ class TestHtml5Notify: 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"}) + service = html5.get_service(hass, VAPID_CONF) assert service is not None @@ -111,7 +112,7 @@ class TestHtml5Notify: assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] # 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["tag"] == "test" @@ -126,7 +127,7 @@ class TestHtml5Notify: 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"}) + service = html5.get_service(hass, VAPID_CONF) assert service is not None @@ -140,39 +141,11 @@ class TestHtml5Notify: assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] # 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["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") def test_fcm_key_include(self, mock_wp): """Test if the FCM header is included.""" @@ -266,14 +239,6 @@ class TestHtml5Notify: 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): """Test that the HTML view works.""" 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"] # 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["icon"] == "beer.png"