Skip to main content

sipx_transport/
ws.rs

1//! SIP over WebSocket (RFC 7118).
2//!
3//! Two things make this a transport of its own rather than TCP with a wrapper round it, and
4//! both come from `docs/specs/sip-tls.md` §4.
5//!
6//! **The frame is the message.** RFC 7118 §5 puts exactly one SIP message in each WebSocket
7//! message — not `Content-Length` framing, which is what every other stream transport here
8//! uses. So a peer that sends half a message, or two, has not sent something sipx should try to
9//! make sense of; it has revealed that it does not agree about where messages begin.
10//!
11//! **The client cannot be connected back to.** A browser has no listening port, so its `Via`
12//! sent-by is an invented name that will never resolve, and everything sipx sends it goes back
13//! over the connection it came in on. That is the RFC 5923 rule from the TCP transport made
14//! absolute: here there is no fallback, because there is nowhere to fall back to.
15//!
16//! WSS is this module over the TLS from [`crate::tls`] — the same certificate policy and the
17//! same code, because a second implementation of a security check is how one of the two ends up
18//! weaker.
19
20use std::net::SocketAddr;
21use std::sync::Arc;
22use std::time::Duration;
23
24use bytes::Bytes;
25use futures_util::{SinkExt as _, StreamExt as _};
26use sipx_sip::error::{FramingError, ParseError};
27use sipx_sip::{Limits, Message, StreamParser, parse_datagram};
28use tokio::io::{AsyncRead, AsyncWrite};
29use tokio::sync::mpsc;
30use tokio_tungstenite::tungstenite::Message as Frame;
31use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response};
32use tokio_tungstenite::tungstenite::http::{HeaderMap, HeaderValue, StatusCode};
33use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
34use tokio_tungstenite::{WebSocketStream, accept_hdr_async_with_config, client_async_with_config};
35
36use crate::target::{ConnectionKey, TransportKind};
37use crate::tcp::Event;
38use crate::{ConnectionState, policy::ObservationHub};
39
40/// The subprotocol name RFC 7118 §4.2 registers.
41pub const SUBPROTOCOL: &str = "sip";
42
43/// The header both halves of the handshake negotiate it in.
44const PROTOCOL_HEADER: &str = "sec-websocket-protocol";
45
46/// A negotiated WebSocket carrying SIP.
47pub type Socket<S> = WebSocketStream<S>;
48
49/// What can go wrong establishing a WebSocket.
50#[derive(Debug, thiserror::Error)]
51#[non_exhaustive]
52pub enum WsError {
53    /// The peer never agreed to carry SIP.
54    ///
55    /// Refusing is the point rather than an inconvenience: without the subprotocol there is no
56    /// agreement about what the frames mean, and guessing is how a stack ends up parsing
57    /// somebody else's protocol as SIP.
58    #[error("{peer} did not agree to the sip subprotocol (RFC 7118 §4.2)")]
59    Subprotocol {
60        /// Who we were talking to.
61        peer: String,
62    },
63    /// The upgrade itself failed.
64    #[error("websocket handshake with {peer}: {detail}")]
65    Handshake {
66        /// Who we were talking to.
67        peer: String,
68        /// What went wrong.
69        detail: String,
70    },
71}
72
73/// Perform the client half of the handshake, asking for the `sip` subprotocol at `path`.
74///
75/// `authority` is the host and port from the URI — what goes in `Host`, and under WSS the name
76/// the certificate had to be valid for. `path` is the resource to ask for: RFC 7118 §5 does not
77/// fix one, so where a server serves SIP is the server's business and the caller's to know.
78pub async fn connect<S>(
79    stream: S,
80    authority: &str,
81    path: &str,
82    secure: bool,
83) -> Result<Socket<S>, WsError>
84where
85    S: AsyncRead + AsyncWrite + Unpin,
86{
87    connect_with_limits(stream, authority, path, secure, &Limits::stream()).await
88}
89
90/// The client handshake with the endpoint's actual SIP allocation limits.
91pub(crate) async fn connect_with_limits<S>(
92    stream: S,
93    authority: &str,
94    path: &str,
95    secure: bool,
96    limits: &Limits,
97) -> Result<Socket<S>, WsError>
98where
99    S: AsyncRead + AsyncWrite + Unpin,
100{
101    let failed = |detail: String| WsError::Handshake {
102        peer: authority.to_owned(),
103        detail,
104    };
105
106    let request =
107        upgrade_request(authority, path, secure).map_err(|error| failed(error.to_string()))?;
108
109    let (socket, response) =
110        client_async_with_config(request, stream, Some(websocket_config(limits)))
111            .await
112            .map_err(|error| failed(error.to_string()))?;
113
114    // A server that ignores the subprotocol and upgrades anyway has agreed to nothing, and
115    // taking the connection on that basis is exactly the guess RFC 7118 §4.2 forbids.
116    //
117    // Redundant today: the handshake below already refuses a response that does not echo a
118    // subprotocol we asked for, so this never fires. It stays because that is a *dependency's*
119    // behaviour, and a guarantee sipx makes should not quietly become a guarantee sipx hopes
120    // someone else still makes.
121    if !offers_sip(response.headers()) {
122        return Err(WsError::Subprotocol {
123            peer: authority.to_owned(),
124        });
125    }
126
127    Ok(socket)
128}
129
130/// The upgrade request sipx sends.
131///
132/// Written out in full rather than half-specified: the handshake takes what it is given and
133/// refuses a request missing any of these, so there is nothing gained by leaving one out and a
134/// confusing failure to be had by trying.
135///
136/// `Sec-WebSocket-Key` is a fresh nonce whose echo proves the peer understood the upgrade rather
137/// than being an HTTP server that says yes to everything (RFC 6455 §4.1).
138///
139/// The URI carries the resource and `Host` does not. They come apart precisely here: `Host` is
140/// the authority alone (RFC 7230 §5.4), while the request-target is the path the server matches
141/// its routes against — and a server serving SIP at `/ws` answers `404` to the `/` this used to
142/// send unconditionally.
143fn upgrade_request(
144    authority: &str,
145    path: &str,
146    secure: bool,
147) -> Result<
148    tokio_tungstenite::tungstenite::http::Request<()>,
149    tokio_tungstenite::tungstenite::http::Error,
150> {
151    let scheme = if secure { "wss" } else { "ws" };
152    tokio_tungstenite::tungstenite::http::Request::builder()
153        .method("GET")
154        .uri(format!("{scheme}://{authority}{path}"))
155        .header("Host", authority)
156        .header("Connection", "Upgrade")
157        .header("Upgrade", "websocket")
158        .header("Sec-WebSocket-Version", "13")
159        .header(
160            "Sec-WebSocket-Key",
161            tokio_tungstenite::tungstenite::handshake::client::generate_key(),
162        )
163        .header(PROTOCOL_HEADER, SUBPROTOCOL)
164        .body(())
165}
166
167/// Perform the server half, refusing a peer that does not offer the `sip` subprotocol.
168// The refusal type is an HTTP response and is as large as one. It is not ours to box: it is the
169// shape the handshake callback must return.
170#[allow(clippy::result_large_err)]
171pub async fn accept<S>(stream: S, peer: SocketAddr) -> Result<Socket<S>, WsError>
172where
173    S: AsyncRead + AsyncWrite + Unpin,
174{
175    accept_with_limits(stream, peer, &Limits::stream()).await
176}
177
178/// The server handshake with the endpoint's actual SIP allocation limits.
179// The callback's refusal is an HTTP response and is as large as one. The dependency fixes that
180// return type, so boxing it locally would only move the allocation after the frame bound.
181#[allow(clippy::result_large_err)]
182pub(crate) async fn accept_with_limits<S>(
183    stream: S,
184    peer: SocketAddr,
185    limits: &Limits,
186) -> Result<Socket<S>, WsError>
187where
188    S: AsyncRead + AsyncWrite + Unpin,
189{
190    accept_hdr_async_with_config(
191        stream,
192        |request: &Request, mut response: Response| {
193            if !offers_sip(request.headers()) {
194                let mut refusal = ErrorResponse::new(Some(format!(
195                    "this endpoint speaks the {SUBPROTOCOL} subprotocol only (RFC 7118 §4.2)"
196                )));
197                *refusal.status_mut() = StatusCode::BAD_REQUEST;
198                return Err(refusal);
199            }
200            // Echoing it is what makes the negotiation two-sided: the client is entitled to check
201            // this answer just as sipx checks the server's.
202            response
203                .headers_mut()
204                .insert(PROTOCOL_HEADER, HeaderValue::from_static(SUBPROTOCOL));
205            Ok(response)
206        },
207        Some(websocket_config(limits)),
208    )
209    .await
210    .map_err(|error| WsError::Handshake {
211        peer: peer.to_string(),
212        detail: error.to_string(),
213    })
214}
215
216/// Cap the WebSocket implementation before it allocates from the peer's frame length.
217fn websocket_config(limits: &Limits) -> WebSocketConfig {
218    WebSocketConfig::default()
219        .max_message_size(Some(limits.max_message_bytes))
220        .max_frame_size(Some(limits.max_message_bytes))
221}
222
223/// A `Via` sent-by for an endpoint that can never be connected back to (RFC 7118 §5.2).
224///
225/// `.invalid` is reserved by RFC 2606 and guaranteed never to resolve, which is the whole
226/// point: nothing anywhere must ever try to open a connection to it. Advertising a real
227/// address here would be worse than useless — it would send a proxy off to a port that is not
228/// listening while the connection it should have used sits open.
229#[must_use]
230pub fn invented_sent_by() -> String {
231    use rand::Rng;
232    let value: u64 = rand::rng().random();
233    format!("{value:016x}.invalid")
234}
235
236/// Whether these headers name the `sip` subprotocol.
237///
238/// A peer may offer several, comma-separated or in repeated headers; RFC 6455 §4.1 allows both.
239fn offers_sip(headers: &HeaderMap) -> bool {
240    headers
241        .get_all(PROTOCOL_HEADER)
242        .iter()
243        .filter_map(|value| value.to_str().ok())
244        .flat_map(|value| value.split(','))
245        .any(|token| token.trim().eq_ignore_ascii_case(SUBPROTOCOL))
246}
247
248/// Handshake as a client and then pump, reporting a failure the same way a refused connection
249/// is reported — because to everything upstream that is what it is.
250#[allow(
251    clippy::too_many_arguments,
252    reason = "the generation travels beside the existing connection identity and pump policy"
253)]
254pub(crate) async fn dial<S>(
255    stream: S,
256    authority: &str,
257    key: ConnectionKey,
258    id: u64,
259    outgoing: mpsc::Receiver<Bytes>,
260    events: mpsc::Sender<Event>,
261    limits: Limits,
262    keepalive: Duration,
263    observations: Option<Arc<ObservationHub>>,
264    admission_generation: Option<u64>,
265    authenticated: bool,
266) where
267    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
268{
269    let secure = key.transport == TransportKind::Wss;
270    // The resource travels on the key rather than beside it, because it is part of what makes
271    // this connection this connection — see [`ConnectionKey`].
272    match connect_with_limits(stream, authority, key.ws_path(), secure, &limits).await {
273        Ok(socket) => {
274            crate::tcp::observe_ready(
275                observations.as_ref(),
276                &key,
277                id,
278                admission_generation,
279                authenticated,
280            );
281            pump(socket, key, id, outgoing, events, limits, keepalive).await;
282        }
283        Err(error) => {
284            tracing::warn!(%error, peer = %key.peer, "websocket handshake failed");
285            crate::tcp::observe_state(
286                observations.as_ref(),
287                &key,
288                id,
289                admission_generation,
290                ConnectionState::Failed,
291            );
292        }
293    }
294}
295
296/// Read and write one WebSocket until it ends.
297pub(crate) async fn pump<S>(
298    socket: Socket<S>,
299    key: ConnectionKey,
300    id: u64,
301    mut outgoing: mpsc::Receiver<Bytes>,
302    events: mpsc::Sender<Event>,
303    limits: Limits,
304    keepalive: Duration,
305) where
306    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
307{
308    let (peer, transport) = (key.peer, key.transport);
309    let (mut sink, mut source) = socket.split();
310
311    // Intermediaries close sockets that have said nothing for a while, and a registration whose
312    // connection has silently died is a phone that rings nowhere. A Ping (RFC 6455 §5.5.2) is
313    // the cheapest thing that keeps the path open and, because the peer must answer it, the
314    // only one that also tells us the path is still there.
315    let mut ping = tokio::time::interval(keepalive);
316    ping.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
317    ping.tick().await;
318
319    loop {
320        tokio::select! {
321            frame = source.next() => {
322                let payload = match frame {
323                    Some(Ok(Frame::Text(text))) => Bytes::from(text),
324                    Some(Ok(Frame::Binary(data))) => data,
325                    // Pongs are answered by the protocol layer before this ever sees them.
326                    Some(Ok(Frame::Ping(_) | Frame::Pong(_) | Frame::Frame(_))) => continue,
327                    Some(Ok(Frame::Close(_))) | None => break,
328                    Some(Err(error)) => {
329                        tracing::debug!(%error, %peer, "websocket read failed");
330                        break;
331                    }
332                };
333                match parse_one(payload, &limits) {
334                    Ok(message) => {
335                        if events
336                            .send(Event::Message {
337                                message: Box::new(message),
338                                source: peer,
339                                transport,
340                                id,
341                                #[cfg(feature = "quic")]
342                                quic_reply: None,
343                            })
344                            .await
345                            .is_err()
346                        {
347                            return;
348                        }
349                    }
350                    Err(detail) => {
351                        // The same reasoning as a `Content-Length` framing error on TCP: once
352                        // the two ends disagree about where a message ends, nothing further
353                        // from this peer can be trusted to be what it claims.
354                        // discard: everything in flight on this connection, for the same reason as
355                        // TCP's framing error. Counted by the driver, which the `FramingFailed`
356                        // below tells; a connection task has no `Meters` in scope.
357                        tracing::debug!(%peer, %detail, "closing on a malformed websocket message");
358                        let _ = events.send(Event::FramingFailed { key: key.clone() }).await;
359                        break;
360                    }
361                }
362            },
363            Some(bytes) = outgoing.recv() => {
364                if sink.send(frame_for(bytes)).await.is_err() {
365                    break;
366                }
367            }
368            _ = ping.tick() => {
369                if sink.send(Frame::Ping(Bytes::new())).await.is_err() {
370                    break;
371                }
372            }
373        }
374    }
375
376    // A close frame, so the peer learns this was deliberate rather than a network failure it
377    // should retry through.
378    // discard: a best-effort close on a connection that is already going. Failure means the
379    // peer has gone, which is the very state being reported on the next line.
380    let _ = sink.close().await;
381}
382
383/// Parse exactly one SIP message out of one WebSocket message.
384///
385/// Strict, and deliberately stricter than the datagram parser it borrows from. RFC 3261 §18.3
386/// says octets after a message in a *datagram* are noise to be ignored; RFC 7118 §5 says a
387/// WebSocket message carries one SIP message and no more, so the same octets here mean the peer
388/// is framing wrongly.
389#[derive(Debug, thiserror::Error)]
390enum WsFramingError {
391    #[error(transparent)]
392    Sip(#[from] ParseError),
393    #[error(
394        "a WebSocket message carries exactly one SIP message (RFC 7118 §5); \
395         this one held {complete} complete and {trailing} octets of another"
396    )]
397    Shape { complete: usize, trailing: usize },
398}
399
400fn parse_one(frame: Bytes, limits: &Limits) -> Result<Message, WsFramingError> {
401    // The stream parser is reused rather than reimplemented: it already knows every rule about
402    // where a message ends, and a second copy of those rules is a second place for them to
403    // drift. What differs is only what is done with the answer — here, anything other than
404    // exactly one whole message is a fault.
405    let mut parser = StreamParser::new(*limits);
406    match parser.push(&frame) {
407        Ok(mut messages) => {
408            let trailing = parser.pending();
409            if trailing == 0
410                && messages.len() == 1
411                && let Some(message) = messages.pop()
412            {
413                return Ok(message);
414            }
415            Err(WsFramingError::Shape {
416                complete: messages.len(),
417                trailing,
418            })
419        }
420        // The one framing rule that does not carry over. `Content-Length` is mandatory on a
421        // stream because nothing else says where a message ends. Here the frame says, so a
422        // message without one is legal and its body runs to the end of the frame — which is
423        // what RFC 3261 §20.14 already prescribes wherever the transport delimits.
424        Err(ParseError::Framing(FramingError::ContentLengthRequired)) => {
425            parse_datagram(frame, limits).map_err(WsFramingError::from)
426        }
427        Err(error) => Err(error.into()),
428    }
429}
430
431/// Put a message in a frame.
432///
433/// Text where the bytes allow it. RFC 7118 §5 permits either, and text is what a browser's
434/// network panel and every WebSocket capture tool will show as readable SIP; a body that is not
435/// valid UTF-8 leaves binary as the only correct choice.
436fn frame_for(bytes: Bytes) -> Frame {
437    match tokio_tungstenite::tungstenite::Utf8Bytes::try_from(bytes.clone()) {
438        Ok(text) => Frame::Text(text),
439        Err(_) => Frame::Binary(bytes),
440    }
441}
442
443#[cfg(test)]
444#[allow(
445    clippy::unwrap_used,
446    clippy::expect_used,
447    clippy::panic,
448    clippy::indexing_slicing
449)]
450mod tests {
451    use super::*;
452
453    const OPTIONS: &str = "OPTIONS sip:a@b.com SIP/2.0\r\n\
454         Via: SIP/2.0/WS df7jal23ls0d.invalid;branch=z9hG4bKx\r\n\
455         To: <sip:a@b.com>\r\n\
456         From: <sip:c@d.net>;tag=1\r\n\
457         Call-ID: x@y\r\n\
458         CSeq: 1 OPTIONS\r\n\
459         Content-Length: 0\r\n\r\n";
460
461    fn parse(text: &str) -> Result<Message, WsFramingError> {
462        parse_one(Bytes::copy_from_slice(text.as_bytes()), &Limits::stream())
463    }
464
465    #[test]
466    fn one_message_in_one_frame_is_parsed() {
467        parse(OPTIONS).expect("one message in one frame");
468    }
469
470    /// W3, and the half of RFC 7118 §5 that is easy to get wrong: this is precisely what the
471    /// TCP transport is *supposed* to accept, held over until the rest arrives.
472    #[test]
473    fn a_message_split_across_frames_is_malformed() {
474        let half = &OPTIONS[..OPTIONS.len() / 2];
475        let error = parse(half).expect_err("half a message is not a message");
476        assert!(error.to_string().contains("exactly one"), "{error}");
477    }
478
479    /// The other half: two messages in one frame is not two messages, it is a framing fault.
480    /// Note that the *datagram* parser would quietly return the first and drop the second.
481    #[test]
482    fn two_messages_in_one_frame_are_malformed() {
483        let error = parse(&format!("{OPTIONS}{OPTIONS}")).expect_err("two is not one");
484        assert!(error.to_string().contains("exactly one"), "{error}");
485    }
486
487    /// Trailing octets are the same fault, and the case that separates this from
488    /// `parse_datagram` — there they are noise the RFC says to ignore.
489    #[test]
490    fn octets_after_the_message_are_malformed() {
491        parse(&format!("{OPTIONS}garbage"))
492            .expect_err("a frame holds one message and nothing else");
493    }
494
495    /// A frame delimits, so `Content-Length` is not load-bearing the way it is on a stream.
496    /// Refusing here would reject messages the transport can frame perfectly well.
497    #[test]
498    fn a_message_without_content_length_is_accepted() {
499        let without = OPTIONS.replace("Content-Length: 0\r\n", "");
500        parse(&without).expect("the frame says where it ends");
501    }
502
503    #[test]
504    fn a_frame_holding_nothing_like_sip_is_refused() {
505        parse("hello").expect_err("not a SIP message");
506    }
507
508    // Production's enum is deliberate: its exhaustive matches stop compiling when a transport is
509    // added until X-64's framing expectations classify it. QUIC has its own one-stream/one-message
510    // tests; this table is the five paths named by X-64.
511    const RFC3261_FRAMING_PATHS: [TransportKind; 5] = [
512        TransportKind::Udp,
513        TransportKind::Tcp,
514        TransportKind::Tls,
515        TransportKind::Ws,
516        TransportKind::Wss,
517    ];
518
519    fn body_limit_refusal(path: TransportKind, frame: &[u8], limits: &Limits) -> ParseError {
520        match path {
521            TransportKind::Udp => parse_datagram(Bytes::copy_from_slice(frame), limits)
522                .expect_err("the datagram body limit must refuse"),
523            TransportKind::Tcp | TransportKind::Tls => {
524                let mut parser = StreamParser::new(*limits);
525                parser
526                    .push(frame)
527                    .expect_err("the stream body limit must refuse")
528            }
529            TransportKind::Ws | TransportKind::Wss => {
530                match parse_one(Bytes::copy_from_slice(frame), limits)
531                    .expect_err("the WebSocket body limit must refuse")
532                {
533                    WsFramingError::Sip(error) => error,
534                    error @ WsFramingError::Shape { .. } => {
535                        panic!("the SIP limit must run before frame-shape handling: {error}")
536                    }
537                }
538            }
539            TransportKind::Quic => {
540                panic!(
541                    "QUIC is bounded by its one-stream/one-message reader, not this RFC 3261 table"
542                )
543            }
544        }
545    }
546
547    async fn handshaken_pair(
548        secure: bool,
549        client_limits: Limits,
550        server_limits: Limits,
551    ) -> (
552        Socket<tokio::io::DuplexStream>,
553        Socket<tokio::io::DuplexStream>,
554    ) {
555        let (client_io, server_io) = tokio::io::duplex(4096);
556        let peer = "127.0.0.1:5060".parse().expect("a peer address");
557        let (client, server) = tokio::join!(
558            connect_with_limits(client_io, "example.com", "/", secure, &client_limits),
559            accept_with_limits(server_io, peer, &server_limits),
560        );
561        (
562            client.expect("the client handshake completes"),
563            server.expect("the server handshake completes"),
564        )
565    }
566
567    fn limits_with_message_bound(max_message_bytes: usize) -> Limits {
568        Limits {
569            max_message_bytes,
570            max_body_bytes: max_message_bytes,
571            ..Limits::stream()
572        }
573    }
574
575    fn assert_message_too_long(
576        error: &tokio_tungstenite::tungstenite::Error,
577        expected_size: usize,
578        expected_limit: usize,
579    ) {
580        use tokio_tungstenite::tungstenite::error::CapacityError;
581        assert!(
582            matches!(
583                error,
584                tokio_tungstenite::tungstenite::Error::Capacity(
585                    CapacityError::MessageTooLong { size, max_size }
586                ) if *size == expected_size && *max_size == expected_limit
587            ),
588            "the handshake did not install the configured decoder bound: {error}"
589        );
590    }
591
592    /// RFC 6455 §5.2 and RFC 7118 §5: the actual client handshake installs the configured limit
593    /// in both WS and WSS modes. The oversized frame is refused by the WebSocket decoder itself,
594    /// before SIP parsing could hide a missing pre-allocation bound.
595    #[tokio::test]
596    async fn client_handshake_holds_the_frame_bound_for_ws_and_wss() {
597        const HELD: usize = 32;
598        const SENT: usize = 64;
599        for secure in [false, true] {
600            let (mut client, mut server) = handshaken_pair(
601                secure,
602                limits_with_message_bound(HELD),
603                limits_with_message_bound(256),
604            )
605            .await;
606            assert_eq!(client.get_config().max_frame_size, Some(HELD));
607            assert_eq!(client.get_config().max_message_size, Some(HELD));
608
609            server
610                .send(Frame::binary(vec![0; SENT]))
611                .await
612                .expect("the permissive peer sends the probe");
613            let error = client
614                .next()
615                .await
616                .expect("the probe has a decoder outcome")
617                .expect_err("an oversized frame is refused before SIP parsing");
618            assert_message_too_long(&error, SENT, HELD);
619        }
620    }
621
622    /// RFC 6455 §5.2 and RFC 7118 §5: the actual server handshake independently installs the
623    /// configured limit. Running both URI modes pins the shared WS/WSS server seam without
624    /// inferring its behavior from the client configuration.
625    #[tokio::test]
626    async fn server_handshake_holds_the_frame_bound_for_ws_and_wss() {
627        const HELD: usize = 32;
628        const SENT: usize = 64;
629        for secure in [false, true] {
630            let (mut client, mut server) = handshaken_pair(
631                secure,
632                limits_with_message_bound(256),
633                limits_with_message_bound(HELD),
634            )
635            .await;
636            assert_eq!(server.get_config().max_frame_size, Some(HELD));
637            assert_eq!(server.get_config().max_message_size, Some(HELD));
638
639            client
640                .send(Frame::binary(vec![0; SENT]))
641                .await
642                .expect("the permissive peer sends the probe");
643            let error = server
644                .next()
645                .await
646                .expect("the probe has a decoder outcome")
647                .expect_err("an oversized frame is refused before SIP parsing");
648            assert_message_too_long(&error, SENT, HELD);
649        }
650    }
651
652    /// RFC 3261 §20.14, RFC 7118 §5 and RFC 6455 §5.2: every network framing path refuses a
653    /// declared body above its bound before reserving it. WS and WSS additionally pass the same
654    /// bound into the WebSocket decoder, where the peer's frame length is first observed.
655    #[test]
656    fn pre_allocation_body_and_frame_bounds_hold_on_every_framing_path() {
657        let limits = Limits {
658            max_message_bytes: 256,
659            max_body_bytes: 4,
660            ..Limits::stream()
661        };
662        let frame = b"MESSAGE sip:a@b SIP/2.0\r\nContent-Length: 5\r\n\r\n";
663
664        for path in RFC3261_FRAMING_PATHS {
665            assert_eq!(
666                body_limit_refusal(path, frame, &limits),
667                ParseError::Limit {
668                    limit: sipx_sip::error::LimitKind::BodyBytes,
669                    value: 5,
670                },
671                "{path:?} did not return the typed body-size refusal"
672            );
673
674            if matches!(path, TransportKind::Ws | TransportKind::Wss) {
675                let config = websocket_config(&limits);
676                assert_eq!(
677                    config.max_frame_size,
678                    Some(limits.max_message_bytes),
679                    "{path:?} would allocate an oversized frame before SIP parsing"
680                );
681                assert_eq!(
682                    config.max_message_size,
683                    Some(limits.max_message_bytes),
684                    "{path:?} would assemble an oversized fragmented message"
685                );
686            }
687        }
688    }
689
690    /// RFC 3261 §20.14, RFC 4475 §3.1.2.2 and RFC 7118 §5: a short body is refused or held as
691    /// bounded incomplete input, while bytes beyond a declared body are never read into it. A
692    /// datagram ignores its RFC-permitted trailing noise; a byte stream holds it for the next
693    /// message; a WebSocket frame refuses it because one frame is exactly one message.
694    #[test]
695    fn body_length_disagreement_is_typed_or_bounded_on_every_framing_path() {
696        let limits = Limits {
697            max_message_bytes: 256,
698            max_body_bytes: 16,
699            ..Limits::stream()
700        };
701        let prefix = b"MESSAGE sip:a@b SIP/2.0\r\nContent-Length: 4\r\n\r\n";
702        let mut short = prefix.to_vec();
703        short.extend_from_slice(b"abc");
704        let mut long = prefix.to_vec();
705        long.extend_from_slice(b"abcde");
706
707        for path in RFC3261_FRAMING_PATHS {
708            match path {
709                TransportKind::Udp => {
710                    assert!(
711                        matches!(
712                            parse_datagram(Bytes::copy_from_slice(&short), &limits),
713                            Err(ParseError::Framing(FramingError::BodyTruncated))
714                        ),
715                        "UDP did not type the short-body refusal"
716                    );
717                    let message = parse_datagram(Bytes::copy_from_slice(&long), &limits)
718                        .expect("UDP ignores octets beyond the declared body");
719                    assert_eq!(message.body().as_ref(), b"abcd");
720                }
721                TransportKind::Tcp | TransportKind::Tls => {
722                    let mut short_parser = StreamParser::new(limits);
723                    assert!(
724                        short_parser.push(&short).expect("bounded wait").is_empty(),
725                        "{path:?} read a short body as complete"
726                    );
727                    assert_eq!(
728                        short_parser.pending(),
729                        3,
730                        "{path:?} did not bound pending input"
731                    );
732
733                    let mut long_parser = StreamParser::new(limits);
734                    let messages = long_parser.push(&long).expect("one complete message");
735                    assert_eq!(
736                        messages.len(),
737                        1,
738                        "{path:?} did not frame exactly one message"
739                    );
740                    assert_eq!(messages[0].body().as_ref(), b"abcd");
741                    assert_eq!(
742                        long_parser.pending(),
743                        1,
744                        "{path:?} read the next message's byte into this body"
745                    );
746                }
747                TransportKind::Ws | TransportKind::Wss => {
748                    assert!(
749                        matches!(
750                            parse_one(Bytes::copy_from_slice(&short), &limits),
751                            Err(WsFramingError::Shape {
752                                complete: 0,
753                                trailing: 3
754                            })
755                        ),
756                        "{path:?} did not type the short-frame refusal"
757                    );
758                    assert!(
759                        matches!(
760                            parse_one(Bytes::copy_from_slice(&long), &limits),
761                            Err(WsFramingError::Shape {
762                                complete: 1,
763                                trailing: 1
764                            })
765                        ),
766                        "{path:?} accepted bytes beyond the declared body"
767                    );
768                }
769                TransportKind::Quic => {
770                    panic!(
771                        "QUIC is bounded by its one-stream/one-message reader, not this RFC 3261 table"
772                    )
773                }
774            }
775        }
776    }
777
778    #[test]
779    fn a_sip_message_travels_as_text() {
780        assert!(matches!(
781            frame_for(Bytes::copy_from_slice(OPTIONS.as_bytes())),
782            Frame::Text(_)
783        ));
784    }
785
786    /// A body that is not UTF-8 cannot go in a text frame — RFC 6455 §5.6 requires text frames
787    /// to be valid UTF-8, and a peer must fail the connection when they are not.
788    #[test]
789    fn a_binary_body_travels_as_binary() {
790        assert!(matches!(
791            frame_for(Bytes::from_static(b"MESSAGE sip:a SIP/2.0\r\n\r\n\xff\xfe")),
792            Frame::Binary(_)
793        ));
794    }
795
796    #[test]
797    fn the_subprotocol_is_found_however_it_is_offered() {
798        let mut headers = HeaderMap::new();
799        headers.append(PROTOCOL_HEADER, HeaderValue::from_static("sip"));
800        assert!(offers_sip(&headers));
801
802        let mut listed = HeaderMap::new();
803        listed.append(PROTOCOL_HEADER, HeaderValue::from_static("chat, SIP, echo"));
804        assert!(
805            offers_sip(&listed),
806            "comma-separated, and case is not part of it"
807        );
808
809        let mut repeated = HeaderMap::new();
810        repeated.append(PROTOCOL_HEADER, HeaderValue::from_static("chat"));
811        repeated.append(PROTOCOL_HEADER, HeaderValue::from_static("sip"));
812        assert!(offers_sip(&repeated), "repeated headers are one list");
813
814        let mut other = HeaderMap::new();
815        other.append(PROTOCOL_HEADER, HeaderValue::from_static("chat, sipx"));
816        assert!(!offers_sip(&other), "a longer token is a different token");
817
818        assert!(
819            !offers_sip(&HeaderMap::new()),
820            "offering none is not offering sip"
821        );
822    }
823
824    /// The line this transport used to get wrong. The request-target was `/` whatever the
825    /// caller wanted, so a server serving SIP anywhere else answered `404` and the connection
826    /// simply never existed — see `docs/specs/sip-tls.md` §6, W13.
827    #[test]
828    fn the_upgrade_asks_for_the_resource_it_was_given() {
829        let request = upgrade_request("127.0.0.1:8088", "/ws", false).expect("a request");
830        assert_eq!(request.uri().to_string(), "ws://127.0.0.1:8088/ws");
831        assert_eq!(request.uri().path(), "/ws");
832        // `Host` is the authority and nothing else (RFC 7230 §5.4). The resource belongs in the
833        // request-target, which is where a server looks for it.
834        assert_eq!(
835            request.headers().get("Host").expect("a Host"),
836            "127.0.0.1:8088"
837        );
838    }
839
840    #[test]
841    fn the_root_is_still_what_a_caller_naming_nothing_asks_for() {
842        let request = upgrade_request("127.0.0.1:5060", "/", false).expect("a request");
843        assert_eq!(request.uri().to_string(), "ws://127.0.0.1:5060/");
844    }
845
846    /// WSS changes the scheme and nothing else about where the resource goes.
847    #[test]
848    fn a_secure_upgrade_keeps_the_resource() {
849        let request = upgrade_request("sipx.test:443", "/ws", true).expect("a request");
850        assert_eq!(request.uri().to_string(), "wss://sipx.test:443/ws");
851    }
852
853    /// Servers do exist that want a token in the query, and RFC 7118 §5 no more forbids that
854    /// than it fixes the path. Whatever the caller named is what is asked for.
855    #[test]
856    fn a_query_string_survives_the_handshake() {
857        let request = upgrade_request("127.0.0.1:8088", "/ws?token=abc", false).expect("a request");
858        assert_eq!(request.uri().path(), "/ws");
859        assert_eq!(request.uri().query(), Some("token=abc"));
860    }
861
862    /// It must never resolve, and it must never be the same twice: RFC 7118 §5.2 asks for a
863    /// unique name so two clients behind one proxy stay distinguishable.
864    #[test]
865    fn an_invented_sent_by_is_unresolvable_and_unique() {
866        let one = invented_sent_by();
867        assert!(one.ends_with(".invalid"), "{one}");
868        assert_ne!(one, invented_sent_by());
869    }
870}