Files
siprouter/rust/crates/proxy-engine/src/registrar.rs

153 lines
4.9 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,
// These fields are populated at REGISTER time for logging/debugging but are
// not read back — device identity flows via the `device_registered` push
// event, not via struct queries. Kept behind allow(dead_code) because
// removing them would churn handle_register for no runtime benefit.
#[allow(dead_code)]
pub display_name: String,
#[allow(dead_code)]
pub extension: String,
pub contact_addr: SocketAddr,
#[allow(dead_code)]
pub registered_at: Instant,
pub expires_at: Instant,
#[allow(dead_code)]
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)
}
/// 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
})
}
}