Security:
- Add token-based WebSocket authentication (closes critical security gap)
- In-memory token store with 24h TTL (token_store.py)
- POST /api/auth/token exchanges Basic Auth for WS token
- GET /api/auth/status public endpoint for auth check
- WebSocket validates token query param, rejects with close code 4401
- Frontend LoginPrompt modal for credential entry
- WebSocket manager handles full auth flow with auth_required state
- No-op when AUTH_ENABLED=false (preserves existing behavior)
HTTPS:
- Wire HTTPS toggle in Settings UI (POST /api/system/ssl/toggle)
- Add certificate regeneration button
- Display SSL info (expiration, SANs, SHA256 fingerprint)
Plugins:
- Wire trigger_hook("pm3_command") in PM3 service
- Wire trigger_hook("update_check") in update manager
Build/Infrastructure:
- Enable NetworkManager in pi-gen AP setup stage
- Add HF booster board detection patch for Proxmark3
- Update LED PWM control patch
- Fix BLE adapter, UPS drivers, WiFi manager improvements
- Update HTTPS support stage script
Documentation:
- Update PROJECT_STATUS.md and IMPLEMENTATION_PRIORITIES.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
480 lines
17 KiB
Python
480 lines
17 KiB
Python
"""BlueZ GATT adapter using the bless library.
|
|
|
|
This module bridges our GATT server handlers to BlueZ via bless,
|
|
enabling BLE peripheral functionality on Linux.
|
|
|
|
Architecture:
|
|
bless BLEServer (handles BlueZ D-Bus)
|
|
|
|
|
BlueZGATTAdapter (this file - bridges handlers)
|
|
|
|
|
DangerousPiGATTServer (handlers call service layer)
|
|
|
|
|
Service Layer (business logic)
|
|
"""
|
|
import asyncio
|
|
import logging
|
|
import sys
|
|
import threading
|
|
from typing import Dict, Optional, Any, Union
|
|
|
|
from bless import (
|
|
BlessServer,
|
|
BlessGATTCharacteristic,
|
|
GATTCharacteristicProperties,
|
|
GATTAttributePermissions,
|
|
)
|
|
|
|
from .gatt_server import DangerousPiGATTServer
|
|
from .characteristics import (
|
|
PM3CharacteristicUUIDs,
|
|
WiFiCharacteristicUUIDs,
|
|
SystemCharacteristicUUIDs,
|
|
UpdateCharacteristicUUIDs,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Device name for BLE advertising
|
|
DEFAULT_DEVICE_NAME = "Dangerous-Pi"
|
|
|
|
|
|
class BlueZGATTAdapter:
|
|
"""Adapter connecting bless BLE server to our GATT handlers.
|
|
|
|
This class:
|
|
- Initializes the bless BLE server
|
|
- Registers all services and characteristics via GATT dictionary
|
|
- Routes read/write requests to DangerousPiGATTServer handlers
|
|
- Sends notifications via bless
|
|
"""
|
|
|
|
def __init__(self, device_name: str = DEFAULT_DEVICE_NAME):
|
|
"""Initialize the BlueZ GATT adapter.
|
|
|
|
Args:
|
|
device_name: BLE device name for advertising
|
|
"""
|
|
self.device_name = device_name
|
|
self.server: Optional[BlessServer] = None
|
|
self.gatt_server = DangerousPiGATTServer()
|
|
self._is_running = False
|
|
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
|
|
# Platform-specific trigger for async coordination
|
|
self._trigger: Union[asyncio.Event, threading.Event]
|
|
if sys.platform in ["darwin", "win32"]:
|
|
self._trigger = threading.Event()
|
|
else:
|
|
self._trigger = asyncio.Event()
|
|
|
|
@property
|
|
def is_running(self) -> bool:
|
|
"""Check if BLE server is running."""
|
|
return self._is_running
|
|
|
|
def _build_gatt_dict(self) -> Dict:
|
|
"""Build the GATT dictionary for bless.
|
|
|
|
Returns:
|
|
Dictionary mapping service UUIDs to characteristic definitions
|
|
"""
|
|
return {
|
|
# PM3 Service
|
|
PM3CharacteristicUUIDs.SERVICE: {
|
|
PM3CharacteristicUUIDs.COMMAND_WRITE: {
|
|
"Properties": GATTCharacteristicProperties.write,
|
|
"Permissions": GATTAttributePermissions.writeable,
|
|
"Value": None,
|
|
},
|
|
PM3CharacteristicUUIDs.COMMAND_RESULT: {
|
|
"Properties": (
|
|
GATTCharacteristicProperties.read |
|
|
GATTCharacteristicProperties.notify
|
|
),
|
|
"Permissions": GATTAttributePermissions.readable,
|
|
"Value": bytearray(b'{}'),
|
|
},
|
|
PM3CharacteristicUUIDs.STATUS: {
|
|
"Properties": GATTCharacteristicProperties.read,
|
|
"Permissions": GATTAttributePermissions.readable,
|
|
"Value": bytearray(b'{"connected": false}'),
|
|
},
|
|
PM3CharacteristicUUIDs.SESSION_CREATE: {
|
|
"Properties": GATTCharacteristicProperties.write,
|
|
"Permissions": GATTAttributePermissions.writeable,
|
|
"Value": None,
|
|
},
|
|
PM3CharacteristicUUIDs.SESSION_RELEASE: {
|
|
"Properties": GATTCharacteristicProperties.write,
|
|
"Permissions": GATTAttributePermissions.writeable,
|
|
"Value": None,
|
|
},
|
|
},
|
|
# WiFi Service
|
|
WiFiCharacteristicUUIDs.SERVICE: {
|
|
WiFiCharacteristicUUIDs.STATUS: {
|
|
"Properties": GATTCharacteristicProperties.read,
|
|
"Permissions": GATTAttributePermissions.readable,
|
|
"Value": bytearray(b'{"mode": "unknown"}'),
|
|
},
|
|
WiFiCharacteristicUUIDs.SCAN: {
|
|
"Properties": (
|
|
GATTCharacteristicProperties.write |
|
|
GATTCharacteristicProperties.notify
|
|
),
|
|
"Permissions": (
|
|
GATTAttributePermissions.readable |
|
|
GATTAttributePermissions.writeable
|
|
),
|
|
"Value": None,
|
|
},
|
|
WiFiCharacteristicUUIDs.CONNECT: {
|
|
"Properties": GATTCharacteristicProperties.write,
|
|
"Permissions": GATTAttributePermissions.writeable,
|
|
"Value": None,
|
|
},
|
|
WiFiCharacteristicUUIDs.MODE: {
|
|
"Properties": (
|
|
GATTCharacteristicProperties.read |
|
|
GATTCharacteristicProperties.write
|
|
),
|
|
"Permissions": (
|
|
GATTAttributePermissions.readable |
|
|
GATTAttributePermissions.writeable
|
|
),
|
|
"Value": bytearray(b'client'),
|
|
},
|
|
},
|
|
# System Service
|
|
SystemCharacteristicUUIDs.SERVICE: {
|
|
SystemCharacteristicUUIDs.INFO: {
|
|
"Properties": (
|
|
GATTCharacteristicProperties.read |
|
|
GATTCharacteristicProperties.notify
|
|
),
|
|
"Permissions": GATTAttributePermissions.readable,
|
|
"Value": bytearray(b'{}'),
|
|
},
|
|
SystemCharacteristicUUIDs.SHUTDOWN: {
|
|
"Properties": GATTCharacteristicProperties.write,
|
|
"Permissions": GATTAttributePermissions.writeable,
|
|
"Value": None,
|
|
},
|
|
SystemCharacteristicUUIDs.RESTART: {
|
|
"Properties": GATTCharacteristicProperties.write,
|
|
"Permissions": GATTAttributePermissions.writeable,
|
|
"Value": None,
|
|
},
|
|
},
|
|
# Update Service
|
|
UpdateCharacteristicUUIDs.SERVICE: {
|
|
UpdateCharacteristicUUIDs.CHECK: {
|
|
"Properties": (
|
|
GATTCharacteristicProperties.write |
|
|
GATTCharacteristicProperties.notify
|
|
),
|
|
"Permissions": (
|
|
GATTAttributePermissions.readable |
|
|
GATTAttributePermissions.writeable
|
|
),
|
|
"Value": None,
|
|
},
|
|
UpdateCharacteristicUUIDs.PROGRESS: {
|
|
"Properties": (
|
|
GATTCharacteristicProperties.read |
|
|
GATTCharacteristicProperties.notify
|
|
),
|
|
"Permissions": GATTAttributePermissions.readable,
|
|
"Value": bytearray(b'{"progress": 0}'),
|
|
},
|
|
},
|
|
}
|
|
|
|
def _on_read(self, characteristic: BlessGATTCharacteristic) -> bytearray:
|
|
"""Handle BLE read request.
|
|
|
|
Note: This callback is called synchronously from the D-Bus event handler.
|
|
We cannot block on async operations here as it would deadlock the event loop.
|
|
Instead, we return cached values and schedule async updates in the background.
|
|
|
|
Args:
|
|
characteristic: The characteristic being read
|
|
|
|
Returns:
|
|
Characteristic value as bytearray
|
|
"""
|
|
uuid = str(characteristic.uuid).lower()
|
|
logger.info("BLE Read request for characteristic %s", uuid)
|
|
|
|
# Return the current cached value (synchronously)
|
|
# Async handlers update these values in the background
|
|
if characteristic.value:
|
|
logger.info("Read returning cached: %s", characteristic.value[:50] if len(characteristic.value) > 50 else characteristic.value)
|
|
return characteristic.value
|
|
|
|
# Return default JSON if no cached value
|
|
default_value = bytearray(b'{"status": "initializing"}')
|
|
logger.info("Read returning default: %s", default_value)
|
|
return default_value
|
|
|
|
def _on_write(self, characteristic: BlessGATTCharacteristic, value: Any):
|
|
"""Handle BLE write request.
|
|
|
|
Args:
|
|
characteristic: The characteristic being written
|
|
value: Value being written
|
|
"""
|
|
uuid = str(characteristic.uuid).lower() # Use lowercase to match our UUID format
|
|
logger.info("BLE Write request for characteristic %s: %s", uuid, value)
|
|
|
|
# Update the characteristic value
|
|
characteristic.value = value
|
|
|
|
handler = self.gatt_server.get_characteristic_handler(uuid)
|
|
if handler and handler.write_handler:
|
|
try:
|
|
# Convert value to bytes if needed
|
|
if isinstance(value, bytearray):
|
|
data = bytes(value)
|
|
elif isinstance(value, bytes):
|
|
data = value
|
|
else:
|
|
data = bytes(value)
|
|
|
|
# Run async handler in event loop
|
|
if self._loop and self._loop.is_running():
|
|
asyncio.run_coroutine_threadsafe(
|
|
handler.write_handler(data),
|
|
self._loop
|
|
)
|
|
except Exception as e:
|
|
logger.error("Error handling write for %s: %s", uuid, e)
|
|
|
|
def _on_subscribe(self, characteristic: BlessGATTCharacteristic, **kwargs):
|
|
"""Handle subscription to characteristic notifications."""
|
|
logger.info("Client subscribed to %s", characteristic.uuid)
|
|
|
|
def _on_unsubscribe(self, characteristic: BlessGATTCharacteristic, **kwargs):
|
|
"""Handle unsubscription from characteristic notifications."""
|
|
logger.info("Client unsubscribed from %s", characteristic.uuid)
|
|
|
|
async def start(self, retries: int = 3, retry_delay: float = 2.0) -> bool:
|
|
"""Start the BLE GATT server.
|
|
|
|
Args:
|
|
retries: Number of startup attempts (for boot timing issues)
|
|
retry_delay: Delay between retries in seconds
|
|
|
|
Returns:
|
|
True if started successfully, False otherwise
|
|
"""
|
|
if self._is_running:
|
|
logger.warning("BLE server already running")
|
|
return True
|
|
|
|
last_error = None
|
|
for attempt in range(retries):
|
|
try:
|
|
self._loop = asyncio.get_running_loop()
|
|
|
|
# Create bless server
|
|
self.server = BlessServer(name=self.device_name, loop=self._loop)
|
|
|
|
# Set up callbacks (bless 0.3.0+ API)
|
|
self.server.read_request_func = self._on_read
|
|
self.server.write_request_func = self._on_write
|
|
|
|
# Build and add GATT structure
|
|
gatt = self._build_gatt_dict()
|
|
await self.server.add_gatt(gatt)
|
|
|
|
# Start the server (begins advertising)
|
|
await self.server.start()
|
|
|
|
# Start GATT server handlers
|
|
await self.gatt_server.start()
|
|
|
|
# Register notification callbacks
|
|
self._setup_notification_callbacks()
|
|
|
|
self._is_running = True
|
|
logger.info("BLE GATT server started, advertising as '%s'", self.device_name)
|
|
return True
|
|
|
|
except Exception as e:
|
|
last_error = e
|
|
if attempt < retries - 1:
|
|
logger.warning(
|
|
"BLE server start attempt %d/%d failed: %s, retrying in %.1fs...",
|
|
attempt + 1, retries, e, retry_delay
|
|
)
|
|
# Clean up failed server before retry
|
|
if self.server:
|
|
try:
|
|
await self.server.stop()
|
|
except Exception:
|
|
pass
|
|
self.server = None
|
|
await asyncio.sleep(retry_delay)
|
|
else:
|
|
logger.error("Failed to start BLE server after %d attempts: %s", retries, e)
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
self._is_running = False
|
|
return False
|
|
|
|
def _setup_notification_callbacks(self):
|
|
"""Set up notification callbacks for characteristics that support notify."""
|
|
notify_chars = [
|
|
PM3CharacteristicUUIDs.COMMAND_RESULT,
|
|
WiFiCharacteristicUUIDs.SCAN,
|
|
SystemCharacteristicUUIDs.INFO,
|
|
UpdateCharacteristicUUIDs.CHECK,
|
|
UpdateCharacteristicUUIDs.PROGRESS,
|
|
]
|
|
|
|
for uuid in notify_chars:
|
|
self._register_notification_callback(uuid)
|
|
|
|
def _register_notification_callback(self, uuid: str):
|
|
"""Register notification callback for a characteristic.
|
|
|
|
Args:
|
|
uuid: Characteristic UUID
|
|
"""
|
|
async def send_notification(value: bytes):
|
|
if self.server and self._is_running:
|
|
try:
|
|
char = self.server.get_characteristic(uuid)
|
|
if char:
|
|
char.value = bytearray(value)
|
|
service_uuid = self._get_service_uuid_for_characteristic(uuid)
|
|
# update_value is not async in bless 0.3+
|
|
self.server.update_value(service_uuid, uuid)
|
|
logger.debug("Notification sent for %s", uuid)
|
|
except Exception as e:
|
|
logger.error("Failed to send notification for %s: %s", uuid, e)
|
|
|
|
self.gatt_server.register_notification_callback(uuid, send_notification)
|
|
|
|
def _get_service_uuid_for_characteristic(self, char_uuid: str) -> str:
|
|
"""Get the service UUID that contains a characteristic.
|
|
|
|
Args:
|
|
char_uuid: Characteristic UUID
|
|
|
|
Returns:
|
|
Service UUID
|
|
"""
|
|
# Check each service's characteristics
|
|
if char_uuid in [
|
|
PM3CharacteristicUUIDs.COMMAND_WRITE,
|
|
PM3CharacteristicUUIDs.COMMAND_RESULT,
|
|
PM3CharacteristicUUIDs.STATUS,
|
|
PM3CharacteristicUUIDs.SESSION_CREATE,
|
|
PM3CharacteristicUUIDs.SESSION_RELEASE,
|
|
]:
|
|
return PM3CharacteristicUUIDs.SERVICE
|
|
elif char_uuid in [
|
|
WiFiCharacteristicUUIDs.STATUS,
|
|
WiFiCharacteristicUUIDs.SCAN,
|
|
WiFiCharacteristicUUIDs.CONNECT,
|
|
WiFiCharacteristicUUIDs.MODE,
|
|
]:
|
|
return WiFiCharacteristicUUIDs.SERVICE
|
|
elif char_uuid in [
|
|
SystemCharacteristicUUIDs.INFO,
|
|
SystemCharacteristicUUIDs.SHUTDOWN,
|
|
SystemCharacteristicUUIDs.RESTART,
|
|
]:
|
|
return SystemCharacteristicUUIDs.SERVICE
|
|
elif char_uuid in [
|
|
UpdateCharacteristicUUIDs.CHECK,
|
|
UpdateCharacteristicUUIDs.PROGRESS,
|
|
]:
|
|
return UpdateCharacteristicUUIDs.SERVICE
|
|
else:
|
|
return PM3CharacteristicUUIDs.SERVICE # Default
|
|
|
|
async def stop(self):
|
|
"""Stop the BLE GATT server."""
|
|
if not self._is_running:
|
|
return
|
|
|
|
try:
|
|
await self.gatt_server.stop()
|
|
|
|
if self.server:
|
|
await self.server.stop()
|
|
self.server = None
|
|
|
|
self._is_running = False
|
|
logger.info("BLE server stopped")
|
|
|
|
except Exception as e:
|
|
logger.error("Error stopping BLE server: %s", e)
|
|
|
|
async def send_notification(self, uuid: str, value: bytes) -> bool:
|
|
"""Send a notification for a characteristic.
|
|
|
|
Args:
|
|
uuid: Characteristic UUID
|
|
value: Notification value
|
|
|
|
Returns:
|
|
True if sent successfully
|
|
"""
|
|
if not self.server or not self._is_running:
|
|
return False
|
|
|
|
try:
|
|
char = self.server.get_characteristic(uuid)
|
|
if char:
|
|
char.value = bytearray(value)
|
|
service_uuid = self._get_service_uuid_for_characteristic(uuid)
|
|
# update_value is not async in bless 0.3+
|
|
self.server.update_value(service_uuid, uuid)
|
|
return True
|
|
except Exception as e:
|
|
logger.error("Error sending notification for %s: %s", uuid, e)
|
|
|
|
return False
|
|
|
|
|
|
# Singleton instance for application use
|
|
_adapter_instance: Optional[BlueZGATTAdapter] = None
|
|
|
|
|
|
def get_ble_adapter() -> BlueZGATTAdapter:
|
|
"""Get or create the singleton BLE adapter instance.
|
|
|
|
Returns:
|
|
BlueZGATTAdapter instance
|
|
"""
|
|
global _adapter_instance
|
|
if _adapter_instance is None:
|
|
_adapter_instance = BlueZGATTAdapter()
|
|
return _adapter_instance
|
|
|
|
|
|
async def start_ble_server(device_name: str = DEFAULT_DEVICE_NAME) -> bool:
|
|
"""Start the BLE GATT server.
|
|
|
|
Args:
|
|
device_name: BLE device name for advertising
|
|
|
|
Returns:
|
|
True if started successfully
|
|
"""
|
|
adapter = get_ble_adapter()
|
|
adapter.device_name = device_name
|
|
return await adapter.start()
|
|
|
|
|
|
async def stop_ble_server():
|
|
"""Stop the BLE GATT server."""
|
|
adapter = get_ble_adapter()
|
|
await adapter.stop()
|