172 lines
5.5 KiB
Rust
172 lines
5.5 KiB
Rust
|
|
//! 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<DeviceConfig>,
|
||
|
|
/// Currently registered devices, keyed by device ID.
|
||
|
|
registered: HashMap<String, RegisteredDevice>,
|
||
|
|
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<Vec<u8>> {
|
||
|
|
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!("<sip:{}:{}>", 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<SocketAddr> {
|
||
|
|
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<serde_json::Value> {
|
||
|
|
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
|
||
|
|
}
|
||
|
|
}
|