2025-10-05 14:35:44 +02:00
//! Implémentation de ServiceInstance.
2025-10-09 20:26:51 +02:00
//!
//! Ce module contient l'implémentation complète de [`ServiceInstance`],
//! qui représente une instance active d'un service UPnP.
//!
//! # Composants principaux
//!
//! - [`ServiceInstance`] : Structure principale contenant l'état du service
//! - [`event_sub_handler`] : Handler Axum pour les abonnements aux événements
//! - [`control_handler`] : Handler Axum pour les appels SOAP
//!
//! # Gestion des événements
//!
//! L'instance gère automatiquement :
//! - Les souscriptions aux événements (SUBSCRIBE/UNSUBSCRIBE)
//! - L'envoi d'événements initiaux aux nouveaux abonnés
//! - Les notifications périodiques des changements d'état
//! - Le séquençage des messages par abonné
//!
//! # Architecture
//!
//! ```text
//! ServiceInstance
//! ├── Variables d'état (StateVarInstanceSet)
//! ├── Actions (ActionInstanceSet)
//! ├── Abonnés (HashMap<SID, Callback>)
//! ├── Buffer de changements (Mutex<HashMap>)
//! └── Séquences (Mutex<HashMap<SID, u32>>)
//! ```
2025-10-05 14:35:44 +02:00
use axum ::{
2025-10-09 07:00:48 +02:00
body ::Body ,
2025-10-05 14:35:44 +02:00
extract ::{ Request , State } ,
http ::{ HeaderMap , StatusCode } ,
response ::{ IntoResponse , Response } ,
2025-10-09 07:00:48 +02:00
} ;
2025-10-11 10:08:28 +02:00
use bevy_reflect ::Reflect ;
2025-10-09 07:00:48 +02:00
use std ::{
collections ::HashMap ,
future ::Future ,
pin ::Pin ,
sync ::{ Arc , Mutex , RwLock } ,
time ::Duration ,
2025-10-05 14:35:44 +02:00
} ;
use tokio ::time ;
2025-10-09 20:37:05 +02:00
use tracing ::{ debug , error , info , warn } ;
2025-10-09 07:00:48 +02:00
use xmltree ::{ Element , EmitterConfig , XMLNode } ;
2025-10-11 10:08:28 +02:00
use quick_xml ::escape ::escape ;
2025-10-05 14:35:44 +02:00
use crate ::{
2025-10-09 07:00:48 +02:00
UpnpInstance , UpnpObject , UpnpObjectType , UpnpTyped , UpnpTypedInstance ,
2025-10-05 14:35:44 +02:00
actions ::{ ActionInstance , ActionInstanceSet } ,
2025-10-05 21:58:36 +02:00
devices ::DeviceInstance ,
2025-10-09 07:00:48 +02:00
services ::{ Service , ServiceError } ,
state_variables ::{ StateVarInstance , StateVarInstanceSet , UpnpVariable } ,
2025-10-05 14:35:44 +02:00
} ;
/// Méthodes HTTP pour les événements UPnP.
pub const METHOD_SUBSCRIBE : & str = " SUBSCRIBE " ;
pub const METHOD_UNSUBSCRIBE : & str = " UNSUBSCRIBE " ;
/// Instance de service UPnP.
///
/// Représente une instance concrète d'un service UPnP, attachée à un device.
/// Gère l'exécution des actions, les notifications d'événements et les abonnements.
///
/// # Fonctionnalités
///
/// - Exécution d'actions via SOAP
/// - Gestion des abonnements aux événements (SUBSCRIBE/UNSUBSCRIBE)
/// - Notifications automatiques des changements d'état
/// - Génération de la description SCPD
///
/// # Cycle de vie
///
/// 1. Création via [`Service::create_instance`](crate::UpnpModel::create_instance)
/// 2. Enregistrement des URLs avec [`register_urls`](Self::register_urls)
/// 3. Démarrage du notifier avec [`start_notifier`](Self::start_notifier)
///
/// # Examples
///
2025-10-09 22:36:33 +02:00
/// ```rust,ignore
2025-10-05 14:35:44 +02:00
/// # use pmoupnp::services::Service;
2025-10-09 22:36:33 +02:00
/// # use pmoupnp::UpnpModel;
/// # use pmoserver::Server;
2025-10-05 14:35:44 +02:00
/// # use std::time::Duration;
/// # #[tokio::main]
/// # async fn main() {
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
///
/// // Enregistrer les endpoints
/// let mut server = Server::new("test", "http://localhost:8080", 8080);
/// instance.register_urls(&mut server).await.unwrap();
///
/// // Démarrer les notifications
/// let _handle = instance.start_notifier(Duration::from_secs(5));
/// # }
/// ```
#[ derive(Clone) ]
pub struct ServiceInstance {
/// Métadonnées de l'objet
object : UpnpObjectType ,
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
/// Référence vers le modèle
model : Arc < Service > ,
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
/// Identifiant du service
identifier : String ,
2025-10-09 07:00:48 +02:00
2025-10-05 21:58:36 +02:00
/// Device parent (optionnel) - utilisé via interior mutability
device : Arc < RwLock < Option < Arc < DeviceInstance > > > > ,
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
/// Variables d'état instanciées
statevariables : StateVarInstanceSet ,
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
/// Actions instanciées
actions : ActionInstanceSet ,
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
/// Abonnés aux événements (SID -> Callback URL)
subscribers : Arc < RwLock < HashMap < String , String > > > ,
2025-10-09 07:00:48 +02:00
2025-10-11 10:08:28 +02:00
/// Buffer des changements en attente de notification (nom de variable -> valeur réflexive)
changed_buffer : Arc < Mutex < HashMap < String , Arc < dyn Reflect > > > > ,
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
/// Compteurs de séquence par abonné
seqid : Arc < Mutex < HashMap < String , u32 > > > ,
}
impl std ::fmt ::Debug for ServiceInstance {
fn fmt ( & self , f : & mut std ::fmt ::Formatter < '_ > ) -> std ::fmt ::Result {
f . debug_struct ( " ServiceInstance " )
. field ( " object " , & self . object )
. field ( " identifier " , & self . identifier )
. field ( " device " , & self . device )
. field ( " statevariables " , & self . statevariables )
. field ( " actions " , & self . actions )
. finish ( )
}
}
impl UpnpTyped for ServiceInstance {
fn as_upnp_object_type ( & self ) -> & UpnpObjectType {
& self . object
}
}
impl UpnpInstance for ServiceInstance {
type Model = Service ;
fn new ( model : & Service ) -> Self {
// Phase 1 : Créer les instances de variables d'état
let mut statevariables = StateVarInstanceSet ::new ( ) ;
for v in model . variables ( ) {
if let Err ( e ) = statevariables . insert ( Arc ::new ( StateVarInstance ::new ( & * v ) ) ) {
error! ( " Failed to insert state variable: {:?} " , e ) ;
}
}
// Phase 2 : Créer les instances d'actions avec validation
let mut actions = ActionInstanceSet ::new ( ) ;
for a in model . actions ( ) {
// Vérifier que toutes les variables référencées existent
let mut missing_vars = Vec ::new ( ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
for arg in a . arguments ( ) . all ( ) {
let related_var_name = arg . state_variable ( ) . get_name ( ) ;
if statevariables . get_by_name ( related_var_name ) . is_none ( ) {
missing_vars . push ( related_var_name . to_string ( ) ) ;
}
}
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
if ! missing_vars . is_empty ( ) {
error! (
" Action '{}' references missing state variables: {:?} " ,
a . get_name ( ) ,
missing_vars
) ;
continue ;
}
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
// Créer l'instance d'action
let action_instance = Arc ::new ( ActionInstance ::new ( & * a ) ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
// ✅ Phase 3 : ACTIVER le binding des arguments aux variables d'instance
for arg_instance in action_instance . arguments_set ( ) . all ( ) {
let var_name = arg_instance . get_model ( ) . state_variable ( ) . get_name ( ) ;
if let Some ( var_instance ) = statevariables . get_by_name ( var_name ) {
// ✅ Activer cette ligne (déjà présente dans ArgumentInstance)
arg_instance . bind_variable ( var_instance ) ;
}
}
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
if let Err ( e ) = actions . insert ( action_instance ) {
error! ( " Failed to insert action '{}': {:?} " , a . get_name ( ) , e ) ;
}
}
Self {
object : UpnpObjectType {
name : model . name ( ) . to_string ( ) ,
object_type : " ServiceInstance " . to_string ( ) ,
} ,
model : Arc ::new ( model . clone ( ) ) ,
identifier : model . identifier ( ) . to_string ( ) ,
2025-10-05 21:58:36 +02:00
device : Arc ::new ( RwLock ::new ( None ) ) ,
2025-10-05 14:35:44 +02:00
statevariables ,
actions ,
subscribers : Arc ::new ( RwLock ::new ( HashMap ::new ( ) ) ) ,
changed_buffer : Arc ::new ( Mutex ::new ( HashMap ::new ( ) ) ) ,
seqid : Arc ::new ( Mutex ::new ( HashMap ::new ( ) ) ) ,
}
}
}
impl UpnpTypedInstance for ServiceInstance {
fn get_model ( & self ) -> & Self ::Model {
& self . model
}
}
impl UpnpObject for ServiceInstance {
fn to_xml_element ( & self ) -> Element {
let mut elem = Element ::new ( " service " ) ;
let mut service_type = Element ::new ( " serviceType " ) ;
2025-10-09 07:00:48 +02:00
service_type
. children
. push ( XMLNode ::Text ( self . service_type ( ) ) ) ;
2025-10-05 14:35:44 +02:00
elem . children . push ( XMLNode ::Element ( service_type ) ) ;
let mut service_id = Element ::new ( " serviceId " ) ;
service_id . children . push ( XMLNode ::Text ( self . service_id ( ) ) ) ;
elem . children . push ( XMLNode ::Element ( service_id ) ) ;
let mut scpd_url = Element ::new ( " SCPDURL " ) ;
2025-10-05 21:58:36 +02:00
scpd_url . children . push ( XMLNode ::Text ( self . scpd_route ( ) ) ) ;
2025-10-05 14:35:44 +02:00
elem . children . push ( XMLNode ::Element ( scpd_url ) ) ;
let mut control_url = Element ::new ( " controlURL " ) ;
2025-10-09 07:00:48 +02:00
control_url
. children
. push ( XMLNode ::Text ( self . control_route ( ) ) ) ;
2025-10-05 14:35:44 +02:00
elem . children . push ( XMLNode ::Element ( control_url ) ) ;
let mut event_sub_url = Element ::new ( " eventSubURL " ) ;
2025-10-09 07:00:48 +02:00
event_sub_url
. children
. push ( XMLNode ::Text ( self . event_route ( ) ) ) ;
2025-10-05 14:35:44 +02:00
elem . children . push ( XMLNode ::Element ( event_sub_url ) ) ;
elem
}
}
impl ServiceInstance {
2025-10-10 23:49:59 +02:00
/// Enregistre cette instance de service auprès de toutes ses variables.
///
/// Cette méthode doit être appelée APRÈS la création de l'Arc<ServiceInstance>
/// pour permettre aux variables de notifier le service lors de leurs changements.
///
/// # Arguments
///
/// * `self_arc` - Arc pointant vers cette instance
///
/// # Examples
///
/// ```rust,ignore
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # use std::sync::Arc;
/// let service = Service::new("AVTransport".to_string());
/// let instance = Arc::new(service.create_instance());
/// instance.register_with_variables(&instance);
/// ```
pub fn register_with_variables ( self : & Arc < Self > ) {
let weak_self = Arc ::downgrade ( self ) ;
for var in self . statevariables . all ( ) {
var . register_service ( weak_self . clone ( ) ) ;
}
}
2025-10-05 14:35:44 +02:00
/// Retourne l'identifiant du service.
2025-10-09 20:26:51 +02:00
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.identifier(), "AVTransport");
/// ```
2025-10-05 14:35:44 +02:00
pub fn identifier ( & self ) -> & str {
& self . identifier
}
/// Retourne le type de service UPnP.
///
/// Format: `urn:schemas-upnp-org:service:{name}:{version}`
2025-10-09 20:26:51 +02:00
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.service_type(), "urn:schemas-upnp-org:service:AVTransport:1");
/// ```
2025-10-05 14:35:44 +02:00
pub fn service_type ( & self ) -> String {
self . model . service_type ( )
}
/// Retourne l'ID de service UPnP.
///
/// Format: `urn:upnp-org:serviceId:{identifier}`
2025-10-09 20:26:51 +02:00
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.service_id(), "urn:upnp-org:serviceId:AVTransport");
/// ```
2025-10-05 14:35:44 +02:00
pub fn service_id ( & self ) -> String {
format! ( " urn:upnp-org:serviceId: {} " , self . identifier )
}
2025-10-09 20:26:51 +02:00
/// Récupère une variable d'état par son nom.
///
/// # Arguments
///
/// * `name` - Nom de la variable d'état
///
/// # Returns
///
/// `Some(Arc<StateVarInstance>)` si la variable existe, `None` sinon.
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// if let Some(var) = instance.get_variable("TransportState") {
/// println!("Value: {}", var.value());
/// }
/// ```
2025-10-05 14:35:44 +02:00
pub fn get_variable ( & self , name : & str ) -> Option < Arc < StateVarInstance > > {
self . statevariables . get_by_name ( name )
}
2025-10-09 07:00:48 +02:00
2025-10-09 20:26:51 +02:00
/// Récupère une action par son nom.
///
/// # Arguments
///
/// * `name` - Nom de l'action
///
/// # Returns
///
/// `Some(Arc<ActionInstance>)` si l'action existe, `None` sinon.
///
/// # Examples
///
2025-10-09 22:36:33 +02:00
/// ```rust
2025-10-09 20:26:51 +02:00
/// # use pmoupnp::services::Service;
2025-10-09 22:36:33 +02:00
/// # use pmoupnp::{UpnpModel, UpnpTyped};
2025-10-09 20:26:51 +02:00
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// if let Some(action) = instance.get_action("Play") {
/// println!("Action found: {}", action.get_name());
/// }
/// ```
2025-10-05 14:35:44 +02:00
pub fn get_action ( & self , name : & str ) -> Option < Arc < ActionInstance > > {
self . actions . get_by_name ( name )
}
2025-10-05 21:58:36 +02:00
/// Définit le device parent pour ce service.
///
/// Cette méthode doit être appelée après la création du service instance
/// pour établir la relation avec le device parent.
2025-10-09 20:26:51 +02:00
///
/// # Arguments
///
/// * `device` - Le device parent
///
/// # Examples
///
2025-10-09 22:36:33 +02:00
/// ```rust
2025-10-09 20:26:51 +02:00
/// # use pmoupnp::services::Service;
/// # use pmoupnp::devices::Device;
/// # use pmoupnp::UpnpModel;
/// # use std::sync::Arc;
/// # let service = Service::new("AVTransport".to_string());
2025-10-09 22:36:33 +02:00
/// # let device = Device::new("MediaRenderer".to_string(), "urn:schemas-upnp-org:device:MediaRenderer:1".to_string(), "My MediaRenderer".to_string());
2025-10-09 20:26:51 +02:00
/// let service_instance = service.create_instance();
2025-10-09 22:36:33 +02:00
/// let device_instance = device.create_instance();
2025-10-09 20:26:51 +02:00
/// service_instance.set_device(device_instance);
/// ```
2025-10-05 21:58:36 +02:00
pub fn set_device ( & self , device : Arc < DeviceInstance > ) {
let mut dev = self . device . write ( ) . unwrap ( ) ;
* dev = Some ( device ) ;
}
/// Retourne la route du service (chemin relatif).
2025-10-09 20:26:51 +02:00
///
/// # Returns
///
/// Chemin relatif incluant le device parent si présent.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.route(), "/service/AVTransport");
/// ```
2025-10-05 21:58:36 +02:00
pub fn route ( & self ) -> String {
let device = self . device . read ( ) . unwrap ( ) ;
match device . as_ref ( ) {
Some ( device ) = > format! ( " {} /service/ {} " , device . route ( ) , self . get_name ( ) ) ,
2025-10-05 14:35:44 +02:00
None = > format! ( " /service/ {} " , self . get_name ( ) ) ,
}
}
2025-10-05 21:58:36 +02:00
/// Retourne la route de contrôle SOAP.
2025-10-09 20:26:51 +02:00
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.control_route(), "/service/AVTransport/control");
/// ```
2025-10-05 21:58:36 +02:00
pub fn control_route ( & self ) -> String {
format! ( " {} /control " , self . route ( ) )
2025-10-05 14:35:44 +02:00
}
2025-10-05 21:58:36 +02:00
/// Retourne la route de souscription aux événements.
2025-10-09 20:26:51 +02:00
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.event_route(), "/service/AVTransport/event");
/// ```
2025-10-05 21:58:36 +02:00
pub fn event_route ( & self ) -> String {
format! ( " {} /event " , self . route ( ) )
2025-10-05 14:35:44 +02:00
}
2025-10-05 21:58:36 +02:00
/// Retourne la route de la description SCPD.
2025-10-09 20:26:51 +02:00
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.scpd_route(), "/service/AVTransport/desc.xml");
/// ```
2025-10-05 21:58:36 +02:00
pub fn scpd_route ( & self ) -> String {
format! ( " {} /desc.xml " , self . route ( ) )
2025-10-05 14:35:44 +02:00
}
/// Retourne l'USN (Unique Service Name).
2025-10-09 20:26:51 +02:00
///
/// L'USN combine l'UUID du device parent et le type de service UPnP.
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// let usn = instance.usn();
/// // Format: uuid:{device-uuid}::urn:schemas-upnp-org:service:AVTransport:1
/// ```
2025-10-05 14:35:44 +02:00
pub fn usn ( & self ) -> String {
2025-10-05 21:58:36 +02:00
let device = self . device . read ( ) . unwrap ( ) ;
match device . as_ref ( ) {
2025-10-05 14:35:44 +02:00
Some ( device ) = > format! ( " uuid: {} ::urn: {} " , device . udn ( ) , self . service_type ( ) ) ,
None = > format! ( " uuid::urn: {} " , self . service_type ( ) ) ,
}
}
2025-10-09 20:26:51 +02:00
/// Retourne une référence vers l'ensemble des variables d'état.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// let vars = instance.statevariables();
/// println!("Variables count: {}", vars.all().len());
/// ```
2025-10-05 14:35:44 +02:00
pub fn statevariables ( & self ) -> & StateVarInstanceSet {
& self . statevariables
}
2025-10-09 20:26:51 +02:00
/// Retourne une référence vers l'ensemble des actions.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// let actions = instance.actions();
/// println!("Actions count: {}", actions.all().len());
/// ```
2025-10-05 14:35:44 +02:00
pub fn actions ( & self ) -> & ActionInstanceSet {
& self . actions
}
2025-10-09 20:37:05 +02:00
/// Retourne une action par son nom.
///
/// # Arguments
///
/// * `name` - Nom de l'action
///
/// # Returns
///
/// `Some(Arc<ActionInstance>)` si trouvée, `None` sinon.
pub fn action ( & self , name : & str ) -> Option < Arc < crate ::actions ::ActionInstance > > {
self . actions . get_by_name ( name )
}
2025-10-06 06:01:41 +02:00
/// Enregistre les routes UPnP dans le serveur.
2025-10-05 14:35:44 +02:00
///
/// # Errors
///
/// Retourne une erreur si l'enregistrement des routes échoue.
2025-10-06 20:24:04 +02:00
pub async fn register_urls ( & self , server : & mut pmoserver ::Server ) -> Result < ( ) , ServiceError > {
2025-10-05 21:58:36 +02:00
let device = self . device . read ( ) . unwrap ( ) ;
2025-10-09 07:00:48 +02:00
let device_name = device
. as_ref ( )
. map ( | d | d . get_name ( ) . clone ( ) )
. unwrap_or_else ( | | " unknown " . to_string ( ) ) ;
let server_url = device
. as_ref ( )
. map ( | d | d . base_url ( ) . to_string ( ) )
. unwrap_or_default ( ) ;
2025-10-05 21:58:36 +02:00
drop ( device ) ;
2025-10-05 14:35:44 +02:00
info! (
" ✅ Service description for {}:{} available at : {}{} " ,
2025-10-05 21:58:36 +02:00
device_name ,
2025-10-05 14:35:44 +02:00
self . get_name ( ) ,
2025-10-05 21:58:36 +02:00
server_url ,
self . scpd_route ( ) ,
2025-10-05 14:35:44 +02:00
) ;
// Handler SCPD
let instance_scpd = self . clone ( ) ;
2025-10-09 07:00:48 +02:00
server
. add_handler ( & self . scpd_route ( ) , move | | {
let instance = instance_scpd . clone ( ) ;
async move { instance . scpd_handler ( ) . await }
} )
. await ;
2025-10-05 14:35:44 +02:00
// Handler control
2025-10-09 20:37:05 +02:00
let instance_control = Arc ::new ( self . clone ( ) ) ;
2025-10-09 07:00:48 +02:00
server
. add_post_handler_with_state ( & self . control_route ( ) , control_handler , instance_control )
. await ;
2025-10-05 14:35:44 +02:00
// Handler événements
let instance_event = self . clone ( ) ;
2025-10-09 07:00:48 +02:00
server
. add_handler_with_state ( & self . event_route ( ) , event_sub_handler , instance_event )
. await ;
2025-10-05 14:35:44 +02:00
Ok ( ( ) )
}
2025-10-09 20:26:51 +02:00
/// Génère l'élément XML SCPD (Service Control Protocol Description).
///
/// Cette méthode crée un élément XML conforme à la spécification UPnP décrivant
/// le service, ses actions et ses variables d'état.
///
/// # Returns
///
/// Un élément `xmltree::Element` représentant le document SCPD.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// let scpd = instance.scpd_element();
/// assert_eq!(scpd.name, "scpd");
/// ```
2025-10-05 14:35:44 +02:00
pub fn scpd_element ( & self ) -> Element {
let mut elem = Element ::new ( " scpd " ) ;
elem . attributes . insert (
" xmlns " . to_string ( ) ,
" urn:schemas-upnp-org:service-1-0 " . to_string ( ) ,
) ;
// specVersion
let mut spec = Element ::new ( " specVersion " ) ;
let mut major = Element ::new ( " major " ) ;
major . children . push ( XMLNode ::Text ( " 1 " . to_string ( ) ) ) ;
spec . children . push ( XMLNode ::Element ( major ) ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
let mut minor = Element ::new ( " minor " ) ;
minor . children . push ( XMLNode ::Text ( " 0 " . to_string ( ) ) ) ;
spec . children . push ( XMLNode ::Element ( minor ) ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
elem . children . push ( XMLNode ::Element ( spec ) ) ;
// actionList
if ! self . actions . all ( ) . is_empty ( ) {
2025-10-09 07:00:48 +02:00
elem . children
. push ( XMLNode ::Element ( self . actions . to_xml_element ( ) ) ) ;
2025-10-05 14:35:44 +02:00
}
// serviceStateTable
if ! self . statevariables . all ( ) . is_empty ( ) {
2025-10-09 07:00:48 +02:00
elem . children
. push ( XMLNode ::Element ( self . statevariables . to_xml_element ( ) ) ) ;
2025-10-05 14:35:44 +02:00
}
elem
}
2025-10-09 20:26:51 +02:00
/// Handler HTTP pour la description SCPD.
///
/// Génère et retourne le document XML SCPD décrivant le service.
/// Cette méthode est appelée lorsqu'un client accède à l'URL SCPD du service.
///
/// # Returns
///
/// Une réponse HTTP 200 avec le XML SCPD, ou 500 en cas d'erreur de sérialisation.
///
/// # Format de réponse
///
/// - Content-Type: `text/xml; charset="utf-8"`
/// - Body: Document SCPD formaté avec indentation
2025-10-05 14:35:44 +02:00
async fn scpd_handler ( & self ) -> Response {
2025-10-09 20:37:05 +02:00
info! ( " 📋 SCPD requested for service {} " , self . get_name ( ) ) ;
2025-10-05 14:35:44 +02:00
let elem = self . scpd_element ( ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
let config = EmitterConfig ::new ( )
. perform_indent ( true )
. indent_string ( " " ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
let mut xml_output = Vec ::new ( ) ;
if let Err ( e ) = elem . write_with_config ( & mut xml_output , config ) {
2025-10-09 20:37:05 +02:00
error! ( " ❌ Failed to serialize SCPD XML: {} " , e ) ;
2025-10-05 14:35:44 +02:00
return StatusCode ::INTERNAL_SERVER_ERROR . into_response ( ) ;
}
2025-10-06 11:54:32 +02:00
let xml = String ::from_utf8_lossy ( & xml_output ) . to_string ( ) ;
2025-10-09 20:37:05 +02:00
debug! ( " ✅ SCPD generated for {} ({} bytes) " , self . get_name ( ) , xml . len ( ) ) ;
2025-10-05 14:35:44 +02:00
(
StatusCode ::OK ,
2025-10-09 07:00:48 +02:00
[ (
axum ::http ::header ::CONTENT_TYPE ,
" text/xml; charset= \" utf-8 \" " ,
) ] ,
2025-10-05 14:35:44 +02:00
xml ,
2025-10-09 07:00:48 +02:00
)
. into_response ( )
2025-10-05 14:35:44 +02:00
}
/// Ajoute un abonné aux événements.
2025-10-09 20:26:51 +02:00
///
/// # Arguments
///
/// * `sid` - Identifiant de la souscription (SID)
/// * `callback` - URL de callback pour les notifications
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # #[tokio::main]
/// # async fn main() {
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// instance.add_subscriber(
/// "uuid:12345".to_string(),
/// "<http://192.168.1.100:8080/callback>".to_string()
/// ).await;
/// # }
/// ```
2025-10-05 14:35:44 +02:00
pub async fn add_subscriber ( & self , sid : String , callback : String ) {
let mut subscribers = self . subscribers . write ( ) . unwrap ( ) ;
subscribers . insert ( sid , callback ) ;
}
2025-10-09 20:26:51 +02:00
/// Renouvelle un abonnement existant.
///
/// # Arguments
///
/// * `sid` - Identifiant de la souscription (SID)
/// * `timeout` - Nouvelle durée de validité (format "Second-{n}")
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # #[tokio::main]
/// # async fn main() {
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// instance.renew_subscriber("uuid:12345", "Second-1800").await;
/// # }
/// ```
2025-10-05 14:35:44 +02:00
pub async fn renew_subscriber ( & self , sid : & str , timeout : & str ) {
info! ( " ♻️ Renewed SID {} for timeout {} " , sid , timeout ) ;
}
/// Supprime un abonné.
2025-10-09 20:26:51 +02:00
///
/// # Arguments
///
/// * `sid` - Identifiant de la souscription (SID) à supprimer
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # #[tokio::main]
/// # async fn main() {
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// instance.remove_subscriber("uuid:12345").await;
/// # }
/// ```
2025-10-05 14:35:44 +02:00
pub async fn remove_subscriber ( & self , sid : & str ) {
let mut subscribers = self . subscribers . write ( ) . unwrap ( ) ;
subscribers . remove ( sid ) ;
}
/// Envoie l'événement initial à un nouvel abonné.
2025-10-09 20:26:51 +02:00
///
/// Lorsqu'un client s'abonne aux événements, cette méthode lui envoie
/// immédiatement les valeurs actuelles de toutes les variables d'état
/// qui envoient des notifications.
///
/// # Arguments
///
/// * `sid` - Identifiant de la souscription (SID)
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # #[tokio::main]
/// # async fn main() {
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// instance.send_initial_event("uuid:12345".to_string()).await;
/// # }
/// ```
2025-10-05 14:35:44 +02:00
pub async fn send_initial_event ( & self , sid : String ) {
let callback = {
let subscribers = self . subscribers . read ( ) . unwrap ( ) ;
subscribers . get ( & sid ) . cloned ( )
} ;
if let Some ( callback ) = callback {
let mut changed = HashMap ::new ( ) ;
for sv in self . statevariables . all ( ) {
if sv . is_sending_notification ( ) {
changed . insert ( sv . get_name ( ) . to_string ( ) , sv . value ( ) . to_string ( ) ) ;
}
}
if changed . is_empty ( ) {
return ;
}
tokio ::spawn ( async move {
let callback = callback . trim ( ) . trim_matches ( | c | c = = '<' | | c = = '>' ) ;
2025-10-09 07:00:48 +02:00
let mut body =
r # "<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"# . to_string ( ) ;
2025-10-05 14:35:44 +02:00
for ( name , val ) in changed {
2025-10-09 07:00:48 +02:00
body . push_str ( & format! (
" <e:property><{0}>{1}</{0}></e:property> " ,
name , val
) ) ;
2025-10-05 14:35:44 +02:00
}
body . push_str ( " </e:propertyset> " ) ;
let client = reqwest ::Client ::new ( ) ;
match client
. request ( reqwest ::Method ::from_bytes ( b " NOTIFY " ) . unwrap ( ) , callback )
. header ( " Content-Type " , r # "text/xml; charset="utf-8"# )
. header ( " NT " , " upnp:event " )
. header ( " NTS " , " upnp:propchange " )
. header ( " SID " , & sid )
. header ( " SEQ " , " 0 " )
. body ( body )
. send ( )
. await
{
Ok ( resp ) = > {
2025-10-09 07:00:48 +02:00
info! (
" ✅ Initial event sent to {}, status={} " ,
callback ,
resp . status ( )
) ;
2025-10-05 14:35:44 +02:00
}
Err ( e ) = > {
error! ( " Failed to send initial event to {}: {} " , callback , e ) ;
}
}
} ) ;
}
}
2025-10-09 20:26:51 +02:00
/// Marque un changement de variable à notifier ultérieurement.
///
/// Les changements sont mis en buffer et seront envoyés lors du prochain
/// appel à [`notify_subscribers`](Self::notify_subscribers).
///
/// # Arguments
///
/// * `name` - Nom de la variable d'état modifiée
/// * `value` - Nouvelle valeur de la variable
///
/// # Examples
///
2025-10-11 10:08:28 +02:00
/// ```rust,ignore
2025-10-09 20:26:51 +02:00
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
2025-10-11 10:08:28 +02:00
/// let value = Arc::new("PLAYING".to_string()) as Arc<dyn Reflect>;
/// instance.event_to_be_sent("TransportState".to_string(), value);
2025-10-09 20:26:51 +02:00
/// ```
2025-10-11 10:08:28 +02:00
pub fn event_to_be_sent ( & self , name : String , value : Arc < dyn Reflect > ) {
2025-10-05 14:35:44 +02:00
let mut buffer = self . changed_buffer . lock ( ) . unwrap ( ) ;
buffer . insert ( name , value ) ;
}
/// Récupère le prochain numéro de séquence pour un abonné.
2025-10-09 20:26:51 +02:00
///
/// Chaque notification envoyée à un abonné doit avoir un numéro de séquence
/// unique et croissant.
///
/// # Arguments
///
/// * `sid` - Identifiant de la souscription (SID)
///
/// # Returns
///
/// Le prochain numéro de séquence sous forme de chaîne.
2025-10-05 14:35:44 +02:00
fn next_seq ( & self , sid : & str ) -> String {
let mut seqid = self . seqid . lock ( ) . unwrap ( ) ;
let counter = seqid . entry ( sid . to_string ( ) ) . or_insert ( 0 ) ;
* counter + = 1 ;
counter . to_string ( )
}
2025-10-09 20:26:51 +02:00
/// Notifie tous les abonnés des changements en attente.
///
/// Cette méthode envoie les changements bufferisés à tous les abonnés actuels
/// via des requêtes HTTP NOTIFY. Les changements sont envoyés de manière
/// asynchrone dans des tâches séparées.
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # #[tokio::main]
/// # async fn main() {
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
2025-10-11 10:08:28 +02:00
/// # use std::sync::Arc;
/// # use bevy_reflect::Reflect;
/// let value = Arc::new("PLAYING".to_string()) as Arc<dyn Reflect>;
/// instance.event_to_be_sent("TransportState".to_string(), value);
2025-10-09 20:26:51 +02:00
/// instance.notify_subscribers().await;
/// # }
/// ```
2025-10-05 14:35:44 +02:00
pub async fn notify_subscribers ( & self ) {
let subscribers_copy = {
let subscribers = self . subscribers . read ( ) . unwrap ( ) ;
if subscribers . is_empty ( ) {
return ;
}
subscribers . clone ( )
} ;
let changed = {
let mut buffer = self . changed_buffer . lock ( ) . unwrap ( ) ;
if buffer . is_empty ( ) {
return ;
}
std ::mem ::take ( & mut * buffer )
} ;
for ( sid , callback ) in subscribers_copy {
let changed_clone = changed . clone ( ) ;
let seq = self . next_seq ( & sid ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
tokio ::spawn ( async move {
let callback = callback . trim ( ) . trim_matches ( | c | c = = '<' | | c = = '>' ) ;
2025-10-09 07:00:48 +02:00
let mut body =
r # "<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"# . to_string ( ) ;
2025-10-05 14:35:44 +02:00
for ( name , val ) in changed_clone {
2025-10-11 10:08:28 +02:00
// Convertir la valeur Reflect en String
let val_str = Self ::reflect_to_string ( & * val ) ;
2025-10-09 07:00:48 +02:00
body . push_str ( & format! (
" <e:property><{0}>{1}</{0}></e:property> " ,
2025-10-11 10:08:28 +02:00
name , val_str
2025-10-09 07:00:48 +02:00
) ) ;
2025-10-05 14:35:44 +02:00
}
body . push_str ( " </e:propertyset> " ) ;
let client = reqwest ::Client ::new ( ) ;
match client
. request ( reqwest ::Method ::from_bytes ( b " NOTIFY " ) . unwrap ( ) , callback )
. header ( " Content-Type " , r # "text/xml; charset="utf-8"# )
. header ( " NT " , " upnp:event " )
. header ( " NTS " , " upnp:propchange " )
. header ( " SID " , & sid )
. header ( " SEQ " , seq )
. body ( body )
. send ( )
. await
{
Ok ( _ ) = > {
info! ( " ✅ Notified subscriber {} of changes " , callback ) ;
}
Err ( e ) = > {
error! ( " Failed to notify subscriber {}: {} " , callback , e ) ;
}
}
} ) ;
}
}
2025-10-11 10:08:28 +02:00
/// Convertit une valeur Reflect en String pour la notification UPnP.
///
/// Cette fonction gère plusieurs cas :
/// - Types primitifs : formatage direct
/// - Structures serde (pmodidl, etc.) : sérialisation XML
/// - Autres types : fallback sur Debug
///
/// Le résultat est déjà échappé XML-safe selon les normes UPnP.
fn reflect_to_string ( value : & dyn Reflect ) -> String {
use std ::any ::Any ;
use bevy_reflect ::ReflectRef ;
// Essayer de downcaster vers des types primitifs courants
if let Some ( v ) = value . as_any ( ) . downcast_ref ::< String > ( ) {
// Échapper les caractères XML spéciaux
return escape ( v ) . to_string ( ) ;
} else if let Some ( v ) = value . as_any ( ) . downcast_ref ::< u8 > ( ) {
return v . to_string ( ) ;
} else if let Some ( v ) = value . as_any ( ) . downcast_ref ::< u16 > ( ) {
return v . to_string ( ) ;
} else if let Some ( v ) = value . as_any ( ) . downcast_ref ::< u32 > ( ) {
return v . to_string ( ) ;
} else if let Some ( v ) = value . as_any ( ) . downcast_ref ::< i8 > ( ) {
return v . to_string ( ) ;
} else if let Some ( v ) = value . as_any ( ) . downcast_ref ::< i16 > ( ) {
return v . to_string ( ) ;
} else if let Some ( v ) = value . as_any ( ) . downcast_ref ::< i32 > ( ) {
return v . to_string ( ) ;
} else if let Some ( v ) = value . as_any ( ) . downcast_ref ::< f32 > ( ) {
return v . to_string ( ) ;
} else if let Some ( v ) = value . as_any ( ) . downcast_ref ::< f64 > ( ) {
return v . to_string ( ) ;
} else if let Some ( v ) = value . as_any ( ) . downcast_ref ::< bool > ( ) {
return if * v { " 1 " } else { " 0 " } . to_string ( ) ;
} else if let Some ( v ) = value . as_any ( ) . downcast_ref ::< char > ( ) {
return escape ( & v . to_string ( ) ) . to_string ( ) ;
}
// Pour les structures complexes, essayer de sérialiser avec bevy_reflect
match value . reflect_ref ( ) {
ReflectRef ::Struct ( s ) = > {
// Construire un XML simple pour la struct
Self ::serialize_struct_to_xml ( s )
}
ReflectRef ::TupleStruct ( ts ) = > {
// Pour les tuple structs, essayer d'extraire la valeur si c'est un wrapper
if ts . field_len ( ) = = 1 {
if let Some ( inner ) = ts . field ( 0 ) {
// Convertir PartialReflect en Reflect si possible
if let Some ( reflect_val ) = inner . try_as_reflect ( ) {
Self ::reflect_to_string ( reflect_val )
} else {
format! ( " {:?} " , value )
}
} else {
format! ( " {:?} " , value )
}
} else {
format! ( " {:?} " , value )
}
}
ReflectRef ::Enum ( e ) = > {
// Pour les enums, formater comme "Variant(value)"
let variant_name = e . variant_name ( ) ;
if e . field_len ( ) = = 1 {
if let Some ( field ) = e . field_at ( 0 ) {
// Convertir PartialReflect en Reflect si possible
if let Some ( reflect_val ) = field . try_as_reflect ( ) {
format! ( " {} " , Self ::reflect_to_string ( reflect_val ) )
} else {
variant_name . to_string ( )
}
} else {
variant_name . to_string ( )
}
} else {
variant_name . to_string ( )
}
}
_ = > {
// Fallback: utiliser Debug et échapper
let debug_str = format! ( " {:?} " , value ) ;
escape ( & debug_str ) . to_string ( )
}
}
}
/// Sérialise une structure Reflect en XML simple.
fn serialize_struct_to_xml ( s : & dyn bevy_reflect ::Struct ) -> String {
use std ::fmt ::Write ;
use bevy_reflect ::TypeInfo ;
let mut xml = String ::new ( ) ;
// Commencer par ouvrir la balise avec le nom du type
let type_name = s . get_represented_type_info ( )
. and_then ( | ti | {
if let TypeInfo ::Struct ( si ) = ti {
Some ( si . type_path_table ( ) . short_path ( ) )
} else {
None
}
} )
. unwrap_or ( " struct " ) ;
let _ = write! ( & mut xml , " <{}> " , type_name ) ;
// Ajouter chaque champ
for i in 0 .. s . field_len ( ) {
if let Some ( field_name ) = s . name_at ( i ) {
if let Some ( field_value ) = s . field_at ( i ) {
// Convertir PartialReflect en Reflect si possible
if let Some ( reflect_val ) = field_value . try_as_reflect ( ) {
let value_str = Self ::reflect_to_string ( reflect_val ) ;
let _ = write! ( & mut xml , " <{}>{}</{}> " , field_name , value_str , field_name ) ;
}
}
}
}
let _ = write! ( & mut xml , " </{}> " , type_name ) ;
xml
}
2025-10-05 14:35:44 +02:00
/// Démarre le notifier périodique.
///
/// # Arguments
///
/// * `interval` - Intervalle entre les notifications
///
/// # Returns
///
/// Un handle vers la tâche tokio du notifier.
pub fn start_notifier ( & self , interval : Duration ) -> tokio ::task ::JoinHandle < ( ) > {
let instance = self . clone ( ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
tokio ::spawn ( async move {
let mut ticker = time ::interval ( interval ) ;
info! ( " ✅ Starting notifier every {:?} " , interval ) ;
loop {
ticker . tick ( ) . await ;
instance . notify_subscribers ( ) . await ;
}
} )
}
}
/// Handler Axum pour les événements (SUBSCRIBE/UNSUBSCRIBE).
2025-10-09 20:26:51 +02:00
///
/// Gère les requêtes HTTP SUBSCRIBE et UNSUBSCRIBE selon la spécification
/// UPnP Device Architecture.
///
/// # Opérations supportées
///
/// - `SUBSCRIBE` sans SID : Nouvelle souscription
/// - `SUBSCRIBE` avec SID : Renouvellement d'abonnement
/// - `UNSUBSCRIBE` : Annulation d'abonnement
///
/// # Arguments
///
/// * `instance` - L'instance du service
/// * `headers` - En-têtes HTTP de la requête
/// * `req` - La requête HTTP complète
///
/// # Returns
///
/// Une réponse HTTP avec le SID et le timeout pour SUBSCRIBE,
/// ou une simple confirmation pour UNSUBSCRIBE.
2025-10-09 07:00:48 +02:00
async fn event_sub_handler (
2025-10-05 14:35:44 +02:00
State ( instance ) : State < ServiceInstance > ,
headers : HeaderMap ,
req : Request < Body > ,
2025-10-09 07:00:48 +02:00
) -> Response {
2025-10-05 14:35:44 +02:00
info! ( " 📡 Event Subscription request for {} " , instance . get_name ( ) ) ;
let method = req . method ( ) . as_str ( ) ;
2025-10-09 07:00:48 +02:00
let sid = headers
. get ( " SID " )
. and_then ( | v | v . to_str ( ) . ok ( ) )
. unwrap_or ( " " ) ;
let timeout = headers
. get ( " Timeout " )
. and_then ( | v | v . to_str ( ) . ok ( ) )
. unwrap_or ( " " ) ;
let callback = headers
. get ( " Callback " )
. and_then ( | v | v . to_str ( ) . ok ( ) )
. unwrap_or ( " " ) ;
2025-10-05 14:35:44 +02:00
match method {
METHOD_SUBSCRIBE = > {
let ( response_sid , response_timeout ) = if sid . is_empty ( ) {
// Nouvelle souscription
let new_sid = format! ( " uuid: {} " , uuid ::Uuid ::new_v4 ( ) ) ;
if ! callback . is_empty ( ) {
2025-10-09 07:00:48 +02:00
instance
. add_subscriber ( new_sid . clone ( ) , callback . to_string ( ) )
. await ;
2025-10-05 14:35:44 +02:00
}
let timeout_val = if timeout . is_empty ( ) {
" Second-1800 "
} else {
timeout
} ;
2025-10-09 07:00:48 +02:00
info! (
" 🔒 New subscription: SID={}, Callback={}, Timeout={} " ,
new_sid , callback , timeout_val
) ;
2025-10-05 14:35:44 +02:00
let sid_clone = new_sid . clone ( ) ;
let instance_clone = instance . clone ( ) ;
tokio ::spawn ( async move {
instance_clone . send_initial_event ( sid_clone ) . await ;
} ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
( new_sid , timeout_val . to_string ( ) )
} else {
// Renouvellement
instance . renew_subscriber ( sid , timeout ) . await ;
info! ( " ♻️ Renew subscription: SID={}, Timeout={} " , sid , timeout ) ;
( sid . to_string ( ) , timeout . to_string ( ) )
} ;
(
StatusCode ::OK ,
[
(
2025-10-09 07:00:48 +02:00
axum ::http ::header ::HeaderName ::from_static ( " sid " ) ,
axum ::http ::HeaderValue ::from_str ( & response_sid ) . unwrap ( ) ,
2025-10-05 14:35:44 +02:00
) ,
(
2025-10-09 07:00:48 +02:00
axum ::http ::header ::HeaderName ::from_static ( " timeout " ) ,
axum ::http ::HeaderValue ::from_str ( & response_timeout ) . unwrap ( ) ,
2025-10-05 14:35:44 +02:00
) ,
] ,
2025-10-09 07:00:48 +02:00
)
. into_response ( )
2025-10-05 14:35:44 +02:00
}
METHOD_UNSUBSCRIBE = > {
if ! sid . is_empty ( ) {
instance . remove_subscriber ( sid ) . await ;
info! ( " ❌ Unsubscribe SID={} " , sid ) ;
}
StatusCode ::OK . into_response ( )
}
_ = > {
warn! ( " Unsupported EventSub method: {} " , method ) ;
StatusCode ::METHOD_NOT_ALLOWED . into_response ( )
}
}
}
/// Handler Axum pour le contrôle SOAP.
2025-10-09 20:26:51 +02:00
///
/// Gère les requêtes de contrôle SOAP pour invoquer des actions sur le service.
///
/// # Arguments
///
2025-10-09 20:37:05 +02:00
/// * `instance` - L'instance du service (Arc-wrapped)
/// * `body` - Corps de la requête SOAP
2025-10-09 20:26:51 +02:00
///
/// # Returns
///
2025-10-09 20:37:05 +02:00
/// Une réponse SOAP avec le résultat de l'action, ou un SOAP fault en cas d'erreur.
2025-10-09 20:26:51 +02:00
///
2025-10-09 20:37:05 +02:00
/// # Erreurs
2025-10-09 20:26:51 +02:00
///
2025-10-09 20:37:05 +02:00
/// Retourne un SOAP fault dans les cas suivants :
/// - Parsing SOAP invalide
/// - Action non trouvée
/// - Arguments invalides
/// - Échec de l'exécution de l'action
async fn control_handler ( State ( instance ) : State < Arc < ServiceInstance > > , body : String ) -> Response {
use crate ::{
soap ::{ parse_soap_action , build_soap_response , build_soap_fault , error_codes } ,
variable_types ::{ StateValue , UpnpVarType } ,
UpnpTypedInstance ,
} ;
use std ::collections ::HashMap ;
use tracing ::debug ;
2025-10-05 14:35:44 +02:00
info! ( " 📡 Control request for {} " , instance . get_name ( ) ) ;
2025-10-09 20:37:05 +02:00
// Parser le SOAP pour extraire l'action et ses arguments
let soap_action = match parse_soap_action ( body . as_bytes ( ) ) {
Ok ( action ) = > action ,
Err ( e ) = > {
error! ( " ❌ Failed to parse SOAP: {:?} " , e ) ;
let fault_xml = build_soap_fault (
" s:Client " ,
" Invalid SOAP request " ,
Some ( error_codes ::INVALID_ACTION ) ,
Some ( " The SOAP request could not be parsed " )
) . unwrap_or_else ( | _ | String ::from ( " <?xml version= \" 1.0 \" ?><s:Envelope xmlns:s= \" http://schemas.xmlsoap.org/soap/envelope/ \" ><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope> " ) ) ;
return (
StatusCode ::INTERNAL_SERVER_ERROR ,
[ ( axum ::http ::header ::CONTENT_TYPE , " text/xml; charset= \" utf-8 \" " ) ] ,
fault_xml ,
) . into_response ( ) ;
}
} ;
debug! ( " 🎬 Received SOAP action: {} " , soap_action . name ) ;
2025-10-19 02:28:22 +02:00
debug! ( " 🎬 SOAP arguments: {:?} " , soap_action . args ) ;
2025-10-09 20:37:05 +02:00
// Trouver l'action correspondante dans l'instance
let action_instance = match instance . action ( & soap_action . name ) {
Some ( action_inst ) = > action_inst ,
None = > {
error! ( " ❌ Action not found: {} " , soap_action . name ) ;
let fault_xml = build_soap_fault (
" s:Client " ,
" Invalid Action " ,
Some ( error_codes ::INVALID_ACTION ) ,
Some ( & format! ( " Action ' {} ' not found " , soap_action . name ) )
) . unwrap_or_else ( | _ | String ::from ( " <?xml version= \" 1.0 \" ?><s:Envelope xmlns:s= \" http://schemas.xmlsoap.org/soap/envelope/ \" ><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope> " ) ) ;
return (
StatusCode ::INTERNAL_SERVER_ERROR ,
[ ( axum ::http ::header ::CONTENT_TYPE , " text/xml; charset= \" utf-8 \" " ) ] ,
fault_xml ,
) . into_response ( ) ;
}
} ;
// Convertir les arguments SOAP (String) en ActionData (StateValue)
2025-10-19 02:28:22 +02:00
// D'abord, initialiser tous les arguments IN avec leurs valeurs par défaut
2025-10-09 20:37:05 +02:00
let mut action_data = HashMap ::new ( ) ;
2025-10-19 02:28:22 +02:00
for arg_inst in action_instance . arguments_set ( ) . all ( ) {
let arg_model = arg_inst . as_ref ( ) . get_model ( ) ;
if arg_model . is_in ( ) {
if let Some ( var_inst ) = arg_inst . get_variable_instance ( ) {
// Utiliser la valeur par défaut de la variable
let default_value = var_inst . value ( ) ;
action_data . insert ( arg_inst . get_name ( ) . to_string ( ) , default_value ) ;
}
}
}
// Puis, écraser avec les valeurs fournies dans le SOAP
2025-10-09 20:37:05 +02:00
for ( arg_name , arg_value ) in soap_action . args {
2025-10-19 02:28:22 +02:00
debug! ( " 🔍 Processing SOAP arg: {} = '{}' " , arg_name , arg_value ) ;
2025-10-09 20:37:05 +02:00
// Trouver l'argument correspondant pour obtenir son type
if let Some ( arg_inst ) = action_instance . argument ( & arg_name ) {
if let Some ( var_inst ) = arg_inst . get_variable_instance ( ) {
let var_model = var_inst . as_ref ( ) . get_model ( ) ;
// Parser la valeur selon le type de la variable
match StateValue ::from_string ( & arg_value , & var_model . as_state_var_type ( ) ) {
Ok ( value ) = > {
2025-10-19 02:28:22 +02:00
debug! ( " ✅ Parsed {} = {:?} " , arg_name , value ) ;
2025-10-09 20:37:05 +02:00
action_data . insert ( arg_name , value ) ;
}
Err ( e ) = > {
error! ( " ❌ Failed to parse argument '{}': {:?} " , arg_name , e ) ;
let fault_xml = build_soap_fault (
" s:Client " ,
" Invalid Arguments " ,
Some ( error_codes ::ARGUMENT_VALUE_INVALID ) ,
Some ( & format! ( " Invalid value for argument ' {} ' " , arg_name ) )
) . unwrap_or_else ( | _ | String ::from ( " <?xml version= \" 1.0 \" ?><s:Envelope xmlns:s= \" http://schemas.xmlsoap.org/soap/envelope/ \" ><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope> " ) ) ;
return (
StatusCode ::BAD_REQUEST ,
[ ( axum ::http ::header ::CONTENT_TYPE , " text/xml; charset= \" utf-8 \" " ) ] ,
fault_xml ,
) . into_response ( ) ;
}
}
}
}
}
let action_data = Arc ::new ( action_data ) ;
// Exécuter l'action
match action_instance . run ( action_data ) . await {
Ok ( output_data ) = > {
// Convertir les StateValue en String pour SOAP
let mut soap_values = HashMap ::new ( ) ;
for ( key , value ) in output_data . iter ( ) {
soap_values . insert ( key . clone ( ) , value . to_string ( ) ) ;
}
// Construire la réponse SOAP
let response_xml = build_soap_response (
& instance . service_type ( ) ,
& soap_action . name ,
soap_values
) . unwrap_or_else ( | _ | {
build_soap_fault (
" s:Server " ,
" Action Failed " ,
Some ( error_codes ::ACTION_FAILED ) ,
Some ( " Failed to build SOAP response " )
) . unwrap_or_else ( | _ | String ::from ( " <?xml version= \" 1.0 \" ?><s:Envelope xmlns:s= \" http://schemas.xmlsoap.org/soap/envelope/ \" ><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope> " ) )
} ) ;
(
StatusCode ::OK ,
[ ( axum ::http ::header ::CONTENT_TYPE , " text/xml; charset= \" utf-8 \" " ) ] ,
response_xml ,
) . into_response ( )
}
Err ( e ) = > {
error! ( " ❌ Action execution failed: {:?} " , e ) ;
let fault_xml = build_soap_fault (
" s:Server " ,
" Action Failed " ,
Some ( error_codes ::ACTION_FAILED ) ,
Some ( & format! ( " Action execution failed: {:?} " , e ) )
) . unwrap_or_else ( | _ | String ::from ( " <?xml version= \" 1.0 \" ?><s:Envelope xmlns:s= \" http://schemas.xmlsoap.org/soap/envelope/ \" ><s:Body><s:Fault><faultcode>s:Server</faultcode><faultstring>Internal Error</faultstring></s:Fault></s:Body></s:Envelope> " ) ) ;
(
StatusCode ::INTERNAL_SERVER_ERROR ,
[ ( axum ::http ::header ::CONTENT_TYPE , " text/xml; charset= \" utf-8 \" " ) ] ,
fault_xml ,
) . into_response ( )
}
}
2025-10-05 14:35:44 +02:00
}
#[ cfg(test) ]
mod tests {
use super ::* ;
use crate ::services ::Service ;
2025-10-11 10:08:28 +02:00
use bevy_reflect ::Reflect ;
#[ test ]
fn test_reflect_to_string_primitives ( ) {
// Test des types primitifs
assert_eq! ( ServiceInstance ::reflect_to_string ( & 42 i32 ) , " 42 " ) ;
assert_eq! ( ServiceInstance ::reflect_to_string ( & 3.14 f64 ) , " 3.14 " ) ;
assert_eq! ( ServiceInstance ::reflect_to_string ( & true ) , " 1 " ) ;
assert_eq! ( ServiceInstance ::reflect_to_string ( & false ) , " 0 " ) ;
assert_eq! ( ServiceInstance ::reflect_to_string ( & 'a' ) , " a " ) ;
}
#[ test ]
fn test_reflect_to_string_xml_escaping ( ) {
// Test de l'échappement XML
let test_str = " Test <tag> & \" quotes \" " . to_string ( ) ;
let result = ServiceInstance ::reflect_to_string ( & test_str ) ;
// Vérifier que les caractères sont échappés
assert! ( result . contains ( " < " ) ) ;
assert! ( result . contains ( " > " ) ) ;
assert! ( result . contains ( " & " ) ) ;
assert! ( result . contains ( " " " ) ) ;
}
#[ test ]
fn test_reflect_to_string_struct ( ) {
#[ derive(Debug, Clone, Reflect) ]
struct TestStruct {
name : String ,
value : i32 ,
}
let test = TestStruct {
name : " Test " . to_string ( ) ,
value : 42 ,
} ;
let result = ServiceInstance ::reflect_to_string ( & test ) ;
// Vérifier que c'est du XML
assert! ( result . starts_with ( " < " ) ) ;
assert! ( result . ends_with ( " > " ) ) ;
assert! ( result . contains ( " name " ) ) ;
assert! ( result . contains ( " value " ) ) ;
assert! ( result . contains ( " Test " ) ) ;
assert! ( result . contains ( " 42 " ) ) ;
println! ( " Serialized struct: {} " , result ) ;
}
#[ test ]
fn test_reflect_to_string_nested_struct ( ) {
#[ derive(Debug, Clone, Reflect) ]
struct Address {
street : String ,
city : String ,
}
#[ derive(Debug, Clone, Reflect) ]
struct Person {
name : String ,
age : u32 ,
address : Address ,
}
let person = Person {
name : " John <Doe> " . to_string ( ) , // Test XML escaping
age : 30 ,
address : Address {
street : " 123 Main St & Ave " . to_string ( ) ,
city : " Springfield " . to_string ( ) ,
} ,
} ;
let result = ServiceInstance ::reflect_to_string ( & person ) ;
// Vérifier la structure XML
assert! ( result . contains ( " <Person> " ) ) ;
assert! ( result . contains ( " </Person> " ) ) ;
assert! ( result . contains ( " name " ) ) ;
assert! ( result . contains ( " age " ) ) ;
assert! ( result . contains ( " address " ) ) ;
// Vérifier l'échappement XML dans les valeurs imbriquées
assert! ( result . contains ( " < " ) ) ;
assert! ( result . contains ( " > " ) ) ;
assert! ( result . contains ( " & " ) ) ;
println! ( " Nested struct XML: {} " , result ) ;
}
2025-10-05 14:35:44 +02:00
#[ test ]
fn test_service_instance_creation ( ) {
let service = Service ::new ( " AVTransport " . to_string ( ) ) ;
let instance = ServiceInstance ::new ( & service ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
assert_eq! ( instance . get_name ( ) , " AVTransport " ) ;
assert_eq! ( instance . identifier ( ) , " AVTransport " ) ;
}
#[ test ]
fn test_service_urls ( ) {
let service = Service ::new ( " AVTransport " . to_string ( ) ) ;
let instance = ServiceInstance ::new ( & service ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 21:58:36 +02:00
assert_eq! ( instance . route ( ) , " /service/AVTransport " ) ;
assert_eq! ( instance . control_route ( ) , " /service/AVTransport/control " ) ;
assert_eq! ( instance . event_route ( ) , " /service/AVTransport/event " ) ;
assert_eq! ( instance . scpd_route ( ) , " /service/AVTransport/desc.xml " ) ;
2025-10-05 14:35:44 +02:00
}
#[ test ]
fn test_service_type ( ) {
let mut service = Service ::new ( " AVTransport " . to_string ( ) ) ;
service . set_version ( 2 ) . unwrap ( ) ;
let instance = ServiceInstance ::new ( & service ) ;
2025-10-09 07:00:48 +02:00
2025-10-05 14:35:44 +02:00
assert_eq! (
instance . service_type ( ) ,
" urn:schemas-upnp-org:service:AVTransport:2 "
) ;
}
2025-10-09 07:00:48 +02:00
}