callback_server/lib.rs
1//! Internal implementation detail of [`sonos-sdk`](https://crates.io/crates/sonos-sdk). Not intended for direct use.
2//!
3//! Generic UPnP callback server for receiving event notifications.
4//!
5//! This crate provides a lightweight HTTP server for handling UPnP NOTIFY requests.
6//! It is designed to be generic and has no knowledge of device-specific protocols.
7//!
8//! # Overview
9//!
10//! The callback server consists of three main components:
11//!
12//! - [`CallbackServer`]: HTTP server that binds to a local port and receives incoming
13//! UPnP event notifications via HTTP POST requests.
14//! - [`EventRouter`]: Routes incoming events based on subscription IDs to registered
15//! handlers via channels.
16//! - [`NotificationPayload`]: Generic data structure containing subscription ID and
17//! raw XML event body.
18//!
19//! # Architecture
20//!
21//! The callback server is designed to be a thin HTTP layer that:
22//!
23//! 1. Binds to an available port in a specified range
24//! 2. Validates incoming UPnP NOTIFY requests
25//! 3. Extracts subscription IDs and event XML
26//! 4. Routes events to registered handlers via channels
27//!
28//! All device-specific logic (speaker IDs, service types, event parsing) should be
29//! handled by the consuming crate through an adapter layer.
30//!
31//! # Example: Basic Usage
32//!
33//! ```no_run
34//! use callback_server::{CallbackServer, NotificationPayload};
35//! use tokio::sync::mpsc;
36//!
37//! #[tokio::main]
38//! async fn main() -> Result<(), String> {
39//! // Create a channel for receiving notifications
40//! let (tx, mut rx) = mpsc::unbounded_channel::<NotificationPayload>();
41//!
42//! // Create and start the callback server
43//! let server = CallbackServer::new((3400, 3500), tx).await?;
44//!
45//! println!("Callback server listening at: {}", server.base_url());
46//!
47//! // Register a subscription
48//! server.router().register("uuid:subscription-123".to_string()).await;
49//!
50//! // Handle incoming notifications
51//! tokio::spawn(async move {
52//! while let Some(notification) = rx.recv().await {
53//! println!("Received event for subscription: {}", notification.subscription_id);
54//! println!("Event XML: {}", notification.event_xml);
55//! }
56//! });
57//!
58//! // Server runs until shutdown is called
59//! // server.shutdown().await?;
60//!
61//! Ok(())
62//! }
63//! ```
64//!
65//! # Example: With Adapter Layer
66//!
67//! Device-specific crates should create an adapter layer that wraps the generic
68//! types and adds domain-specific context:
69//!
70//! ```no_run
71//! use callback_server::{CallbackServer, NotificationPayload};
72//! use tokio::sync::mpsc;
73//! use std::collections::HashMap;
74//! use std::sync::Arc;
75//! use tokio::sync::RwLock;
76//!
77//! // Device-specific event with additional context
78//! #[derive(Debug, Clone)]
79//! struct DeviceEvent {
80//! subscription_id: String,
81//! device_id: String,
82//! service_type: String,
83//! event_xml: String,
84//! }
85//!
86//! #[tokio::main]
87//! async fn main() -> Result<(), String> {
88//! // Create channels
89//! let (notification_tx, mut notification_rx) = mpsc::unbounded_channel::<NotificationPayload>();
90//! let (device_event_tx, mut device_event_rx) = mpsc::unbounded_channel::<DeviceEvent>();
91//!
92//! // Create callback server
93//! let server = CallbackServer::new((3400, 3500), notification_tx).await?;
94//!
95//! // Maintain mapping from subscription ID to device context
96//! let subscription_map: Arc<RwLock<HashMap<String, (String, String)>>> =
97//! Arc::new(RwLock::new(HashMap::new()));
98//!
99//! // Spawn adapter task to add device-specific context
100//! let map_clone = subscription_map.clone();
101//! tokio::spawn(async move {
102//! while let Some(notification) = notification_rx.recv().await {
103//! let map = map_clone.read().await;
104//! if let Some((device_id, service_type)) = map.get(¬ification.subscription_id) {
105//! let device_event = DeviceEvent {
106//! subscription_id: notification.subscription_id,
107//! device_id: device_id.clone(),
108//! service_type: service_type.clone(),
109//! event_xml: notification.event_xml,
110//! };
111//! let _ = device_event_tx.send(device_event);
112//! }
113//! }
114//! });
115//!
116//! // Register subscription with device context
117//! let sub_id = "uuid:subscription-123".to_string();
118//! server.router().register(sub_id.clone()).await;
119//! subscription_map.write().await.insert(
120//! sub_id,
121//! ("device-001".to_string(), "AVTransport".to_string())
122//! );
123//!
124//! // Process device-specific events
125//! tokio::spawn(async move {
126//! while let Some(event) = device_event_rx.recv().await {
127//! println!("Device {} service {} event", event.device_id, event.service_type);
128//! }
129//! });
130//!
131//! Ok(())
132//! }
133//! ```
134//!
135//! # Private Workspace Crate
136//!
137//! This crate is intended for internal use within the workspace and is not published
138//! to crates.io. It provides the foundation for device-specific event handling layers.
139
140pub mod firewall_detection;
141pub mod router;
142mod server;
143
144pub use firewall_detection::{
145 CoordinatorStats, DetectionReason, DetectionResult, DeviceFirewallState,
146 FirewallDetectionConfig, FirewallDetectionCoordinator, FirewallStatus,
147};
148pub use router::{EventRouter, NotificationPayload};
149pub use server::CallbackServer;