1818
1919import aiohttp_session
2020from aiohttp import web
21+ from cryptography .hazmat .primitives import serialization
2122from tenacity import (
2223 AsyncRetrying ,
2324 before_sleep_log ,
@@ -71,16 +72,39 @@ def __init__(self, message: str, *, status_code: int | None = None) -> None:
7172
7273
7374class PushNotifier :
75+ @staticmethod
76+ def _strip_wrapping_quotes (value : str ) -> str :
77+ if len (value ) >= 2 and (
78+ (value [0 ] == '"' and value [- 1 ] == '"' ) or (value [0 ] == "'" and value [- 1 ] == "'" )
79+ ):
80+ return value [1 :- 1 ]
81+ return value
82+
83+ @classmethod
84+ def _normalize_vapid_private_key (cls , raw_value : str ) -> str :
85+ key = cls ._strip_wrapping_quotes (raw_value .strip ())
86+ # Accept env values that store PEM newlines as escaped sequences.
87+ if "\\ n" in key :
88+ key = key .replace ("\\ n" , "\n " )
89+ return key .strip ()
90+
91+ @staticmethod
92+ def _validate_vapid_private_key (private_key : str ) -> tuple [bool , str | None ]:
93+ try :
94+ serialization .load_pem_private_key (private_key .encode ("utf-8" ), password = None )
95+ return (True , None )
96+ except Exception as exc :
97+ return (False , f"{ type (exc ).__name__ } : { exc } " )
98+
7499 def __init__ (self , app_state ):
75100 self .app_state = app_state
76101 self .queue : asyncio .Queue [CorrMovePushJob ] = asyncio .Queue (maxsize = PUSH_QUEUE_MAXSIZE )
77102
78- private_key = PUSH_VAPID_PRIVATE_KEY .strip ()
79- if "\\ n" in private_key :
80- private_key = private_key .replace ("\\ n" , "\n " )
103+ private_key = self ._normalize_vapid_private_key (PUSH_VAPID_PRIVATE_KEY )
104+ vapid_public_key = self ._strip_wrapping_quotes (PUSH_VAPID_PUBLIC_KEY .strip ())
81105
82106 self .vapid_private_key = private_key
83- self .vapid_public_key = PUSH_VAPID_PUBLIC_KEY . strip ()
107+ self .vapid_public_key = vapid_public_key
84108 self .vapid_claims : dict [str , str | int ] = {"sub" : PUSH_VAPID_SUBJECT }
85109 self .retry_attempts = PUSH_RETRY_MAX_ATTEMPTS
86110 self .retry_wait_initial_seconds = PUSH_RETRY_WAIT_INITIAL_SECONDS
@@ -101,6 +125,16 @@ def __init__(self, app_state):
101125 log .info ("Web push disabled: PUSH_VAPID_PUBLIC_KEY is not configured" )
102126 elif not self .vapid_private_key :
103127 log .info ("Web push disabled: PUSH_VAPID_PRIVATE_KEY is not configured" )
128+ else :
129+ private_key_ok , private_key_error = self ._validate_vapid_private_key (
130+ self .vapid_private_key
131+ )
132+ if not private_key_ok :
133+ self .enabled = False
134+ log .error (
135+ "Web push disabled: invalid PUSH_VAPID_PRIVATE_KEY format (%s)" ,
136+ private_key_error ,
137+ )
104138
105139 def enqueue_corr_move (self , user : User , game_id : str , opponent : str , san : str ) -> None :
106140 """Queue a corr-move notification when the user is eligible."""
@@ -317,8 +351,7 @@ async def _send_with_retry(self, subscription_info: dict, payload: str) -> None:
317351 raise
318352 except Exception as exc :
319353 raise PushSendRetryableError (
320- "Retryable push delivery failure: "
321- f"{ type (exc ).__name__ } : { exc } "
354+ f"Retryable push delivery failure: { type (exc ).__name__ } : { exc } "
322355 ) from exc
323356
324357
0 commit comments