This commit is contained in:
dgtlmoon
2025-08-28 22:11:14 +02:00
parent cfdb484b36
commit 533f3fec1d
3 changed files with 106 additions and 13 deletions
@@ -11,32 +11,44 @@ from loguru import logger
def convert_pem_private_key_for_pywebpush(private_key):
"""
Convert PEM private key to the raw bytes format that pywebpush expects
Convert PEM private key to the format that pywebpush expects
Args:
private_key: PEM private key string or already converted key
Returns:
Private key in the format pywebpush expects
Private key in the format pywebpush expects (PEM string for pywebpush)
"""
if not isinstance(private_key, str) or not private_key.startswith('-----BEGIN'):
# pywebpush expects the PEM string directly
if not isinstance(private_key, str):
return private_key
# If it doesn't look like PEM, return as-is
if not private_key.startswith('-----BEGIN'):
return private_key
try:
from cryptography.hazmat.primitives import serialization
private_key_bytes = private_key.encode()
from cryptography.hazmat.primitives.asymmetric import ec
# Validate the key by loading it
private_key_bytes = private_key.encode('utf-8')
private_key_obj = serialization.load_pem_private_key(private_key_bytes, password=None)
# Get raw private key bytes for pywebpush
private_key_raw = private_key_obj.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption()
)
return private_key_raw
# Verify it's an ECDSA key (required for VAPID)
if not isinstance(private_key_obj, ec.EllipticCurvePrivateKey):
logger.error("Private key is not an ECDSA key - VAPID requires ECDSA")
return private_key
# Ensure the key has the right curve (P-256 for VAPID)
if private_key_obj.curve.name != 'secp256r1':
logger.warning(f"Private key uses curve {private_key_obj.curve.name}, VAPID recommends secp256r1 (P-256)")
# Return the original PEM - pywebpush handles PEM format correctly
return private_key
except Exception as e:
logger.warning(f"Failed to convert private key format, using as-is: {e}")
logger.warning(f"Failed to validate private key format: {e}")
return private_key
@@ -159,6 +159,9 @@ class BrowserNotifications {
return;
}
// First, try to clear any existing subscription with different keys
await this.clearExistingSubscription();
// Create push subscription
const subscription = await this.serviceWorkerRegistration.pushManager.subscribe({
userVisibleOnly: true,
@@ -196,7 +199,13 @@ class BrowserNotifications {
} catch (error) {
console.error(`Failed to subscribe to keyword ${keyword}:`, error);
alert(`Failed to subscribe: ${error.message}`);
// Show user-friendly error message
if (error.message.includes('different applicationServerKey')) {
this.showSubscriptionConflictDialog(keyword, error);
} else {
alert(`Failed to subscribe: ${error.message}`);
}
}
}
@@ -319,6 +328,74 @@ class BrowserNotifications {
return outputArray;
}
async clearExistingSubscription() {
/**
* Clear any existing push subscription that might conflict with our VAPID keys
*/
try {
const existingSubscription = await this.serviceWorkerRegistration.pushManager.getSubscription();
if (existingSubscription) {
console.log('Found existing subscription, unsubscribing...');
await existingSubscription.unsubscribe();
console.log('Successfully cleared existing subscription');
}
} catch (error) {
console.warn('Failed to clear existing subscription:', error);
// Don't throw - this is just cleanup
}
}
showSubscriptionConflictDialog(keyword, error) {
/**
* Show user-friendly dialog for subscription conflicts
*/
const message = `Browser notifications are already set up for a different changedetection.io instance or with different settings.
To fix this:
1. Clear your existing subscription
2. Try subscribing again
Would you like to automatically clear the old subscription and retry?`;
if (confirm(message)) {
this.clearExistingSubscription().then(() => {
// Retry subscription after clearing
setTimeout(() => {
this.subscribeToKeyword(keyword);
}, 500);
});
} else {
alert('To use browser notifications, please manually clear your browser notifications for this site in browser settings, then try again.');
}
}
async clearAllNotifications() {
/**
* Clear all browser notification subscriptions (admin function)
*/
try {
// Clear service worker subscription
const existingSubscription = await this.serviceWorkerRegistration.pushManager.getSubscription();
if (existingSubscription) {
await existingSubscription.unsubscribe();
}
// Clear local storage
this.subscriptions.clear();
// Update UI
this.updateSubscriptionsList();
console.log('All notifications cleared');
alert('All browser notifications have been cleared. You can now subscribe again.');
} catch (error) {
console.error('Failed to clear all notifications:', error);
alert('Failed to clear notifications. Please manually clear them in browser settings.');
}
}
async handleAutoSubscription() {
// Handle auto-subscription for keywords detected from browser:// URLs
try {
@@ -41,6 +41,7 @@
<div class="pure-form-message-inline">
<p><strong>Browser Notification Channels</strong> - These are automatically detected when you use <code>browser://keyword</code> URLs in your notification settings above.</p>
<p>When detected, you'll be prompted to enable browser notifications for these channels so you can receive real-time push notifications even when this tab is closed.</p>
<p><small><strong>Troubleshooting:</strong> If you get "different applicationServerKey" errors, click "Clear All Notifications" below and try again. This happens when switching between different changedetection.io instances.</small></p>
<div id="browser-notification-controls" style="margin-top: 1em;">
<div id="notification-permission-status">
<p>Browser notifications: <span id="permission-status">checking...</span></p>
@@ -52,6 +53,9 @@
<button type="button" id="test-notification-btn" class="pure-button button-secondary button-xsmall" style="display: none;">
Test Notification
</button>
<button type="button" id="clear-notifications-btn" class="pure-button button-secondary button-xsmall" onclick="window.browserNotifications?.clearAllNotifications()" style="margin-left: 0.5em;">
Clear All Notifications
</button>
</div>
</div>
</div>