Skip to content

Commit 5d77784

Browse files
WaelJamiWael Jami
andauthored
fix: preserve binary message bodies when serializing to the wire (#126)
SIP message bodies are opaque octets (RFC 3261 §7.4), but messages were serialized via Display/to_string(), which renders the body with String::from_utf8_lossy. Any non-UTF-8 body (e.g. an application/ISUP part or a binary eCall MSD) was corrupted: each invalid byte became U+FFFD, which changes the payload and breaks Content-Length. The receive path already preserves binary bodies (the parser slices by Content-Length), so send and receive were asymmetric. Add to_bytes() to Request/Response/SipMessage (start line + headers as ASCII, body appended verbatim) and route the UDP and TCP/TLS transports through it. From<_> for Vec<u8> now delegates to to_bytes(). Display is left lossy for human-readable logging. WebSocket is unchanged: SIP-over-WS (RFC 7118) uses UTF-8 text frames. Adds regression tests for Request/Response round-trips and the stream codec. Co-authored-by: Wael Jami <wael.jami@vedecom.fr>
1 parent fa70e8f commit 5d77784

3 files changed

Lines changed: 123 additions & 9 deletions

File tree

src/sip/message.rs

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,29 @@ impl std::fmt::Display for Request {
360360
}
361361
}
362362

363+
impl Request {
364+
/// Serialize the request to its on-the-wire byte representation.
365+
///
366+
/// The request line and headers are ASCII; the body is appended verbatim.
367+
/// A SIP message body is an opaque octet sequence (RFC 3261 §7.4) and may
368+
/// contain arbitrary bytes — e.g. a binary ISUP part or an eCall MSD — so
369+
/// it must not be forced through UTF-8.
370+
///
371+
/// Prefer this over [`Display`](std::fmt::Display) / `to_string()` when
372+
/// putting a message on the wire: `Display` renders the body with
373+
/// [`String::from_utf8_lossy`], which is fine for logging but corrupts
374+
/// non-UTF-8 bytes.
375+
pub fn to_bytes(&self) -> Vec<u8> {
376+
let head = format!(
377+
"{} {} {}\r\n{}\r\n",
378+
self.method, self.uri, self.version, self.headers
379+
);
380+
let mut buf = head.into_bytes();
381+
buf.extend_from_slice(&self.body);
382+
buf
383+
}
384+
}
385+
363386
impl std::convert::TryFrom<Vec<u8>> for Request {
364387
type Error = Error;
365388
fn try_from(bytes: Vec<u8>) -> Result<Self, Error> {
@@ -406,7 +429,7 @@ impl std::convert::From<Request> for String {
406429

407430
impl std::convert::From<Request> for Vec<u8> {
408431
fn from(r: Request) -> Vec<u8> {
409-
r.to_string().into_bytes()
432+
r.to_bytes()
410433
}
411434
}
412435

@@ -507,6 +530,25 @@ impl std::fmt::Display for Response {
507530
}
508531
}
509532

533+
impl Response {
534+
/// Serialize the response to its on-the-wire byte representation.
535+
///
536+
/// See [`Request::to_bytes`]: the body is written verbatim because a SIP
537+
/// body is an opaque octet sequence (RFC 3261 §7.4), not necessarily UTF-8.
538+
pub fn to_bytes(&self) -> Vec<u8> {
539+
let head = format!(
540+
"{} {} {}\r\n{}\r\n",
541+
self.version,
542+
self.status_code.code(),
543+
self.status_code.text(),
544+
self.headers
545+
);
546+
let mut buf = head.into_bytes();
547+
buf.extend_from_slice(&self.body);
548+
buf
549+
}
550+
}
551+
510552
impl std::convert::TryFrom<Vec<u8>> for Response {
511553
type Error = Error;
512554
fn try_from(bytes: Vec<u8>) -> Result<Self, Error> {
@@ -553,7 +595,7 @@ impl std::convert::From<Response> for String {
553595

554596
impl std::convert::From<Response> for Vec<u8> {
555597
fn from(r: Response) -> Vec<u8> {
556-
r.to_string().into_bytes()
598+
r.to_bytes()
557599
}
558600
}
559601

@@ -605,6 +647,19 @@ impl std::fmt::Display for SipMessage {
605647
}
606648
}
607649

650+
impl SipMessage {
651+
/// Serialize the message to its on-the-wire byte representation.
652+
///
653+
/// See [`Request::to_bytes`]. Transports should use this rather than
654+
/// `to_string()` so binary bodies are preserved byte-for-byte.
655+
pub fn to_bytes(&self) -> Vec<u8> {
656+
match self {
657+
SipMessage::Request(r) => r.to_bytes(),
658+
SipMessage::Response(r) => r.to_bytes(),
659+
}
660+
}
661+
}
662+
608663
impl std::convert::TryFrom<Vec<u8>> for SipMessage {
609664
type Error = Error;
610665
fn try_from(bytes: Vec<u8>) -> Result<Self, Error> {
@@ -673,7 +728,7 @@ impl std::convert::From<SipMessage> for String {
673728

674729
impl std::convert::From<SipMessage> for Vec<u8> {
675730
fn from(m: SipMessage) -> Vec<u8> {
676-
m.to_string().into_bytes()
731+
m.to_bytes()
677732
}
678733
}
679734

@@ -767,6 +822,39 @@ mod tests {
767822
assert_eq!(resp.body, b"v=0\r\no=bob 2890844527 2890844527 IN IP4 192.0.2.4\r\ns=-\r\nc=IN IP4 192.0.2.4\r\nt=0 0\r\nm=audio 3456 RTP/AVP 0\r\na=rtpmap:0 PCMU/8000");
768823
}
769824

825+
#[test]
826+
fn request_binary_body_survives_serialization() {
827+
// A SIP message body is an opaque octet payload (RFC 3261 §7.4), not
828+
// necessarily UTF-8 text. e.g. an NG-eCall INVITE carries a binary
829+
// ASN.1 PER-encoded MSD. Serialization to the wire must preserve every
830+
// byte, including bytes that are invalid as UTF-8.
831+
let mut req: Request = invite_request().try_into().unwrap();
832+
let binary_body: Vec<u8> = vec![0x00, 0x01, 0x80, 0xFF, 0xC0, 0xC1, 0xFE, 0x02, 0x7F, 0x90];
833+
req.body = binary_body.clone();
834+
835+
let wire: Vec<u8> = req.into();
836+
837+
assert!(
838+
wire.ends_with(&binary_body),
839+
"binary body corrupted during serialization: got tail {:?}",
840+
&wire[wire.len().saturating_sub(binary_body.len() + 4)..]
841+
);
842+
}
843+
844+
#[test]
845+
fn response_binary_body_survives_serialization() {
846+
let mut resp: Response = ok_response().try_into().unwrap();
847+
let binary_body: Vec<u8> = vec![0x00, 0x80, 0xFF, 0xC0, 0xC1, 0xFE, 0x02];
848+
resp.body = binary_body.clone();
849+
850+
let wire: Vec<u8> = resp.into();
851+
852+
assert!(
853+
wire.ends_with(&binary_body),
854+
"binary body corrupted during serialization"
855+
);
856+
}
857+
770858
#[test]
771859
fn response_has_multiple_via_headers() {
772860
let resp: Response = ok_response().try_into().unwrap();

src/transport/stream.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,9 @@ impl Encoder<SipMessage> for SipCodec {
136136
type Error = crate::Error;
137137

138138
fn encode(&mut self, item: SipMessage, dst: &mut BytesMut) -> Result<()> {
139-
let data = item.to_string();
140-
dst.extend_from_slice(data.as_bytes());
139+
// Use to_bytes() (not to_string()) so binary bodies are preserved
140+
// byte-for-byte; a SIP body is opaque octets (RFC 3261 §7.4).
141+
dst.extend_from_slice(&item.to_bytes());
141142
Ok(())
142143
}
143144
}
@@ -264,7 +265,7 @@ pub async fn send_to_stream<W>(write_half: &Mutex<W>, msg: SipMessage) -> Result
264265
where
265266
W: AsyncWrite + Unpin + Send,
266267
{
267-
send_raw_to_stream(write_half, msg.to_string().as_bytes()).await
268+
send_raw_to_stream(write_half, &msg.to_bytes()).await
268269
}
269270

270271
pub async fn send_raw_to_stream<W>(write_half: &Mutex<W>, data: &[u8]) -> Result<()>
@@ -324,6 +325,29 @@ mod tests {
324325
.to_vec()
325326
}
326327

328+
// ── binary body ──────────────────────────────────────────────────────────
329+
330+
#[test]
331+
fn encode_preserves_binary_body() {
332+
use crate::sip::{Request, SipMessage};
333+
// A SIP body is opaque octets (RFC 3261 §7.4); the stream codec must
334+
// emit it byte-for-byte, including bytes that are invalid as UTF-8.
335+
let mut req: Request = invite_bytes("").as_slice().try_into().unwrap();
336+
let binary_body: Vec<u8> = vec![0x00, 0x80, 0xFF, 0xC0, 0xC1, 0xFE, 0x01, 0x7F];
337+
req.body = binary_body.clone();
338+
339+
let mut codec = make_codec();
340+
let mut dst = BytesMut::new();
341+
codec
342+
.encode(SipMessage::Request(req), &mut dst)
343+
.expect("encode");
344+
345+
assert!(
346+
dst.ends_with(&binary_body),
347+
"stream codec corrupted binary body"
348+
);
349+
}
350+
327351
// ── 基本解码 ──────────────────────────────────────────────────────────────
328352

329353
#[test]

src/transport/udp.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,15 @@ impl UdpConnection {
211211
Some(addr) => addr.get_socketaddr(),
212212
None => SipConnection::get_destination(&msg),
213213
}?;
214-
let buf = msg.to_string();
214+
// Use to_bytes() (not to_string()) so binary bodies are preserved
215+
// byte-for-byte; a SIP body is opaque octets (RFC 3261 §7.4).
216+
let buf = msg.to_bytes();
215217

216-
debug!(len=buf.len(), dest=%destination, src=%self.get_addr(), raw_message=buf, "udp send");
218+
debug!(len=buf.len(), dest=%destination, src=%self.get_addr(), raw_message=%msg, "udp send");
217219

218220
self.inner
219221
.conn
220-
.send_to(buf.as_bytes(), destination)
222+
.send_to(&buf, destination)
221223
.await
222224
.map_err(|e| {
223225
crate::Error::TransportLayerError(e.to_string(), self.get_addr().to_owned())

0 commit comments

Comments
 (0)