Skip to main content

sip_core/
dialog.rs

1use crate::header::{extract_tag, extract_uri, HeaderName};
2use crate::message::{SipMessage, SipMethod};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum DialogState {
6    /// Dialog created, INVITE sent, waiting for response
7    Early,
8    /// 2xx received, dialog confirmed
9    Confirmed,
10    /// BYE sent or received, dialog is ending
11    Terminated,
12}
13
14#[derive(Debug, Clone)]
15pub struct SipDialog {
16    pub call_id: String,
17    pub local_tag: String,
18    pub remote_tag: Option<String>,
19    pub local_uri: String,
20    pub remote_uri: String,
21    pub remote_target: Option<String>,
22    pub local_cseq: u32,
23    pub remote_cseq: Option<u32>,
24    pub state: DialogState,
25}
26
27impl SipDialog {
28    /// Create a dialog from an outgoing INVITE request (UAC side)
29    pub fn new_uac(call_id: String, local_tag: String, local_uri: String, remote_uri: String) -> Self {
30        Self {
31            call_id,
32            local_tag,
33            remote_tag: None,
34            local_uri,
35            remote_uri,
36            remote_target: None,
37            local_cseq: 1,
38            remote_cseq: None,
39            state: DialogState::Early,
40        }
41    }
42
43    /// Create a dialog from an incoming INVITE request (UAS side)
44    pub fn new_uas(
45        call_id: String,
46        local_tag: String,
47        remote_tag: String,
48        local_uri: String,
49        remote_uri: String,
50    ) -> Self {
51        Self {
52            call_id,
53            local_tag,
54            remote_tag: Some(remote_tag),
55            local_uri,
56            remote_uri,
57            remote_target: None,
58            local_cseq: 1,
59            remote_cseq: None,
60            state: DialogState::Early,
61        }
62    }
63
64    /// Try to create a dialog from an incoming INVITE request
65    pub fn from_invite(msg: &SipMessage) -> Option<Self> {
66        if let SipMessage::Request(req) = msg {
67            if req.method != SipMethod::Invite {
68                return None;
69            }
70
71            let call_id = req.headers.get(&HeaderName::CallId)?.0.clone();
72
73            let from_val = req.headers.get(&HeaderName::From)?.as_str();
74            let remote_tag = extract_tag(from_val)?;
75            let remote_uri = extract_uri(from_val)?;
76
77            let to_val = req.headers.get(&HeaderName::To)?.as_str();
78            let local_uri = extract_uri(to_val)?;
79
80            let local_tag = crate::header::generate_tag();
81
82            let contact = req
83                .headers
84                .get(&HeaderName::Contact)
85                .and_then(|v| extract_uri(v.as_str()));
86
87            Some(Self {
88                call_id,
89                local_tag,
90                remote_tag: Some(remote_tag),
91                local_uri,
92                remote_uri,
93                remote_target: contact,
94                local_cseq: 1,
95                remote_cseq: None,
96                state: DialogState::Early,
97            })
98        } else {
99            None
100        }
101    }
102
103    /// Process an incoming response (for UAC dialogs)
104    pub fn process_response(&mut self, msg: &SipMessage) -> bool {
105        if let SipMessage::Response(res) = msg {
106            // Verify Call-ID matches
107            if let Some(call_id) = res.headers.get(&HeaderName::CallId) {
108                if call_id.0 != self.call_id {
109                    return false;
110                }
111            } else {
112                return false;
113            }
114
115            // Extract remote tag from To header
116            if let Some(to_val) = res.headers.get(&HeaderName::To) {
117                if let Some(tag) = extract_tag(to_val.as_str()) {
118                    self.remote_tag = Some(tag);
119                }
120            }
121
122            // Extract remote target from Contact
123            if let Some(contact) = res.headers.get(&HeaderName::Contact) {
124                self.remote_target = extract_uri(contact.as_str());
125            }
126
127            // Update state based on status code
128            match res.status.0 {
129                100..=199 => {
130                    // Provisional: dialog stays Early
131                    if self.state == DialogState::Early {
132                        // Already early, no change
133                    }
134                }
135                200..=299 => {
136                    self.state = DialogState::Confirmed;
137                }
138                300..=699 => {
139                    self.state = DialogState::Terminated;
140                }
141                _ => {}
142            }
143
144            true
145        } else {
146            false
147        }
148    }
149
150    /// Process an incoming BYE request
151    pub fn process_bye(&mut self, msg: &SipMessage) -> bool {
152        if let SipMessage::Request(req) = msg {
153            if req.method == SipMethod::Bye {
154                if let Some(call_id) = req.headers.get(&HeaderName::CallId) {
155                    if call_id.0 == self.call_id {
156                        self.state = DialogState::Terminated;
157                        return true;
158                    }
159                }
160            }
161        }
162        false
163    }
164
165    /// Mark the dialog as terminated (when we send BYE)
166    pub fn terminate(&mut self) {
167        self.state = DialogState::Terminated;
168    }
169
170    /// Get the next CSeq number
171    pub fn next_cseq(&mut self) -> u32 {
172        self.local_cseq += 1;
173        self.local_cseq
174    }
175
176    /// Check if a message belongs to this dialog
177    pub fn matches(&self, msg: &SipMessage) -> bool {
178        let headers = msg.headers();
179
180        // Check Call-ID
181        if let Some(call_id) = headers.get(&HeaderName::CallId) {
182            if call_id.0 != self.call_id {
183                return false;
184            }
185        } else {
186            return false;
187        }
188
189        // Check tags
190        if let Some(from_val) = headers.get(&HeaderName::From) {
191            let from_tag = extract_tag(from_val.as_str());
192            if let Some(to_val) = headers.get(&HeaderName::To) {
193                let to_tag = extract_tag(to_val.as_str());
194
195                // For requests coming from the remote side
196                if msg.is_request() {
197                    if let Some(ref rt) = self.remote_tag {
198                        if from_tag.as_deref() != Some(rt.as_str()) {
199                            return false;
200                        }
201                    }
202                    if to_tag.as_deref() != Some(self.local_tag.as_str()) {
203                        return false;
204                    }
205                } else {
206                    // For responses
207                    if let Some(ref rt) = self.remote_tag {
208                        if to_tag.as_deref() != Some(rt.as_str())
209                            && from_tag.as_deref() != Some(self.local_tag.as_str())
210                        {
211                            return false;
212                        }
213                    }
214                }
215            }
216        }
217
218        true
219    }
220
221    pub fn is_confirmed(&self) -> bool {
222        self.state == DialogState::Confirmed
223    }
224
225    pub fn is_terminated(&self) -> bool {
226        self.state == DialogState::Terminated
227    }
228
229    pub fn is_early(&self) -> bool {
230        self.state == DialogState::Early
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use crate::header::Headers;
238    use crate::message::{SipRequest, SipResponse, StatusCode};
239
240    fn make_invite() -> SipMessage {
241        let mut headers = Headers::new();
242        headers.add(
243            HeaderName::Via,
244            "SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK776",
245        );
246        headers.add(
247            HeaderName::From,
248            "\"Alice\" <sip:alice@atlanta.com>;tag=abc123",
249        );
250        headers.add(HeaderName::To, "<sip:bob@biloxi.com>");
251        headers.add(HeaderName::CallId, "test-call-id-12345");
252        headers.add(HeaderName::CSeq, "1 INVITE");
253        headers.add(HeaderName::Contact, "<sip:alice@10.0.0.1:5060>");
254        headers.add(HeaderName::MaxForwards, "70");
255        headers.add(HeaderName::ContentLength, "0");
256
257        SipMessage::Request(SipRequest {
258            method: SipMethod::Invite,
259            uri: "sip:bob@biloxi.com".to_string(),
260            version: "SIP/2.0".to_string(),
261            headers,
262            body: None,
263        })
264    }
265
266    fn make_200_ok(call_id: &str, from_tag: &str, to_tag: &str) -> SipMessage {
267        let mut headers = Headers::new();
268        headers.add(
269            HeaderName::Via,
270            "SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK776",
271        );
272        headers.add(
273            HeaderName::From,
274            format!("<sip:alice@atlanta.com>;tag={}", from_tag),
275        );
276        headers.add(
277            HeaderName::To,
278            format!("<sip:bob@biloxi.com>;tag={}", to_tag),
279        );
280        headers.add(HeaderName::CallId, call_id);
281        headers.add(HeaderName::CSeq, "1 INVITE");
282        headers.add(HeaderName::Contact, "<sip:bob@10.0.0.2:5060>");
283        headers.add(HeaderName::ContentLength, "0");
284
285        SipMessage::Response(SipResponse {
286            version: "SIP/2.0".to_string(),
287            status: StatusCode::OK,
288            reason: "OK".to_string(),
289            headers,
290            body: None,
291        })
292    }
293
294    fn make_bye(call_id: &str, from_tag: &str, to_tag: &str) -> SipMessage {
295        let mut headers = Headers::new();
296        headers.add(
297            HeaderName::Via,
298            "SIP/2.0/UDP 10.0.0.2:5060;branch=z9hG4bKbye",
299        );
300        headers.add(
301            HeaderName::From,
302            format!("<sip:bob@biloxi.com>;tag={}", from_tag),
303        );
304        headers.add(
305            HeaderName::To,
306            format!("<sip:alice@atlanta.com>;tag={}", to_tag),
307        );
308        headers.add(HeaderName::CallId, call_id);
309        headers.add(HeaderName::CSeq, "1 BYE");
310        headers.add(HeaderName::ContentLength, "0");
311
312        SipMessage::Request(SipRequest {
313            method: SipMethod::Bye,
314            uri: "sip:alice@10.0.0.1:5060".to_string(),
315            version: "SIP/2.0".to_string(),
316            headers,
317            body: None,
318        })
319    }
320
321    #[test]
322    fn test_dialog_new_uac() {
323        let dialog = SipDialog::new_uac(
324            "call-123".to_string(),
325            "tag-local".to_string(),
326            "sip:alice@atlanta.com".to_string(),
327            "sip:bob@biloxi.com".to_string(),
328        );
329
330        assert_eq!(dialog.state, DialogState::Early);
331        assert_eq!(dialog.call_id, "call-123");
332        assert_eq!(dialog.local_tag, "tag-local");
333        assert!(dialog.remote_tag.is_none());
334        assert!(dialog.is_early());
335    }
336
337    #[test]
338    fn test_dialog_from_invite() {
339        let invite = make_invite();
340        let dialog = SipDialog::from_invite(&invite).unwrap();
341
342        assert_eq!(dialog.call_id, "test-call-id-12345");
343        assert_eq!(dialog.remote_tag, Some("abc123".to_string()));
344        assert_eq!(dialog.remote_uri, "sip:alice@atlanta.com");
345        assert_eq!(dialog.local_uri, "sip:bob@biloxi.com");
346        assert_eq!(dialog.state, DialogState::Early);
347    }
348
349    #[test]
350    fn test_dialog_from_invite_requires_invite_method() {
351        let mut headers = Headers::new();
352        headers.add(HeaderName::From, "<sip:alice@atlanta.com>;tag=abc");
353        headers.add(HeaderName::To, "<sip:bob@biloxi.com>");
354        headers.add(HeaderName::CallId, "test");
355
356        let bye = SipMessage::Request(SipRequest {
357            method: SipMethod::Bye,
358            uri: "sip:bob@biloxi.com".to_string(),
359            version: "SIP/2.0".to_string(),
360            headers,
361            body: None,
362        });
363
364        assert!(SipDialog::from_invite(&bye).is_none());
365    }
366
367    #[test]
368    fn test_dialog_process_response_ok() {
369        let mut dialog = SipDialog::new_uac(
370            "test-call-id-12345".to_string(),
371            "local-tag".to_string(),
372            "sip:alice@atlanta.com".to_string(),
373            "sip:bob@biloxi.com".to_string(),
374        );
375
376        let ok = make_200_ok("test-call-id-12345", "local-tag", "remote-tag");
377        assert!(dialog.process_response(&ok));
378        assert_eq!(dialog.state, DialogState::Confirmed);
379        assert_eq!(dialog.remote_tag, Some("remote-tag".to_string()));
380        assert!(dialog.is_confirmed());
381    }
382
383    #[test]
384    fn test_dialog_process_response_wrong_callid() {
385        let mut dialog = SipDialog::new_uac(
386            "call-1".to_string(),
387            "tag-1".to_string(),
388            "sip:alice@a.com".to_string(),
389            "sip:bob@b.com".to_string(),
390        );
391
392        let ok = make_200_ok("call-2", "tag-1", "tag-remote");
393        assert!(!dialog.process_response(&ok));
394        assert_eq!(dialog.state, DialogState::Early);
395    }
396
397    #[test]
398    fn test_dialog_process_bye() {
399        let mut dialog = SipDialog::new_uac(
400            "call-123".to_string(),
401            "local-tag".to_string(),
402            "sip:alice@atlanta.com".to_string(),
403            "sip:bob@biloxi.com".to_string(),
404        );
405        dialog.state = DialogState::Confirmed;
406        dialog.remote_tag = Some("remote-tag".to_string());
407
408        let bye = make_bye("call-123", "remote-tag", "local-tag");
409        assert!(dialog.process_bye(&bye));
410        assert_eq!(dialog.state, DialogState::Terminated);
411        assert!(dialog.is_terminated());
412    }
413
414    #[test]
415    fn test_dialog_terminate() {
416        let mut dialog = SipDialog::new_uac(
417            "call-1".to_string(),
418            "t1".to_string(),
419            "sip:a@a.com".to_string(),
420            "sip:b@b.com".to_string(),
421        );
422        dialog.state = DialogState::Confirmed;
423        dialog.terminate();
424        assert_eq!(dialog.state, DialogState::Terminated);
425    }
426
427    #[test]
428    fn test_dialog_next_cseq() {
429        let mut dialog = SipDialog::new_uac(
430            "call-1".to_string(),
431            "t1".to_string(),
432            "sip:a@a.com".to_string(),
433            "sip:b@b.com".to_string(),
434        );
435        assert_eq!(dialog.next_cseq(), 2);
436        assert_eq!(dialog.next_cseq(), 3);
437        assert_eq!(dialog.next_cseq(), 4);
438    }
439
440    #[test]
441    fn test_dialog_provisional_keeps_early() {
442        let mut dialog = SipDialog::new_uac(
443            "call-prov".to_string(),
444            "local-tag".to_string(),
445            "sip:alice@a.com".to_string(),
446            "sip:bob@b.com".to_string(),
447        );
448
449        let mut headers = Headers::new();
450        headers.add(HeaderName::Via, "SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK1");
451        headers.add(HeaderName::From, "<sip:alice@a.com>;tag=local-tag");
452        headers.add(HeaderName::To, "<sip:bob@b.com>;tag=remote-tag");
453        headers.add(HeaderName::CallId, "call-prov");
454        headers.add(HeaderName::CSeq, "1 INVITE");
455        headers.add(HeaderName::ContentLength, "0");
456
457        let ringing = SipMessage::Response(SipResponse {
458            version: "SIP/2.0".to_string(),
459            status: StatusCode::RINGING,
460            reason: "Ringing".to_string(),
461            headers,
462            body: None,
463        });
464
465        assert!(dialog.process_response(&ringing));
466        assert_eq!(dialog.state, DialogState::Early);
467        assert_eq!(dialog.remote_tag, Some("remote-tag".to_string()));
468    }
469
470    #[test]
471    fn test_dialog_error_response_terminates() {
472        let mut dialog = SipDialog::new_uac(
473            "call-err".to_string(),
474            "local-tag".to_string(),
475            "sip:alice@a.com".to_string(),
476            "sip:bob@b.com".to_string(),
477        );
478
479        let mut headers = Headers::new();
480        headers.add(HeaderName::Via, "SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK1");
481        headers.add(HeaderName::From, "<sip:alice@a.com>;tag=local-tag");
482        headers.add(HeaderName::To, "<sip:bob@b.com>;tag=remote-tag");
483        headers.add(HeaderName::CallId, "call-err");
484        headers.add(HeaderName::CSeq, "1 INVITE");
485        headers.add(HeaderName::ContentLength, "0");
486
487        let not_found = SipMessage::Response(SipResponse {
488            version: "SIP/2.0".to_string(),
489            status: StatusCode::NOT_FOUND,
490            reason: "Not Found".to_string(),
491            headers,
492            body: None,
493        });
494
495        assert!(dialog.process_response(&not_found));
496        assert_eq!(dialog.state, DialogState::Terminated);
497    }
498
499    #[test]
500    fn test_dialog_new_uas() {
501        let dialog = SipDialog::new_uas(
502            "call-uas".to_string(),
503            "local-tag".to_string(),
504            "remote-tag".to_string(),
505            "sip:bob@b.com".to_string(),
506            "sip:alice@a.com".to_string(),
507        );
508
509        assert_eq!(dialog.state, DialogState::Early);
510        assert_eq!(dialog.remote_tag, Some("remote-tag".to_string()));
511    }
512}