Skip to main content

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(&notification.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;