mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-08 17:53:10 +02:00
Make VAPID subject configurable via MESHCORE_VAPID_SUBJECT
iOS/Safari (Apple APNs) rejects the hard-coded mailto:noreply@meshcore.local VAPID subject with 403 BadJwtToken because .local is a reserved TLD; FCM accepts it, so only Apple devices were affected. Add MESHCORE_VAPID_SUBJECT (default unchanged) resolved via a new get_vapid_claims() in app/push/vapid.py, used by both dispatch and the test-notification endpoint. Closes #288
This commit is contained in:
@@ -182,6 +182,7 @@ Web Push is a standalone subsystem in `app/push/`, separate from the fanout modu
|
||||
|
||||
- **Not a fanout module** — Web Push manages per-browser subscriptions (N browsers, each with its own endpoint and delivery state), unlike fanout which is one-config-to-one-destination.
|
||||
- **VAPID keys**: auto-generated P-256 key pair on first startup, stored in `app_settings.vapid_private_key` / `vapid_public_key`. Cached in-module by `app/push/vapid.py`.
|
||||
- **VAPID subject**: the JWT `sub` claim comes from `get_vapid_claims()` in `app/push/vapid.py`, configurable via `MESHCORE_VAPID_SUBJECT` (default `mailto:noreply@meshcore.local`). Apple's APNs rejects `.local` subjects with `403 BadJwtToken`, so iOS/Safari deployments must set a real `mailto:`/`https:` contact.
|
||||
- **Dispatch**: `broadcast_event()` in `websocket.py` fires `push_manager.dispatch_message(data)` alongside fanout for `message` events. The manager checks the global `app_settings.push_conversations` list, then sends to all currently registered subscriptions via `pywebpush` (run in a thread executor).
|
||||
- **Stale cleanup**: HTTP 404/410 from the push service triggers immediate subscription deletion.
|
||||
- **Subscriptions stored** in `push_subscriptions` table with `UNIQUE(endpoint)` for upsert semantics.
|
||||
|
||||
@@ -31,6 +31,7 @@ class Settings(BaseSettings):
|
||||
skip_post_connect_sync: bool = False
|
||||
basic_auth_username: str = ""
|
||||
basic_auth_password: str = ""
|
||||
vapid_subject: str = "mailto:noreply@meshcore.local"
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_transport_exclusivity(self) -> "Settings":
|
||||
|
||||
+2
-3
@@ -13,7 +13,7 @@ from dataclasses import dataclass
|
||||
from pywebpush import WebPushException
|
||||
|
||||
from app.push.send import send_push
|
||||
from app.push.vapid import get_vapid_private_key
|
||||
from app.push.vapid import get_vapid_claims, get_vapid_private_key
|
||||
from app.repository.channels import ChannelRepository
|
||||
from app.repository.push_subscriptions import PushSubscriptionRepository
|
||||
from app.repository.settings import AppSettingsRepository
|
||||
@@ -21,7 +21,6 @@ from app.repository.settings import AppSettingsRepository
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SEND_TIMEOUT = 15 # seconds per push send
|
||||
_VAPID_CLAIMS = {"sub": "mailto:noreply@meshcore.local"}
|
||||
|
||||
|
||||
def _state_key_for_message(data: dict) -> str:
|
||||
@@ -161,7 +160,7 @@ class PushManager:
|
||||
subscription_info=_subscription_info(sub),
|
||||
payload=payload,
|
||||
vapid_private_key=vapid_key,
|
||||
vapid_claims=_VAPID_CLAIMS,
|
||||
vapid_claims=get_vapid_claims(),
|
||||
)
|
||||
result.success = True
|
||||
except WebPushException as e:
|
||||
|
||||
@@ -11,6 +11,7 @@ import logging
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
from py_vapid import Vapid
|
||||
|
||||
from app.config import settings
|
||||
from app.repository.settings import AppSettingsRepository
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -58,3 +59,14 @@ def get_vapid_public_key() -> str:
|
||||
def get_vapid_private_key() -> str:
|
||||
"""Return the cached VAPID private key (base64url). Must call ensure_vapid_keys() first."""
|
||||
return _cached_private_key
|
||||
|
||||
|
||||
def get_vapid_claims() -> dict[str, str]:
|
||||
"""VAPID JWT claims for Web Push.
|
||||
|
||||
The ``sub`` (subject) claim is configurable via ``MESHCORE_VAPID_SUBJECT``.
|
||||
Apple's push service (APNs) rejects subjects on reserved TLDs such as
|
||||
``.local`` with ``403 BadJwtToken``, so iOS/Safari operators must set this
|
||||
to a real ``mailto:`` or ``https:`` contact.
|
||||
"""
|
||||
return {"sub": settings.vapid_subject}
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field
|
||||
from pywebpush import WebPushException
|
||||
|
||||
from app.push.send import send_push
|
||||
from app.push.vapid import get_vapid_private_key, get_vapid_public_key
|
||||
from app.push.vapid import get_vapid_claims, get_vapid_private_key, get_vapid_public_key
|
||||
from app.repository.push_subscriptions import PushSubscriptionRepository
|
||||
from app.repository.settings import AppSettingsRepository
|
||||
|
||||
@@ -123,7 +123,7 @@ async def test_push(subscription_id: str) -> dict:
|
||||
},
|
||||
payload=payload,
|
||||
vapid_private_key=vapid_key,
|
||||
vapid_claims={"sub": "mailto:noreply@meshcore.local"},
|
||||
vapid_claims=get_vapid_claims(),
|
||||
)
|
||||
return {"status": "sent"}
|
||||
except TimeoutError:
|
||||
|
||||
Reference in New Issue
Block a user