//! Device registrar — accepts REGISTER from SIP phones and tracks contacts. //! //! When a device sends REGISTER, the registrar responds with 200 OK //! and stores the device's current contact (source IP:port). //! //! Ported from ts/registrar.ts. use crate::config::DeviceConfig; use crate::ipc::{emit_event, OutTx}; use sip_proto::helpers::generate_tag; use sip_proto::message::{ResponseOptions, SipMessage}; use std::collections::HashMap; use std::net::SocketAddr; use std::time::{Duration, Instant}; const MAX_EXPIRES: u32 = 300; /// A registered device entry. #[derive(Debug, Clone)] pub struct RegisteredDevice { pub device_id: String, pub display_name: String, pub extension: String, pub contact_addr: SocketAddr, pub registered_at: Instant, pub expires_at: Instant, pub aor: String, } /// Manages device registrations. pub struct Registrar { /// Known device configs (from app config). devices: Vec, /// Currently registered devices, keyed by device ID. registered: HashMap, out_tx: OutTx, } impl Registrar { pub fn new(out_tx: OutTx) -> Self { Self { devices: Vec::new(), registered: HashMap::new(), out_tx, } } /// Update the known device list from config. pub fn configure(&mut self, devices: &[DeviceConfig]) { self.devices = devices.to_vec(); } /// Try to handle a SIP REGISTER from a device. /// Returns Some(response_bytes) if handled, None if not a known device. pub fn handle_register( &mut self, msg: &SipMessage, from_addr: SocketAddr, ) -> Option> { if msg.method() != Some("REGISTER") { return None; } // Find the device by matching the source IP against expectedAddress. let from_ip = from_addr.ip().to_string(); let device = self.devices.iter().find(|d| d.expected_address == from_ip)?; let from_header = msg.get_header("From").unwrap_or(""); let aor = SipMessage::extract_uri(from_header) .map(|s| s.to_string()) .unwrap_or_else(|| format!("sip:{}@{}", device.extension, from_ip)); let expires_header = msg.get_header("Expires"); let requested: u32 = expires_header .and_then(|s| s.parse().ok()) .unwrap_or(3600); let expires = requested.min(MAX_EXPIRES); let entry = RegisteredDevice { device_id: device.id.clone(), display_name: device.display_name.clone(), extension: device.extension.clone(), contact_addr: from_addr, registered_at: Instant::now(), expires_at: Instant::now() + Duration::from_secs(expires as u64), aor: aor.clone(), }; self.registered.insert(device.id.clone(), entry); // Emit event to TypeScript. emit_event( &self.out_tx, "device_registered", serde_json::json!({ "device_id": device.id, "display_name": device.display_name, "address": from_ip, "port": from_addr.port(), "aor": aor, "expires": expires, }), ); // Build 200 OK response. let contact = msg .get_header("Contact") .map(|s| s.to_string()) .unwrap_or_else(|| format!("", from_ip, from_addr.port())); let response = SipMessage::create_response( 200, "OK", msg, Some(ResponseOptions { to_tag: Some(generate_tag()), contact: Some(contact), extra_headers: Some(vec![( "Expires".to_string(), expires.to_string(), )]), ..Default::default() }), ); Some(response.serialize()) } /// Get the contact address for a registered device. pub fn get_device_contact(&self, device_id: &str) -> Option { let entry = self.registered.get(device_id)?; if Instant::now() > entry.expires_at { return None; } Some(entry.contact_addr) } /// Check if a source address belongs to a known device. pub fn is_known_device_address(&self, addr: &str) -> bool { self.devices.iter().any(|d| d.expected_address == addr) } /// Find a registered device by its source IP address. pub fn find_by_address(&self, addr: &SocketAddr) -> Option<&RegisteredDevice> { let ip = addr.ip().to_string(); self.registered.values().find(|e| { e.contact_addr.ip().to_string() == ip && Instant::now() <= e.expires_at }) } /// Get all device statuses for the dashboard. pub fn get_all_statuses(&self) -> Vec { let now = Instant::now(); let mut result = Vec::new(); for dc in &self.devices { let reg = self.registered.get(&dc.id); let connected = reg.map(|r| now <= r.expires_at).unwrap_or(false); result.push(serde_json::json!({ "id": dc.id, "displayName": dc.display_name, "address": reg.filter(|_| connected).map(|r| r.contact_addr.ip().to_string()), "port": reg.filter(|_| connected).map(|r| r.contact_addr.port()), "aor": reg.map(|r| r.aor.as_str()).unwrap_or(""), "connected": connected, "isBrowser": false, })); } result } }