Skip to main content

sipx_sip/
build.rs

1//! Building messages.
2//!
3//! The threat this module exists for is header injection: a caller puts a user-supplied
4//! string into a display name or a `Call-ID`, the string contains CRLF, and one header becomes
5//! three — or a body becomes a second request. Every SIP stack has a story about this.
6//!
7//! The usual defence is a `validate()` function the caller is supposed to remember. sipx does
8//! not have one, because "supposed to remember" is not a security property. Instead there is
9//! **no way to build a message from unvalidated bytes**: every constructor here is fallible
10//! and checks its input, and the only unchecked path into a [`Header`] is the parser's, which
11//! is crate-private and operates on bytes that were already framed.
12//!
13//! The test that keeps this true is table-driven over every field a caller can populate, so a
14//! newly added field with no guard fails it.
15
16use bytes::Bytes;
17
18use crate::error::BuildError;
19use crate::headers::grammar::is_token_char;
20use crate::message::{Header, Headers, Method, Request, Response, StatusCode};
21use crate::name::HeaderName;
22use crate::uri::Uri;
23
24/// Reject anything that could end a line or terminate a C string.
25///
26/// CR and LF are the injection vector. NUL is here because it is the classic way to smuggle
27/// a value past a length-agnostic consumer further down the chain, and nothing in SIP needs
28/// one unescaped.
29pub(crate) fn check_value(value: &[u8], field: &'static str) -> Result<(), BuildError> {
30    if let Some(pos) = value
31        .iter()
32        .position(|&b| matches!(b, b'\r' | b'\n' | b'\0'))
33    {
34        return Err(BuildError::IllegalCharacter {
35            field,
36            offset: pos,
37            byte: value.get(pos).copied().unwrap_or(0),
38        });
39    }
40    Ok(())
41}
42
43/// Reject anything that is not a single `token`.
44pub(crate) fn check_token(value: &[u8], field: &'static str) -> Result<(), BuildError> {
45    check_value(value, field)?;
46    if value.is_empty() || !value.iter().all(|&b| is_token_char(b)) {
47        return Err(BuildError::NotAToken { field });
48    }
49    Ok(())
50}
51
52impl Header {
53    /// Build a header, rejecting a value that could inject a line break.
54    ///
55    /// This is the only public way to make a header, and it is fallible on purpose.
56    pub fn build(name: HeaderName, value: impl Into<Bytes>) -> Result<Self, BuildError> {
57        let value = value.into();
58        check_value(&value, "header value")?;
59        if let HeaderName::Other(raw) = &name {
60            check_token(raw, "header name")?;
61        }
62        Ok(Self::new_unchecked(name, value))
63    }
64}
65
66/// Builds a request.
67///
68/// ```
69/// # use sipx_sip::{build::RequestBuilder, Method, Uri, Host, HostName, HeaderName};
70/// let uri = Uri::sip(Host::Name(HostName::new("example.com")?));
71/// let request = RequestBuilder::new(Method::Options, uri)
72///     .header(HeaderName::CallId, "abc123@example.com")?
73///     .max_forwards(70)
74///     .build();
75/// # Ok::<(), sipx_sip::error::BuildError>(())
76/// ```
77#[derive(Debug)]
78pub struct RequestBuilder {
79    method: Method,
80    uri: Uri,
81    headers: Headers,
82    body: Bytes,
83}
84
85impl RequestBuilder {
86    /// Start a request.
87    #[must_use]
88    pub fn new(method: Method, uri: Uri) -> Self {
89        Self {
90            method,
91            uri,
92            headers: Headers::new(),
93            body: Bytes::new(),
94        }
95    }
96
97    /// Append a header, rejecting a value that could inject a line break.
98    pub fn header(mut self, name: HeaderName, value: impl Into<Bytes>) -> Result<Self, BuildError> {
99        self.headers.push(Header::build(name, value)?);
100        Ok(self)
101    }
102
103    /// Replace every header of this name with one carrying this value.
104    ///
105    /// Distinct from [`Self::header`] because appending is right for `Via` and `Route`, where
106    /// repetition is meaningful, and wrong for `To` or `CSeq`, where a second copy makes the
107    /// message invalid.
108    pub fn set_header(
109        mut self,
110        name: &HeaderName,
111        value: impl Into<Bytes>,
112    ) -> Result<Self, BuildError> {
113        let header = Header::build(name.clone(), value)?;
114        self.headers.remove_all(name);
115        self.headers.push(header);
116        Ok(self)
117    }
118
119    /// Append a `Max-Forwards` header. Cannot fail: a `u8` has no CRLF in it.
120    #[must_use]
121    pub fn max_forwards(mut self, hops: u8) -> Self {
122        self.headers.push(Header::new_unchecked(
123            HeaderName::MaxForwards,
124            Bytes::from(hops.to_string()),
125        ));
126        self
127    }
128
129    /// Append a `CSeq` header.
130    pub fn cseq(mut self, sequence: u32, method: &Method) -> Result<Self, BuildError> {
131        check_token(method.as_bytes(), "CSeq method")?;
132        let mut value = sequence.to_string().into_bytes();
133        value.push(b' ');
134        value.extend_from_slice(method.as_bytes());
135        self.headers
136            .push(Header::new_unchecked(HeaderName::CSeq, Bytes::from(value)));
137        Ok(self)
138    }
139
140    /// Set the body, and the `Content-Length` that goes with it.
141    ///
142    /// The two are set together because a body without a matching length is a framing bug
143    /// waiting to happen, and there is no reason to let a caller create one.
144    #[must_use]
145    pub fn body(mut self, body: impl Into<Bytes>) -> Self {
146        let body = body.into();
147        self.headers.remove_all(&HeaderName::ContentLength);
148        self.headers.push(Header::new_unchecked(
149            HeaderName::ContentLength,
150            Bytes::from(body.len().to_string()),
151        ));
152        self.body = body;
153        self
154    }
155
156    /// Finish.
157    ///
158    /// If no body was set, a `Content-Length: 0` is added, because a stream transport cannot
159    /// frame a message without one.
160    #[must_use]
161    pub fn build(mut self) -> Request {
162        if self.headers.get(&HeaderName::ContentLength).is_none() {
163            self.headers.push(Header::new_unchecked(
164                HeaderName::ContentLength,
165                Bytes::from_static(b"0"),
166            ));
167        }
168        let mut request = Request::new(self.method, self.uri);
169        request.headers = self.headers;
170        request.set_body(self.body);
171        request
172    }
173}
174
175/// Builds a response.
176#[derive(Debug)]
177pub struct ResponseBuilder {
178    status: StatusCode,
179    reason: Bytes,
180    headers: Headers,
181    body: Bytes,
182}
183
184impl ResponseBuilder {
185    /// Start a response.
186    ///
187    /// The reason phrase is checked: it is the one field on a start line that carries free
188    /// text, which makes it the obvious place to try to inject a line break.
189    pub fn new(status: StatusCode, reason: impl Into<Bytes>) -> Result<Self, BuildError> {
190        let reason = reason.into();
191        check_value(&reason, "reason phrase")?;
192        Ok(Self {
193            status,
194            reason,
195            headers: Headers::new(),
196            body: Bytes::new(),
197        })
198    }
199
200    /// Start a response to a request, copying the headers RFC 3261 §8.2.6.2 requires.
201    ///
202    /// `Via` is copied **in order and in full**: the response finds its way back by walking
203    /// that list, and reordering or deduplicating it strands the response. `From`, `To`,
204    /// `Call-ID` and `CSeq` are copied verbatim, which also means a request whose `To` was
205    /// unparseable still gets a well-formed response — the point of copying rather than
206    /// re-deriving.
207    pub fn to_request(
208        request: &Request,
209        status: StatusCode,
210        reason: impl Into<Bytes>,
211    ) -> Result<Self, BuildError> {
212        let mut builder = Self::new(status, reason)?;
213        for (name, label) in [
214            (HeaderName::Via, "Via"),
215            (HeaderName::From, "From"),
216            (HeaderName::To, "To"),
217            (HeaderName::CallId, "Call-ID"),
218            (HeaderName::CSeq, "CSeq"),
219        ] {
220            if request.headers.get(&name).is_none() {
221                return Err(BuildError::MissingRequiredResponseHeader { header: label });
222            }
223            for header in request.headers.get_all(&name) {
224                builder.headers.push(header.clone());
225            }
226        }
227        // RFC 7044 §9.4: a UAS returns its cache in every response except 100 when the
228        // request carried a cache or advertised `histinfo`. Centralising this beside the
229        // mandatory response-header copy keeps OPTIONS, errors and call answers consistent.
230        if let Some(history) = crate::headers::history::for_response(request, status) {
231            builder
232                .headers
233                .push(Header::new_unchecked(HeaderName::HistoryInfo, history));
234        }
235        Ok(builder)
236    }
237
238    /// Append a header, rejecting a value that could inject a line break.
239    pub fn header(mut self, name: HeaderName, value: impl Into<Bytes>) -> Result<Self, BuildError> {
240        self.headers.push(Header::build(name, value)?);
241        Ok(self)
242    }
243
244    /// Replace every header of this name with one carrying this value.
245    pub fn set_header(
246        mut self,
247        name: &HeaderName,
248        value: impl Into<Bytes>,
249    ) -> Result<Self, BuildError> {
250        let header = Header::build(name.clone(), value)?;
251        self.headers.remove_all(name);
252        self.headers.push(header);
253        Ok(self)
254    }
255
256    /// Set the body and its `Content-Length`.
257    #[must_use]
258    pub fn body(mut self, body: impl Into<Bytes>) -> Self {
259        let body = body.into();
260        self.headers.remove_all(&HeaderName::ContentLength);
261        self.headers.push(Header::new_unchecked(
262            HeaderName::ContentLength,
263            Bytes::from(body.len().to_string()),
264        ));
265        self.body = body;
266        self
267    }
268
269    /// Finish.
270    #[must_use]
271    pub fn build(mut self) -> Response {
272        if self.headers.get(&HeaderName::ContentLength).is_none() {
273            self.headers.push(Header::new_unchecked(
274                HeaderName::ContentLength,
275                Bytes::from_static(b"0"),
276            ));
277        }
278        let mut response = Response::new(self.status, self.reason);
279        response.headers = self.headers;
280        response.set_body(self.body);
281        response
282    }
283}
284
285#[cfg(test)]
286#[allow(
287    clippy::unwrap_used,
288    clippy::expect_used,
289    clippy::panic,
290    clippy::indexing_slicing
291)]
292mod tests {
293    use super::*;
294    use crate::headers::CSeq;
295    use crate::uri::{Host, HostName};
296
297    fn uri() -> Uri {
298        Uri::sip(Host::Name(
299            HostName::new(Bytes::from_static(b"example.com")).expect("a valid host"),
300        ))
301    }
302
303    fn answerable_invite() -> RequestBuilder {
304        RequestBuilder::new(Method::Invite, uri())
305            .header(HeaderName::Via, "SIP/2.0/UDP host;branch=z9hG4bK1")
306            .expect("valid Via")
307            .header(HeaderName::From, "<sip:a@example.com>;tag=1")
308            .expect("valid From")
309            .header(HeaderName::To, "<sip:b@example.com>")
310            .expect("valid To")
311            .header(HeaderName::CallId, "call@example.com")
312            .expect("valid Call-ID")
313            .cseq(1, &Method::Invite)
314            .expect("valid CSeq")
315    }
316
317    /// Every field a caller can populate, in one table.
318    ///
319    /// The table is the point. Guarding today's fields is easy; the failure mode is a field
320    /// added next year with no guard, and this test fails the moment one appears — provided
321    /// it is added here, which the module documentation asks for.
322    #[test]
323    fn crlf_injection_rejected_in_every_user_supplied_field() {
324        // Each payload ends a header line early and starts a forged one.
325        let payloads: &[&[u8]] = &[
326            b"value\r\nInjected: yes",
327            b"value\rInjected: yes",
328            b"value\nInjected: yes",
329            b"value\0truncated",
330            b"\r\n",
331            b"\n\n",
332        ];
333
334        for payload in payloads {
335            let p = Bytes::copy_from_slice(payload);
336
337            // A header value.
338            assert!(
339                Header::build(HeaderName::Subject, p.clone()).is_err(),
340                "header value accepted {payload:?}"
341            );
342            // An unknown header's *name*.
343            assert!(
344                Header::build(HeaderName::Other(p.clone()), Bytes::from_static(b"x")).is_err(),
345                "header name accepted {payload:?}"
346            );
347            // A request header, through the builder.
348            assert!(
349                RequestBuilder::new(Method::Options, uri())
350                    .header(HeaderName::CallId, p.clone())
351                    .is_err(),
352                "request builder accepted {payload:?}"
353            );
354            // A response reason phrase — free text on the start line.
355            assert!(
356                ResponseBuilder::new(StatusCode::new(200).unwrap(), p.clone()).is_err(),
357                "reason phrase accepted {payload:?}"
358            );
359            // A response header.
360            assert!(
361                ResponseBuilder::new(StatusCode::new(200).unwrap(), "OK")
362                    .unwrap()
363                    .header(HeaderName::Server, p.clone())
364                    .is_err(),
365                "response builder accepted {payload:?}"
366            );
367            // A CSeq method.
368            assert!(
369                RequestBuilder::new(Method::Options, uri())
370                    .cseq(1, &Method::Other(p.clone()))
371                    .is_err(),
372                "CSeq method accepted {payload:?}"
373            );
374            // A hostname, which reaches the wire inside the Request-URI. This is the field
375            // that made HostName a newtype with a private interior: a CRLF here forges an
376            // entire request line, not merely a header.
377            assert!(
378                HostName::new(p.clone()).is_err(),
379                "host name accepted {payload:?}"
380            );
381        }
382    }
383
384    /// A hostname must be a hostname, not merely free of line breaks.
385    #[test]
386    fn host_names_are_validated_not_just_screened() {
387        assert!(HostName::new("example.com").is_ok());
388        assert!(HostName::new("host-5.sub.example.com").is_ok());
389        for bad in [
390            "",
391            "exa mple.com",
392            "host@evil.com",
393            "host;lr",
394            "<host>",
395            "host/path",
396        ] {
397            assert!(HostName::new(bad).is_err(), "{bad:?} should be rejected");
398        }
399    }
400
401    #[test]
402    fn a_built_request_frames_correctly() {
403        let request = RequestBuilder::new(Method::Options, uri())
404            .header(HeaderName::CallId, "abc@example.com")
405            .unwrap()
406            .cseq(1, &Method::Options)
407            .unwrap()
408            .max_forwards(70)
409            .build();
410
411        let mut out = Vec::new();
412        request.write_to(&mut out);
413        let text = String::from_utf8_lossy(&out);
414        assert!(text.starts_with("OPTIONS sip:example.com SIP/2.0\r\n"));
415        assert!(text.contains("CSeq: 1 OPTIONS\r\n"));
416        // Content-Length is added even with no body, because a stream cannot frame without it.
417        assert!(text.contains("Content-Length: 0\r\n"));
418        assert!(text.ends_with("\r\n\r\n"));
419    }
420
421    #[test]
422    fn setting_a_body_sets_the_matching_content_length() {
423        let request = RequestBuilder::new(Method::Options, uri())
424            .body(Bytes::from_static(b"hello"))
425            .build();
426        assert_eq!(request.body().len(), 5);
427        assert_eq!(
428            request
429                .headers
430                .value(&HeaderName::ContentLength)
431                .as_deref()
432                .map(<[u8]>::to_vec),
433            Some(b"5".to_vec())
434        );
435
436        // Replacing the body replaces the length rather than adding a second one, which would
437        // be an unframeable message.
438        let request = RequestBuilder::new(Method::Options, uri())
439            .body(Bytes::from_static(b"hello"))
440            .body(Bytes::from_static(b"hi"))
441            .build();
442        assert_eq!(request.headers.count(&HeaderName::ContentLength), 1);
443        assert_eq!(
444            request
445                .headers
446                .value(&HeaderName::ContentLength)
447                .as_deref()
448                .map(<[u8]>::to_vec),
449            Some(b"2".to_vec())
450        );
451    }
452
453    #[test]
454    fn a_response_copies_the_via_stack_in_order() {
455        let request = RequestBuilder::new(Method::Invite, uri())
456            .header(HeaderName::Via, "SIP/2.0/UDP first;branch=z9hG4bK1")
457            .unwrap()
458            .header(HeaderName::Via, "SIP/2.0/UDP second;branch=z9hG4bK2")
459            .unwrap()
460            .header(HeaderName::From, "<sip:a@b>;tag=1")
461            .unwrap()
462            .header(HeaderName::To, "<sip:c@d>")
463            .unwrap()
464            .header(HeaderName::CallId, "x@y")
465            .unwrap()
466            .cseq(7, &Method::Invite)
467            .unwrap()
468            .build();
469
470        let response =
471            ResponseBuilder::to_request(&request, StatusCode::new(180).unwrap(), "Ringing")
472                .unwrap()
473                .build();
474
475        let vias: Vec<_> = response
476            .headers
477            .get_all(&HeaderName::Via)
478            .map(|h| h.value().to_vec())
479            .collect();
480        assert_eq!(
481            vias,
482            vec![
483                b"SIP/2.0/UDP first;branch=z9hG4bK1".to_vec(),
484                b"SIP/2.0/UDP second;branch=z9hG4bK2".to_vec(),
485            ],
486            "the Via stack must be copied in order; a response walks it back"
487        );
488        assert_eq!(
489            response.headers.typed::<CSeq>().and_then(Result::ok),
490            Some(CSeq {
491                sequence: 7,
492                method: Method::Invite
493            })
494        );
495    }
496
497    #[test]
498    fn history_is_returned_in_responses_other_than_100() {
499        let request = answerable_invite()
500            .header(HeaderName::Supported, "histinfo")
501            .unwrap()
502            .build();
503        let trying = ResponseBuilder::to_request(&request, StatusCode::new(100).unwrap(), "Trying")
504            .unwrap()
505            .build();
506        assert!(trying.headers.get(&HeaderName::HistoryInfo).is_none());
507
508        let ringing =
509            ResponseBuilder::to_request(&request, StatusCode::new(180).unwrap(), "Ringing")
510                .unwrap()
511                .build();
512        assert_eq!(
513            ringing.headers.value(&HeaderName::HistoryInfo).as_deref(),
514            Some(&b"<sip:example.com>;index=1"[..])
515        );
516    }
517
518    #[test]
519    fn repeated_history_rows_are_one_ordered_cache() {
520        let request = answerable_invite()
521            .header(HeaderName::HistoryInfo, "<sip:first@example.com>;index=1")
522            .unwrap()
523            .header(
524                HeaderName::HistoryInfo,
525                "<sip:second@example.com>;index=1.1;mp=1",
526            )
527            .unwrap()
528            .build();
529        let response =
530            ResponseBuilder::to_request(&request, StatusCode::new(180).unwrap(), "Ringing")
531                .unwrap()
532                .build();
533        assert_eq!(
534            response.headers.value(&HeaderName::HistoryInfo).as_deref(),
535            Some(&b"<sip:first@example.com>;index=1, <sip:second@example.com>;index=1.1;mp=1"[..])
536        );
537    }
538
539    #[test]
540    fn history_privacy_anonymizes_a_response_cache() {
541        let request = answerable_invite()
542            .header(
543                HeaderName::HistoryInfo,
544                "<sip:alice@example.com?Reason=SIP%3Bcause%3D302>;index=1",
545            )
546            .unwrap()
547            .header(HeaderName::Privacy, "history")
548            .unwrap()
549            .build();
550        let response =
551            ResponseBuilder::to_request(&request, StatusCode::new(486).unwrap(), "Busy Here")
552                .unwrap()
553                .build();
554        assert_eq!(
555            response.headers.value(&HeaderName::HistoryInfo).as_deref(),
556            Some(&b"<sip:anonymous@anonymous.invalid>;index=1"[..])
557        );
558    }
559
560    /// A request whose `To` cannot be parsed still deserves a well-formed 400. Copying the
561    /// header bytes rather than re-deriving them is what makes that possible.
562    #[test]
563    fn a_response_can_be_built_for_a_request_with_an_unparseable_header() {
564        use crate::{Limits, parse_datagram};
565        let text = "OPTIONS sip:a@b.com SIP/2.0\r\n\
566             Via: SIP/2.0/UDP h;branch=z9hG4bKx\r\n\
567             To: \"unterminated <sip:a@b.com>\r\n\
568             From: <sip:c@d>;tag=1\r\n\
569             Call-ID: x@y\r\n\
570             CSeq: 1 OPTIONS\r\n\
571             Content-Length: 0\r\n\r\n";
572        let msg = parse_datagram(Bytes::from(text), &Limits::datagram()).expect("frames");
573        let request = msg.as_request().expect("a request");
574        assert!(
575            request
576                .headers
577                .typed::<crate::headers::To>()
578                .is_some_and(|r| r.is_err())
579        );
580
581        let response =
582            ResponseBuilder::to_request(request, StatusCode::new(400).unwrap(), "Bad Request")
583                .expect("must still build")
584                .build();
585        let mut out = Vec::new();
586        response.write_to(&mut out);
587        assert!(String::from_utf8_lossy(&out).starts_with("SIP/2.0 400 Bad Request\r\n"));
588    }
589}