Skip to main content

sip_uri/
sip_uri.rs

1use std::fmt;
2use std::str::FromStr;
3
4use crate::error::ParseSipUriError;
5use crate::host::Host;
6use crate::params;
7use crate::parse;
8
9type Params = Vec<(String, Option<String>)>;
10type Headers = Vec<(String, String)>;
11
12type UserinfoResult = Result<(Option<String>, Params, Option<String>), ParseSipUriError>;
13type HostportResult =
14    Result<(Host, Option<u16>, Params, Headers, Option<String>), ParseSipUriError>;
15
16/// SIP or SIPS URI per RFC 3261 §19.
17///
18/// Supports the full grammar including user-params (`;` within userinfo),
19/// password, IPv6 hosts, URI parameters, and headers.
20#[derive(Debug, Clone, PartialEq, Eq)]
21#[non_exhaustive]
22pub struct SipUri {
23    scheme: Scheme,
24    user: Option<String>,
25    user_params: Vec<(String, Option<String>)>,
26    password: Option<String>,
27    host: Host,
28    port: Option<u16>,
29    params: Vec<(String, Option<String>)>,
30    headers: Vec<(String, String)>,
31    fragment: Option<String>,
32}
33
34/// SIP URI scheme.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[non_exhaustive]
37pub enum Scheme {
38    /// `sip:` (default port 5060)
39    Sip,
40    /// `sips:` (default port 5061)
41    Sips,
42}
43
44impl fmt::Display for Scheme {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Scheme::Sip => write!(f, "sip"),
48            Scheme::Sips => write!(f, "sips"),
49        }
50    }
51}
52
53impl SipUri {
54    /// Create a new SIP URI with the given host and `sip:` scheme.
55    pub fn new(host: Host) -> Self {
56        SipUri {
57            scheme: Scheme::Sip,
58            user: None,
59            user_params: Vec::new(),
60            password: None,
61            host,
62            port: None,
63            params: Vec::new(),
64            headers: Vec::new(),
65            fragment: None,
66        }
67    }
68
69    /// Set the URI scheme.
70    pub fn with_scheme(mut self, scheme: Scheme) -> Self {
71        self.scheme = scheme;
72        self
73    }
74
75    /// Set the user part.
76    pub fn with_user(mut self, user: impl Into<String>) -> Self {
77        self.user = Some(user.into());
78        self
79    }
80
81    /// Replace all user-params (parameters within the userinfo, before `@`).
82    pub fn with_user_params(mut self, params: Vec<(String, Option<String>)>) -> Self {
83        self.user_params = params;
84        self
85    }
86
87    /// Add a single user-param (parameter within the userinfo, before `@`).
88    pub fn with_user_param(mut self, name: impl Into<String>, value: Option<String>) -> Self {
89        self.user_params
90            .push((name.into(), value));
91        self
92    }
93
94    /// Set the password.
95    pub fn with_password(mut self, password: impl Into<String>) -> Self {
96        self.password = Some(password.into());
97        self
98    }
99
100    /// Set the port.
101    pub fn with_port(mut self, port: u16) -> Self {
102        self.port = Some(port);
103        self
104    }
105
106    /// Add a URI parameter.
107    pub fn with_param(mut self, name: impl Into<String>, value: Option<String>) -> Self {
108        self.params
109            .push((name.into(), value));
110        self
111    }
112
113    /// Add a header.
114    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
115        self.headers
116            .push((name.into(), value.into()));
117        self
118    }
119
120    /// The URI scheme (`sip` or `sips`).
121    pub fn scheme(&self) -> Scheme {
122        self.scheme
123    }
124
125    /// The user part (without user-params or password).
126    pub fn user(&self) -> Option<&str> {
127        self.user
128            .as_deref()
129    }
130
131    /// Parameters within the userinfo (before `@`), separated by `;` in the user part.
132    ///
133    /// Common in tel-style SIP URIs, e.g., `sip:+15551234567;cpc=emergency@host`.
134    pub fn user_params(&self) -> &[(String, Option<String>)] {
135        &self.user_params
136    }
137
138    /// The password component (deprecated by RFC 3261 but still parseable).
139    pub fn password(&self) -> Option<&str> {
140        self.password
141            .as_deref()
142    }
143
144    /// The host component.
145    pub fn host(&self) -> &Host {
146        &self.host
147    }
148
149    /// The explicit port, if specified.
150    pub fn port(&self) -> Option<u16> {
151        self.port
152    }
153
154    /// URI parameters (after host, separated by `;`).
155    pub fn params(&self) -> &[(String, Option<String>)] {
156        &self.params
157    }
158
159    /// Look up a URI parameter by name (case-insensitive).
160    pub fn param(&self, name: &str) -> Option<&Option<String>> {
161        params::find_param(&self.params, name)
162    }
163
164    /// URI headers (after `?`).
165    pub fn headers(&self) -> &[(String, String)] {
166        &self.headers
167    }
168
169    /// Look up a header by name (case-insensitive).
170    pub fn header(&self, name: &str) -> Option<&str> {
171        self.headers
172            .iter()
173            .find(|(n, _)| n.eq_ignore_ascii_case(name))
174            .map(|(_, v)| v.as_str())
175    }
176
177    /// The fragment component (after `#`), if present.
178    ///
179    /// RFC 3261 does not define fragments for SIP URIs, but sofia-sip
180    /// and real-world implementations accept them permissively.
181    pub fn fragment(&self) -> Option<&str> {
182        self.fragment
183            .as_deref()
184    }
185
186    /// Set the fragment component.
187    pub fn with_fragment(mut self, fragment: impl Into<String>) -> Self {
188        self.fragment = Some(fragment.into());
189        self
190    }
191
192    /// Convenience: `user@host:port` or `host:port` string.
193    pub fn user_host(&self) -> String {
194        let mut s = String::new();
195        if let Some(ref u) = self.user {
196            s.push_str(u);
197            s.push('@');
198        }
199        s.push_str(
200            &self
201                .host
202                .to_string(),
203        );
204        if let Some(p) = self.port {
205            s.push(':');
206            s.push_str(&p.to_string());
207        }
208        s
209    }
210}
211
212impl FromStr for SipUri {
213    type Err = ParseSipUriError;
214
215    fn from_str(input: &str) -> Result<Self, Self::Err> {
216        let err = |msg: &str| ParseSipUriError(msg.to_string());
217
218        // 1. Scheme detection
219        let colon_pos = input
220            .find(':')
221            .ok_or_else(|| err("missing scheme"))?;
222        let scheme_str = &input[..colon_pos];
223        let scheme = if scheme_str.eq_ignore_ascii_case("sip") {
224            Scheme::Sip
225        } else if scheme_str.eq_ignore_ascii_case("sips") {
226            Scheme::Sips
227        } else {
228            return Err(err(&format!("unknown scheme '{scheme_str}'")));
229        };
230
231        let rest = &input[colon_pos + 1..];
232
233        // 2. Split userinfo from hostport+params+headers
234        // SIP user-part allows ;/?/ unescaped, so we use the sofia-sip approach:
235        // find @ by scanning past those chars
236        let (userinfo, hostport_rest) = split_userinfo_host(rest)?;
237
238        // 3. Parse userinfo if present
239        let (user, user_params, password) = if let Some(uinfo) = userinfo {
240            parse_userinfo(uinfo)?
241        } else {
242            (None, Vec::new(), None)
243        };
244
245        // 4. Parse host, port, params, headers, fragment from the rest
246        let (host, port, uri_params, headers, fragment) =
247            parse_hostport_params_headers(hostport_rest)?;
248
249        Ok(SipUri {
250            scheme,
251            user,
252            user_params,
253            password,
254            host,
255            port,
256            params: uri_params,
257            headers,
258            fragment,
259        })
260    }
261}
262
263/// Split a SIP URI (after scheme:) into optional userinfo and the rest (host onwards).
264///
265/// Uses the sofia-sip algorithm: scan for `@` looking past `/;?#` which are
266/// allowed unescaped in the SIP user part.
267fn split_userinfo_host(s: &str) -> Result<(Option<&str>, &str), ParseSipUriError> {
268    let err = |msg: &str| ParseSipUriError(msg.to_string());
269
270    // Find the `@` delimiter. In SIP, the user part can contain ;/?/ and even #
271    // (non-conformant phones), so we can't just scan for the first special char.
272    // Strategy: find the last `@` before any unescaped `?` that starts headers
273    // (headers can contain `@` too, e.g., From=foo@bar).
274    //
275    // Actually, per the ABNF, `@` is not in user-unreserved, so any literal `@`
276    // in the pre-headers portion is THE delimiter. We find the rightmost one
277    // before `?` to handle edge cases.
278    if let Some(at_pos) = parse::find_userinfo_at(s) {
279        if at_pos == 0 {
280            return Err(err("empty userinfo before @"));
281        }
282        let userinfo = &s[..at_pos];
283        let rest = &s[at_pos + 1..];
284        if rest.is_empty() {
285            return Err(err("missing host after @"));
286        }
287        Ok((Some(userinfo), rest))
288    } else {
289        // No @, the whole thing is hostport+params+headers
290        Ok((None, s))
291    }
292}
293
294/// Parse the userinfo portion into (user, user_params, password).
295///
296/// Userinfo structure: `user [*(";" user-param)] [":" password]`
297///
298/// The tricky part: `;` and `:` are both allowed in the user part as
299/// user-unreserved chars. But the RFC grammar says user-params are
300/// separated by `;` and password follows `:`.
301///
302/// We split on `:` first to separate user+params from password,
303/// then split the user portion on `;` to separate user from user-params.
304fn parse_userinfo(s: &str) -> UserinfoResult {
305    let err = |msg: &str| ParseSipUriError(msg.to_string());
306
307    // Split user(+params) from password on first `:`
308    // But `:` is in user-unreserved for SIP! The RFC ABNF says:
309    //   userinfo = (user / telephone-subscriber) [":" password] "@"
310    //   user = 1*(unreserved / escaped / user-unreserved)
311    // And user-unreserved does NOT include `:` — that's the password delimiter.
312    // Looking at the ABNF more carefully: user-unreserved = "&"/"="/"+"/"$"/","/";"/"?"/"/"
313    // So `:` is NOT user-unreserved. The first `:` splits user from password.
314    let (user_and_params, password) = if let Some(colon_pos) = s.find(':') {
315        let pwd = &s[colon_pos + 1..];
316        (&s[..colon_pos], Some(parse::canonize_password(pwd)))
317    } else {
318        (s, None)
319    };
320
321    // Split user from user-params on first `;`
322    // In the userinfo, `;` separates user-params (used for tel: style params
323    // like cpc=emergency). The user part itself is before the first `;`.
324    if let Some(semi_pos) = user_and_params.find(';') {
325        let user_part = &user_and_params[..semi_pos];
326        let params_str = &user_and_params[semi_pos + 1..];
327
328        if user_part.is_empty() {
329            return Err(err("empty user before ';'"));
330        }
331
332        let user = parse::canonize_user(user_part);
333        let user_params =
334            params::parse_user_params(params_str).map_err(|e| err(&format!("user param: {e}")))?;
335
336        Ok((Some(user), user_params, password))
337    } else if user_and_params.is_empty() {
338        Ok((None, Vec::new(), password))
339    } else {
340        let user = parse::canonize_user(user_and_params);
341        Ok((Some(user), Vec::new(), password))
342    }
343}
344
345/// Parse host, optional port, URI params, and headers from the portion after `@` (or after scheme: if no userinfo).
346fn parse_hostport_params_headers(s: &str) -> HostportResult {
347    let err = |msg: &str| ParseSipUriError(msg.to_string());
348
349    // Parse host
350    let (host, consumed) = Host::parse_from_uri(s).map_err(|e| err(&e))?;
351
352    let rest = &s[consumed..];
353
354    // Parse optional port
355    let (port, rest) = if let Some(rest) = rest.strip_prefix(':') {
356        // Port: digits until `;`, `?`, `#`, `>`, or end
357        let end = rest
358            .find([';', '?', '#', '>'])
359            .unwrap_or(rest.len());
360        let port_str = &rest[..end];
361
362        if port_str.is_empty() {
363            // Empty port is valid per sofia-sip (e.g., "sip:host:")
364            (None, &rest[end..])
365        } else {
366            let port: u16 = port_str
367                .parse()
368                .map_err(|_| err(&format!("invalid port '{port_str}'")))?;
369            (Some(port), &rest[end..])
370        }
371    } else {
372        (None, rest)
373    };
374
375    // Strip fragment (#...) from the end before parsing params/headers
376    let (rest, fragment) = if let Some(hash_pos) = rest.find('#') {
377        let frag = &rest[hash_pos + 1..];
378        let frag = if frag.is_empty() {
379            None
380        } else {
381            Some(frag.to_string())
382        };
383        (&rest[..hash_pos], frag)
384    } else {
385        (rest, None)
386    };
387
388    // Parse URI params (after `;`) and headers (after `?`)
389    let (params_str, headers_str) = if let Some(rest) = rest.strip_prefix(';') {
390        // Split params from headers on `?`
391        if let Some(q_pos) = rest.find('?') {
392            (&rest[..q_pos], Some(&rest[q_pos + 1..]))
393        } else {
394            (rest, None)
395        }
396    } else if let Some(rest) = rest.strip_prefix('?') {
397        ("", Some(rest))
398    } else if rest.is_empty() {
399        ("", None)
400    } else {
401        return Err(err(&format!(
402            "unexpected character after host/port: '{rest}'"
403        )));
404    };
405
406    let uri_params =
407        params::parse_params(params_str).map_err(|e| err(&format!("URI param: {e}")))?;
408
409    let headers = if let Some(h) = headers_str {
410        params::parse_headers(h).map_err(|e| err(&format!("header: {e}")))?
411    } else {
412        Vec::new()
413    };
414
415    Ok((host, port, uri_params, headers, fragment))
416}
417
418impl fmt::Display for SipUri {
419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420        write!(f, "{}:", self.scheme)?;
421
422        // Userinfo
423        if let Some(ref user) = self.user {
424            write!(f, "{user}")?;
425
426            // User-params
427            params::format_params(&self.user_params, f)?;
428
429            // Password
430            if let Some(ref pwd) = self.password {
431                write!(f, ":{pwd}")?;
432            }
433
434            write!(f, "@")?;
435        }
436
437        // Host
438        self.host
439            .fmt_uri(f)?;
440
441        // Port
442        if let Some(port) = self.port {
443            write!(f, ":{port}")?;
444        }
445
446        // URI parameters
447        params::format_params(&self.params, f)?;
448
449        // Headers
450        params::format_headers(&self.headers, f)?;
451
452        // Fragment
453        if let Some(ref frag) = self.fragment {
454            write!(f, "#{frag}")?;
455        }
456
457        Ok(())
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464    use std::net::Ipv4Addr;
465
466    #[test]
467    fn parse_simple() {
468        let uri: SipUri = "sip:joe@example.com"
469            .parse()
470            .unwrap();
471        assert_eq!(uri.scheme(), Scheme::Sip);
472        assert_eq!(uri.user(), Some("joe"));
473        assert_eq!(uri.host(), &Host::Hostname("example.com".into()));
474        assert_eq!(uri.port(), None);
475    }
476
477    #[test]
478    fn parse_minimal_user_host() {
479        let uri: SipUri = "sip:u@h"
480            .parse()
481            .unwrap();
482        assert_eq!(uri.user(), Some("u"));
483        assert_eq!(uri.host(), &Host::Hostname("h".into()));
484    }
485
486    #[test]
487    fn parse_host_only() {
488        let uri: SipUri = "sip:test.host"
489            .parse()
490            .unwrap();
491        assert_eq!(uri.user(), None);
492        assert_eq!(uri.host(), &Host::Hostname("test.host".into()));
493    }
494
495    #[test]
496    fn parse_ipv4_host() {
497        let uri: SipUri = "sip:172.21.55.55"
498            .parse()
499            .unwrap();
500        assert_eq!(uri.host(), &Host::IPv4(Ipv4Addr::new(172, 21, 55, 55)));
501    }
502
503    #[test]
504    fn parse_ipv4_with_port() {
505        let uri: SipUri = "sip:172.21.55.55:5060"
506            .parse()
507            .unwrap();
508        assert_eq!(uri.host(), &Host::IPv4(Ipv4Addr::new(172, 21, 55, 55)));
509        assert_eq!(uri.port(), Some(5060));
510    }
511
512    #[test]
513    fn parse_full_sips() {
514        let uri: SipUri = "sips:user:pass@host:32;param=1?From=foo@bar&To=bar@baz"
515            .parse()
516            .unwrap();
517        assert_eq!(uri.scheme(), Scheme::Sips);
518        assert_eq!(uri.user(), Some("user"));
519        assert_eq!(uri.password(), Some("pass"));
520        assert_eq!(uri.host(), &Host::Hostname("host".into()));
521        assert_eq!(uri.port(), Some(32));
522        assert_eq!(uri.params(), &[("param".into(), Some("1".into()))]);
523        assert_eq!(uri.header("From"), Some("foo@bar"));
524        assert_eq!(uri.header("To"), Some("bar@baz"));
525    }
526
527    #[test]
528    fn parse_case_insensitive_scheme() {
529        let uri: SipUri = "SIP:test@127.0.0.1:55"
530            .parse()
531            .unwrap();
532        assert_eq!(uri.scheme(), Scheme::Sip);
533        assert_eq!(uri.user(), Some("test"));
534        assert_eq!(uri.port(), Some(55));
535    }
536
537    #[test]
538    fn parse_empty_port() {
539        let uri: SipUri = "SIP:test@127.0.0.1:"
540            .parse()
541            .unwrap();
542        assert_eq!(uri.scheme(), Scheme::Sip);
543        assert_eq!(uri.port(), None);
544    }
545
546    #[test]
547    fn parse_percent_encoded_user() {
548        let uri: SipUri = "sip:%22foo%22@172.21.55.55:5060"
549            .parse()
550            .unwrap();
551        // %22 is double-quote, not unreserved, stays encoded
552        assert_eq!(uri.user(), Some("%22foo%22"));
553    }
554
555    #[test]
556    fn parse_user_with_slash_semicolon() {
557        let uri: SipUri = "sip:user/path;tel-param:pass@host:32;param=1%3d%3d1"
558            .parse()
559            .unwrap();
560        assert_eq!(uri.user(), Some("user/path"));
561        assert_eq!(uri.user_params(), &[("tel-param".into(), None)]);
562        assert_eq!(uri.password(), Some("pass"));
563        // %3d normalized to uppercase %3D
564        assert_eq!(uri.params(), &[("param".into(), Some("1%3D%3D1".into()))]);
565    }
566
567    #[test]
568    fn parse_reserved_chars_in_user_ipv6() {
569        let uri: SipUri = "sip:&=+$,;?/:&=+$,@[::1]:56001;param=+$,/:@&"
570            .parse()
571            .unwrap();
572        assert_eq!(uri.user(), Some("&=+$,"));
573        // `;` splits user from user-params, `?/` is a param name (no `=`),
574        // and `:` splits the remaining `&=+$,` as the password
575        assert_eq!(uri.user_params(), &[("?/".into(), None)]);
576        assert_eq!(uri.password(), Some("&=+$,"));
577        assert_eq!(
578            uri.host(),
579            &Host::IPv6(
580                "::1"
581                    .parse()
582                    .unwrap()
583            )
584        );
585        assert_eq!(uri.port(), Some(56001));
586    }
587
588    #[test]
589    fn parse_hash_in_user() {
590        // Sofia-sip compatibility: phones put unescaped # in user
591        let uri: SipUri = "SIP:#**00**#;foo=/bar@127.0.0.1"
592            .parse()
593            .unwrap();
594        assert_eq!(uri.user(), Some("#**00**#"));
595        assert_eq!(uri.user_params(), &[("foo".into(), Some("/bar".into()))]);
596    }
597
598    #[test]
599    fn parse_transport_params() {
600        let uri: SipUri = "sip:u:p@host:5060;maddr=127.0.0.1;transport=tcp"
601            .parse()
602            .unwrap();
603        assert_eq!(uri.param("transport"), Some(&Some("tcp".into())));
604        assert_eq!(uri.param("maddr"), Some(&Some("127.0.0.1".into())));
605    }
606
607    #[test]
608    fn parse_params_without_value() {
609        let uri: SipUri = "sip:u:p@host:5060;user=phone;ttl=1;isfocus"
610            .parse()
611            .unwrap();
612        assert_eq!(uri.param("user"), Some(&Some("phone".into())));
613        assert_eq!(uri.param("isfocus"), Some(&None));
614    }
615
616    #[test]
617    fn invalid_double_colon_port() {
618        assert!("sip:test@127.0.0.1::55"
619            .parse::<SipUri>()
620            .is_err());
621    }
622
623    #[test]
624    fn invalid_trailing_colon_port() {
625        assert!("sip:test@127.0.0.1:55:"
626            .parse::<SipUri>()
627            .is_err());
628    }
629
630    #[test]
631    fn invalid_non_numeric_port() {
632        assert!("sip:test@127.0.0.1:sip"
633            .parse::<SipUri>()
634            .is_err());
635    }
636
637    #[test]
638    fn display_roundtrip_simple() {
639        let input = "sip:joe@example.com";
640        let uri: SipUri = input
641            .parse()
642            .unwrap();
643        assert_eq!(uri.to_string(), input);
644    }
645
646    #[test]
647    fn display_roundtrip_full() {
648        let uri: SipUri = "sips:user:pass@host:32;param=1?From=foo@bar&To=bar@baz"
649            .parse()
650            .unwrap();
651        assert_eq!(
652            uri.to_string(),
653            "sips:user:pass@host:32;param=1?From=foo@bar&To=bar@baz"
654        );
655    }
656
657    #[test]
658    fn builder() {
659        let uri = SipUri::new(Host::Hostname("example.com".into()))
660            .with_user("alice")
661            .with_param("transport", Some("tcp".into()));
662        assert_eq!(uri.to_string(), "sip:alice@example.com;transport=tcp");
663    }
664
665    #[test]
666    fn user_host_convenience() {
667        let uri: SipUri = "sip:alice@example.com:5060"
668            .parse()
669            .unwrap();
670        assert_eq!(uri.user_host(), "alice@example.com:5060");
671    }
672
673    #[test]
674    fn no_user_with_host_params() {
675        let uri: SipUri = "sip:172.21.55.55:5060;transport=udp"
676            .parse()
677            .unwrap();
678        assert_eq!(uri.user(), None);
679        assert_eq!(uri.port(), Some(5060));
680        assert_eq!(uri.param("transport"), Some(&Some("udp".into())));
681    }
682}