Files
siprouter/rust/crates/sip-proto/src/dialog.rs

409 lines
13 KiB
Rust

//! SIP dialog state machine (RFC 3261 §12).
//!
//! Tracks local/remote tags, CSeq counters, route set, and remote target.
//! Provides methods to build in-dialog requests (BYE, re-INVITE, ACK, CANCEL).
//!
//! Ported from ts/sip/dialog.ts.
use crate::helpers::{generate_branch, generate_tag};
use crate::message::SipMessage;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DialogState {
Early,
Confirmed,
Terminated,
}
/// SIP dialog state per RFC 3261 §12.
#[derive(Debug, Clone)]
pub struct SipDialog {
pub call_id: String,
pub local_tag: String,
pub remote_tag: Option<String>,
pub local_uri: String,
pub remote_uri: String,
pub local_cseq: u32,
pub remote_cseq: u32,
pub route_set: Vec<String>,
pub remote_target: String,
pub state: DialogState,
pub local_host: String,
pub local_port: u16,
}
impl SipDialog {
/// Create a dialog from an INVITE we are sending (UAC side).
/// The dialog enters Early state; call `process_response()` when responses arrive.
pub fn from_uac_invite(invite: &SipMessage, local_host: &str, local_port: u16) -> Self {
let from = invite.get_header("From").unwrap_or("");
let to = invite.get_header("To").unwrap_or("");
let local_cseq = invite
.get_header("CSeq")
.and_then(|c| c.split_whitespace().next())
.and_then(|s| s.parse().ok())
.unwrap_or(1);
Self {
call_id: invite.call_id().to_string(),
local_tag: SipMessage::extract_tag(from)
.map(|s| s.to_string())
.unwrap_or_else(generate_tag),
remote_tag: None,
local_uri: SipMessage::extract_uri(from)
.unwrap_or("")
.to_string(),
remote_uri: SipMessage::extract_uri(to).unwrap_or("").to_string(),
local_cseq,
remote_cseq: 0,
route_set: Vec::new(),
remote_target: invite
.request_uri()
.or_else(|| SipMessage::extract_uri(to))
.unwrap_or("")
.to_string(),
state: DialogState::Early,
local_host: local_host.to_string(),
local_port,
}
}
/// Create a dialog from an INVITE we received (UAS side).
pub fn from_uas_invite(
invite: &SipMessage,
local_tag: &str,
local_host: &str,
local_port: u16,
) -> Self {
let from = invite.get_header("From").unwrap_or("");
let to = invite.get_header("To").unwrap_or("");
let contact = invite.get_header("Contact");
let remote_target = contact
.and_then(SipMessage::extract_uri)
.or_else(|| SipMessage::extract_uri(from))
.unwrap_or("")
.to_string();
Self {
call_id: invite.call_id().to_string(),
local_tag: local_tag.to_string(),
remote_tag: SipMessage::extract_tag(from).map(|s| s.to_string()),
local_uri: SipMessage::extract_uri(to).unwrap_or("").to_string(),
remote_uri: SipMessage::extract_uri(from).unwrap_or("").to_string(),
local_cseq: 0,
remote_cseq: 0,
route_set: Vec::new(),
remote_target,
state: DialogState::Early,
local_host: local_host.to_string(),
local_port,
}
}
/// Update dialog state from a received response.
pub fn process_response(&mut self, response: &SipMessage) {
let to = response.get_header("To").unwrap_or("");
let tag = SipMessage::extract_tag(to).map(|s| s.to_string());
let code = response.status_code().unwrap_or(0);
// Always update remoteTag from 2xx (RFC 3261 §12.1.2).
if let Some(ref t) = tag {
if code >= 200 && code < 300 {
self.remote_tag = Some(t.clone());
} else if self.remote_tag.is_none() {
self.remote_tag = Some(t.clone());
}
}
// Update remote target from Contact.
if let Some(contact) = response.get_header("Contact") {
if let Some(uri) = SipMessage::extract_uri(contact) {
self.remote_target = uri.to_string();
}
}
// Record-Route → route set (in reverse for UAC).
if self.state == DialogState::Early {
let rr: Vec<String> = response
.headers
.iter()
.filter(|(n, _)| n.to_ascii_lowercase() == "record-route")
.map(|(_, v)| v.clone())
.collect();
if !rr.is_empty() {
let mut reversed = rr;
reversed.reverse();
self.route_set = reversed;
}
}
if code >= 200 && code < 300 {
self.state = DialogState::Confirmed;
} else if code >= 300 {
self.state = DialogState::Terminated;
}
}
/// Build an in-dialog request (BYE, re-INVITE, INFO, ...).
/// Automatically increments the local CSeq.
pub fn create_request(
&mut self,
method: &str,
body: Option<&str>,
content_type: Option<&str>,
extra_headers: Option<Vec<(String, String)>>,
) -> SipMessage {
self.local_cseq += 1;
let branch = generate_branch();
let remote_tag_str = self
.remote_tag
.as_ref()
.map(|t| format!(";tag={t}"))
.unwrap_or_default();
let mut headers = vec![
(
"Via".to_string(),
format!(
"SIP/2.0/UDP {}:{};branch={branch};rport",
self.local_host, self.local_port
),
),
(
"From".to_string(),
format!("<{}>;tag={}", self.local_uri, self.local_tag),
),
(
"To".to_string(),
format!("<{}>{remote_tag_str}", self.remote_uri),
),
("Call-ID".to_string(), self.call_id.clone()),
(
"CSeq".to_string(),
format!("{} {method}", self.local_cseq),
),
("Max-Forwards".to_string(), "70".to_string()),
];
for route in &self.route_set {
headers.push(("Route".to_string(), route.clone()));
}
headers.push((
"Contact".to_string(),
format!("<sip:{}:{}>", self.local_host, self.local_port),
));
if let Some(extra) = extra_headers {
headers.extend(extra);
}
let body_str = body.unwrap_or("");
if !body_str.is_empty() {
if let Some(ct) = content_type {
headers.push(("Content-Type".to_string(), ct.to_string()));
}
}
headers.push(("Content-Length".to_string(), body_str.len().to_string()));
let ruri = self.resolve_ruri();
SipMessage::new(
format!("{method} {ruri} SIP/2.0"),
headers,
body_str.to_string(),
)
}
/// Build an ACK for a 2xx response to INVITE (RFC 3261 §13.2.2.4).
pub fn create_ack(&self) -> SipMessage {
let branch = generate_branch();
let remote_tag_str = self
.remote_tag
.as_ref()
.map(|t| format!(";tag={t}"))
.unwrap_or_default();
let mut headers = vec![
(
"Via".to_string(),
format!(
"SIP/2.0/UDP {}:{};branch={branch};rport",
self.local_host, self.local_port
),
),
(
"From".to_string(),
format!("<{}>;tag={}", self.local_uri, self.local_tag),
),
(
"To".to_string(),
format!("<{}>{remote_tag_str}", self.remote_uri),
),
("Call-ID".to_string(), self.call_id.clone()),
(
"CSeq".to_string(),
format!("{} ACK", self.local_cseq),
),
("Max-Forwards".to_string(), "70".to_string()),
];
for route in &self.route_set {
headers.push(("Route".to_string(), route.clone()));
}
headers.push(("Content-Length".to_string(), "0".to_string()));
let ruri = self.resolve_ruri();
SipMessage::new(format!("ACK {ruri} SIP/2.0"), headers, String::new())
}
/// Build a CANCEL for the original INVITE (same branch, CSeq).
pub fn create_cancel(&self, original_invite: &SipMessage) -> SipMessage {
let via = original_invite.get_header("Via").unwrap_or("").to_string();
let from = original_invite.get_header("From").unwrap_or("").to_string();
let to = original_invite.get_header("To").unwrap_or("").to_string();
let headers = vec![
("Via".to_string(), via),
("From".to_string(), from),
("To".to_string(), to),
("Call-ID".to_string(), self.call_id.clone()),
(
"CSeq".to_string(),
format!("{} CANCEL", self.local_cseq),
),
("Max-Forwards".to_string(), "70".to_string()),
("Content-Length".to_string(), "0".to_string()),
];
let ruri = original_invite
.request_uri()
.unwrap_or(&self.remote_target)
.to_string();
SipMessage::new(
format!("CANCEL {ruri} SIP/2.0"),
headers,
String::new(),
)
}
/// Transition the dialog to terminated state.
pub fn terminate(&mut self) {
self.state = DialogState::Terminated;
}
/// Resolve Request-URI from route set or remote target.
fn resolve_ruri(&self) -> &str {
if !self.route_set.is_empty() {
if let Some(top_route) = SipMessage::extract_uri(&self.route_set[0]) {
if top_route.contains(";lr") {
return &self.remote_target; // loose routing
}
return top_route; // strict routing
}
}
&self.remote_target
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::RequestOptions;
fn make_invite() -> SipMessage {
SipMessage::create_request(
"INVITE",
"sip:callee@host",
RequestOptions {
via_host: "192.168.1.1".to_string(),
via_port: 5070,
via_transport: None,
via_branch: Some("z9hG4bK-test".to_string()),
from_uri: "sip:caller@proxy".to_string(),
from_display_name: None,
from_tag: Some("from-tag".to_string()),
to_uri: "sip:callee@host".to_string(),
to_display_name: None,
to_tag: None,
call_id: Some("test-dialog-call".to_string()),
cseq: Some(1),
contact: Some("<sip:caller@192.168.1.1:5070>".to_string()),
max_forwards: None,
body: None,
content_type: None,
extra_headers: None,
},
)
}
#[test]
fn uac_dialog_lifecycle() {
let invite = make_invite();
let mut dialog = SipDialog::from_uac_invite(&invite, "192.168.1.1", 5070);
assert_eq!(dialog.state, DialogState::Early);
assert_eq!(dialog.call_id, "test-dialog-call");
assert_eq!(dialog.local_tag, "from-tag");
assert!(dialog.remote_tag.is_none());
// Simulate 200 OK
let response = SipMessage::create_response(
200,
"OK",
&invite,
Some(crate::message::ResponseOptions {
to_tag: Some("remote-tag".to_string()),
contact: Some("<sip:callee@10.0.0.1:5060>".to_string()),
..Default::default()
}),
);
dialog.process_response(&response);
assert_eq!(dialog.state, DialogState::Confirmed);
assert_eq!(dialog.remote_tag.as_deref(), Some("remote-tag"));
assert_eq!(dialog.remote_target, "sip:callee@10.0.0.1:5060");
}
#[test]
fn create_bye() {
let invite = make_invite();
let mut dialog = SipDialog::from_uac_invite(&invite, "192.168.1.1", 5070);
dialog.remote_tag = Some("remote-tag".to_string());
dialog.state = DialogState::Confirmed;
let bye = dialog.create_request("BYE", None, None, None);
assert_eq!(bye.method(), Some("BYE"));
assert_eq!(bye.call_id(), "test-dialog-call");
assert_eq!(dialog.local_cseq, 2);
let to = bye.get_header("To").unwrap();
assert!(to.contains("tag=remote-tag"));
}
#[test]
fn create_ack() {
let invite = make_invite();
let mut dialog = SipDialog::from_uac_invite(&invite, "192.168.1.1", 5070);
dialog.remote_tag = Some("remote-tag".to_string());
let ack = dialog.create_ack();
assert_eq!(ack.method(), Some("ACK"));
assert!(ack.get_header("CSeq").unwrap().contains("ACK"));
}
#[test]
fn create_cancel() {
let invite = make_invite();
let dialog = SipDialog::from_uac_invite(&invite, "192.168.1.1", 5070);
let cancel = dialog.create_cancel(&invite);
assert_eq!(cancel.method(), Some("CANCEL"));
assert!(cancel.get_header("CSeq").unwrap().contains("CANCEL"));
assert!(cancel.start_line.contains("sip:callee@host"));
}
}