use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use futures_util::{SinkExt as _, StreamExt as _};
use sipx_sip::error::{FramingError, ParseError};
use sipx_sip::{Limits, Message, StreamParser, parse_datagram};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::sync::mpsc;
use tokio_tungstenite::tungstenite::Message as Frame;
use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response};
use tokio_tungstenite::tungstenite::http::{HeaderMap, HeaderValue, StatusCode};
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
use tokio_tungstenite::{WebSocketStream, accept_hdr_async_with_config, client_async_with_config};
use crate::target::{ConnectionKey, TransportKind};
use crate::tcp::Event;
use crate::{ConnectionState, policy::ObservationHub};
pub const SUBPROTOCOL: &str = "sip";
const PROTOCOL_HEADER: &str = "sec-websocket-protocol";
pub type Socket<S> = WebSocketStream<S>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WsError {
#[error("{peer} did not agree to the sip subprotocol (RFC 7118 §4.2)")]
Subprotocol {
peer: String,
},
#[error("websocket handshake with {peer}: {detail}")]
Handshake {
peer: String,
detail: String,
},
}
pub async fn connect<S>(
stream: S,
authority: &str,
path: &str,
secure: bool,
) -> Result<Socket<S>, WsError>
where
S: AsyncRead + AsyncWrite + Unpin,
{
connect_with_limits(stream, authority, path, secure, &Limits::stream()).await
}
pub(crate) async fn connect_with_limits<S>(
stream: S,
authority: &str,
path: &str,
secure: bool,
limits: &Limits,
) -> Result<Socket<S>, WsError>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let failed = |detail: String| WsError::Handshake {
peer: authority.to_owned(),
detail,
};
let request =
upgrade_request(authority, path, secure).map_err(|error| failed(error.to_string()))?;
let (socket, response) =
client_async_with_config(request, stream, Some(websocket_config(limits)))
.await
.map_err(|error| failed(error.to_string()))?;
if !offers_sip(response.headers()) {
return Err(WsError::Subprotocol {
peer: authority.to_owned(),
});
}
Ok(socket)
}
fn upgrade_request(
authority: &str,
path: &str,
secure: bool,
) -> Result<
tokio_tungstenite::tungstenite::http::Request<()>,
tokio_tungstenite::tungstenite::http::Error,
> {
let scheme = if secure { "wss" } else { "ws" };
tokio_tungstenite::tungstenite::http::Request::builder()
.method("GET")
.uri(format!("{scheme}://{authority}{path}"))
.header("Host", authority)
.header("Connection", "Upgrade")
.header("Upgrade", "websocket")
.header("Sec-WebSocket-Version", "13")
.header(
"Sec-WebSocket-Key",
tokio_tungstenite::tungstenite::handshake::client::generate_key(),
)
.header(PROTOCOL_HEADER, SUBPROTOCOL)
.body(())
}
#[allow(clippy::result_large_err)]
pub async fn accept<S>(stream: S, peer: SocketAddr) -> Result<Socket<S>, WsError>
where
S: AsyncRead + AsyncWrite + Unpin,
{
accept_with_limits(stream, peer, &Limits::stream()).await
}
#[allow(clippy::result_large_err)]
pub(crate) async fn accept_with_limits<S>(
stream: S,
peer: SocketAddr,
limits: &Limits,
) -> Result<Socket<S>, WsError>
where
S: AsyncRead + AsyncWrite + Unpin,
{
accept_hdr_async_with_config(
stream,
|request: &Request, mut response: Response| {
if !offers_sip(request.headers()) {
let mut refusal = ErrorResponse::new(Some(format!(
"this endpoint speaks the {SUBPROTOCOL} subprotocol only (RFC 7118 §4.2)"
)));
*refusal.status_mut() = StatusCode::BAD_REQUEST;
return Err(refusal);
}
response
.headers_mut()
.insert(PROTOCOL_HEADER, HeaderValue::from_static(SUBPROTOCOL));
Ok(response)
},
Some(websocket_config(limits)),
)
.await
.map_err(|error| WsError::Handshake {
peer: peer.to_string(),
detail: error.to_string(),
})
}
fn websocket_config(limits: &Limits) -> WebSocketConfig {
WebSocketConfig::default()
.max_message_size(Some(limits.max_message_bytes))
.max_frame_size(Some(limits.max_message_bytes))
}
#[must_use]
pub fn invented_sent_by() -> String {
use rand::Rng;
let value: u64 = rand::rng().random();
format!("{value:016x}.invalid")
}
fn offers_sip(headers: &HeaderMap) -> bool {
headers
.get_all(PROTOCOL_HEADER)
.iter()
.filter_map(|value| value.to_str().ok())
.flat_map(|value| value.split(','))
.any(|token| token.trim().eq_ignore_ascii_case(SUBPROTOCOL))
}
#[allow(
clippy::too_many_arguments,
reason = "the generation travels beside the existing connection identity and pump policy"
)]
pub(crate) async fn dial<S>(
stream: S,
authority: &str,
key: ConnectionKey,
id: u64,
outgoing: mpsc::Receiver<Bytes>,
events: mpsc::Sender<Event>,
limits: Limits,
keepalive: Duration,
observations: Option<Arc<ObservationHub>>,
admission_generation: Option<u64>,
authenticated: bool,
) where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let secure = key.transport == TransportKind::Wss;
match connect_with_limits(stream, authority, key.ws_path(), secure, &limits).await {
Ok(socket) => {
crate::tcp::observe_ready(
observations.as_ref(),
&key,
id,
admission_generation,
authenticated,
);
pump(socket, key, id, outgoing, events, limits, keepalive).await;
}
Err(error) => {
tracing::warn!(%error, peer = %key.peer, "websocket handshake failed");
crate::tcp::observe_state(
observations.as_ref(),
&key,
id,
admission_generation,
ConnectionState::Failed,
);
}
}
}
pub(crate) async fn pump<S>(
socket: Socket<S>,
key: ConnectionKey,
id: u64,
mut outgoing: mpsc::Receiver<Bytes>,
events: mpsc::Sender<Event>,
limits: Limits,
keepalive: Duration,
) where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let (peer, transport) = (key.peer, key.transport);
let (mut sink, mut source) = socket.split();
let mut ping = tokio::time::interval(keepalive);
ping.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
ping.tick().await;
loop {
tokio::select! {
frame = source.next() => {
let payload = match frame {
Some(Ok(Frame::Text(text))) => Bytes::from(text),
Some(Ok(Frame::Binary(data))) => data,
Some(Ok(Frame::Ping(_) | Frame::Pong(_) | Frame::Frame(_))) => continue,
Some(Ok(Frame::Close(_))) | None => break,
Some(Err(error)) => {
tracing::debug!(%error, %peer, "websocket read failed");
break;
}
};
match parse_one(payload, &limits) {
Ok(message) => {
if events
.send(Event::Message {
message: Box::new(message),
source: peer,
transport,
id,
#[cfg(feature = "quic")]
quic_reply: None,
})
.await
.is_err()
{
return;
}
}
Err(detail) => {
tracing::debug!(%peer, %detail, "closing on a malformed websocket message");
let _ = events.send(Event::FramingFailed { key: key.clone() }).await;
break;
}
}
},
Some(bytes) = outgoing.recv() => {
if sink.send(frame_for(bytes)).await.is_err() {
break;
}
}
_ = ping.tick() => {
if sink.send(Frame::Ping(Bytes::new())).await.is_err() {
break;
}
}
}
}
let _ = sink.close().await;
}
#[derive(Debug, thiserror::Error)]
enum WsFramingError {
#[error(transparent)]
Sip(#[from] ParseError),
#[error(
"a WebSocket message carries exactly one SIP message (RFC 7118 §5); \
this one held {complete} complete and {trailing} octets of another"
)]
Shape { complete: usize, trailing: usize },
}
fn parse_one(frame: Bytes, limits: &Limits) -> Result<Message, WsFramingError> {
let mut parser = StreamParser::new(*limits);
match parser.push(&frame) {
Ok(mut messages) => {
let trailing = parser.pending();
if trailing == 0
&& messages.len() == 1
&& let Some(message) = messages.pop()
{
return Ok(message);
}
Err(WsFramingError::Shape {
complete: messages.len(),
trailing,
})
}
Err(ParseError::Framing(FramingError::ContentLengthRequired)) => {
parse_datagram(frame, limits).map_err(WsFramingError::from)
}
Err(error) => Err(error.into()),
}
}
fn frame_for(bytes: Bytes) -> Frame {
match tokio_tungstenite::tungstenite::Utf8Bytes::try_from(bytes.clone()) {
Ok(text) => Frame::Text(text),
Err(_) => Frame::Binary(bytes),
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
const OPTIONS: &str = "OPTIONS sip:a@b.com SIP/2.0\r\n\
Via: SIP/2.0/WS df7jal23ls0d.invalid;branch=z9hG4bKx\r\n\
To: <sip:a@b.com>\r\n\
From: <sip:c@d.net>;tag=1\r\n\
Call-ID: x@y\r\n\
CSeq: 1 OPTIONS\r\n\
Content-Length: 0\r\n\r\n";
fn parse(text: &str) -> Result<Message, WsFramingError> {
parse_one(Bytes::copy_from_slice(text.as_bytes()), &Limits::stream())
}
#[test]
fn one_message_in_one_frame_is_parsed() {
parse(OPTIONS).expect("one message in one frame");
}
#[test]
fn a_message_split_across_frames_is_malformed() {
let half = &OPTIONS[..OPTIONS.len() / 2];
let error = parse(half).expect_err("half a message is not a message");
assert!(error.to_string().contains("exactly one"), "{error}");
}
#[test]
fn two_messages_in_one_frame_are_malformed() {
let error = parse(&format!("{OPTIONS}{OPTIONS}")).expect_err("two is not one");
assert!(error.to_string().contains("exactly one"), "{error}");
}
#[test]
fn octets_after_the_message_are_malformed() {
parse(&format!("{OPTIONS}garbage"))
.expect_err("a frame holds one message and nothing else");
}
#[test]
fn a_message_without_content_length_is_accepted() {
let without = OPTIONS.replace("Content-Length: 0\r\n", "");
parse(&without).expect("the frame says where it ends");
}
#[test]
fn a_frame_holding_nothing_like_sip_is_refused() {
parse("hello").expect_err("not a SIP message");
}
const RFC3261_FRAMING_PATHS: [TransportKind; 5] = [
TransportKind::Udp,
TransportKind::Tcp,
TransportKind::Tls,
TransportKind::Ws,
TransportKind::Wss,
];
fn body_limit_refusal(path: TransportKind, frame: &[u8], limits: &Limits) -> ParseError {
match path {
TransportKind::Udp => parse_datagram(Bytes::copy_from_slice(frame), limits)
.expect_err("the datagram body limit must refuse"),
TransportKind::Tcp | TransportKind::Tls => {
let mut parser = StreamParser::new(*limits);
parser
.push(frame)
.expect_err("the stream body limit must refuse")
}
TransportKind::Ws | TransportKind::Wss => {
match parse_one(Bytes::copy_from_slice(frame), limits)
.expect_err("the WebSocket body limit must refuse")
{
WsFramingError::Sip(error) => error,
error @ WsFramingError::Shape { .. } => {
panic!("the SIP limit must run before frame-shape handling: {error}")
}
}
}
TransportKind::Quic => {
panic!(
"QUIC is bounded by its one-stream/one-message reader, not this RFC 3261 table"
)
}
}
}
async fn handshaken_pair(
secure: bool,
client_limits: Limits,
server_limits: Limits,
) -> (
Socket<tokio::io::DuplexStream>,
Socket<tokio::io::DuplexStream>,
) {
let (client_io, server_io) = tokio::io::duplex(4096);
let peer = "127.0.0.1:5060".parse().expect("a peer address");
let (client, server) = tokio::join!(
connect_with_limits(client_io, "example.com", "/", secure, &client_limits),
accept_with_limits(server_io, peer, &server_limits),
);
(
client.expect("the client handshake completes"),
server.expect("the server handshake completes"),
)
}
fn limits_with_message_bound(max_message_bytes: usize) -> Limits {
Limits {
max_message_bytes,
max_body_bytes: max_message_bytes,
..Limits::stream()
}
}
fn assert_message_too_long(
error: &tokio_tungstenite::tungstenite::Error,
expected_size: usize,
expected_limit: usize,
) {
use tokio_tungstenite::tungstenite::error::CapacityError;
assert!(
matches!(
error,
tokio_tungstenite::tungstenite::Error::Capacity(
CapacityError::MessageTooLong { size, max_size }
) if *size == expected_size && *max_size == expected_limit
),
"the handshake did not install the configured decoder bound: {error}"
);
}
#[tokio::test]
async fn client_handshake_holds_the_frame_bound_for_ws_and_wss() {
const HELD: usize = 32;
const SENT: usize = 64;
for secure in [false, true] {
let (mut client, mut server) = handshaken_pair(
secure,
limits_with_message_bound(HELD),
limits_with_message_bound(256),
)
.await;
assert_eq!(client.get_config().max_frame_size, Some(HELD));
assert_eq!(client.get_config().max_message_size, Some(HELD));
server
.send(Frame::binary(vec![0; SENT]))
.await
.expect("the permissive peer sends the probe");
let error = client
.next()
.await
.expect("the probe has a decoder outcome")
.expect_err("an oversized frame is refused before SIP parsing");
assert_message_too_long(&error, SENT, HELD);
}
}
#[tokio::test]
async fn server_handshake_holds_the_frame_bound_for_ws_and_wss() {
const HELD: usize = 32;
const SENT: usize = 64;
for secure in [false, true] {
let (mut client, mut server) = handshaken_pair(
secure,
limits_with_message_bound(256),
limits_with_message_bound(HELD),
)
.await;
assert_eq!(server.get_config().max_frame_size, Some(HELD));
assert_eq!(server.get_config().max_message_size, Some(HELD));
client
.send(Frame::binary(vec![0; SENT]))
.await
.expect("the permissive peer sends the probe");
let error = server
.next()
.await
.expect("the probe has a decoder outcome")
.expect_err("an oversized frame is refused before SIP parsing");
assert_message_too_long(&error, SENT, HELD);
}
}
#[test]
fn pre_allocation_body_and_frame_bounds_hold_on_every_framing_path() {
let limits = Limits {
max_message_bytes: 256,
max_body_bytes: 4,
..Limits::stream()
};
let frame = b"MESSAGE sip:a@b SIP/2.0\r\nContent-Length: 5\r\n\r\n";
for path in RFC3261_FRAMING_PATHS {
assert_eq!(
body_limit_refusal(path, frame, &limits),
ParseError::Limit {
limit: sipx_sip::error::LimitKind::BodyBytes,
value: 5,
},
"{path:?} did not return the typed body-size refusal"
);
if matches!(path, TransportKind::Ws | TransportKind::Wss) {
let config = websocket_config(&limits);
assert_eq!(
config.max_frame_size,
Some(limits.max_message_bytes),
"{path:?} would allocate an oversized frame before SIP parsing"
);
assert_eq!(
config.max_message_size,
Some(limits.max_message_bytes),
"{path:?} would assemble an oversized fragmented message"
);
}
}
}
#[test]
fn body_length_disagreement_is_typed_or_bounded_on_every_framing_path() {
let limits = Limits {
max_message_bytes: 256,
max_body_bytes: 16,
..Limits::stream()
};
let prefix = b"MESSAGE sip:a@b SIP/2.0\r\nContent-Length: 4\r\n\r\n";
let mut short = prefix.to_vec();
short.extend_from_slice(b"abc");
let mut long = prefix.to_vec();
long.extend_from_slice(b"abcde");
for path in RFC3261_FRAMING_PATHS {
match path {
TransportKind::Udp => {
assert!(
matches!(
parse_datagram(Bytes::copy_from_slice(&short), &limits),
Err(ParseError::Framing(FramingError::BodyTruncated))
),
"UDP did not type the short-body refusal"
);
let message = parse_datagram(Bytes::copy_from_slice(&long), &limits)
.expect("UDP ignores octets beyond the declared body");
assert_eq!(message.body().as_ref(), b"abcd");
}
TransportKind::Tcp | TransportKind::Tls => {
let mut short_parser = StreamParser::new(limits);
assert!(
short_parser.push(&short).expect("bounded wait").is_empty(),
"{path:?} read a short body as complete"
);
assert_eq!(
short_parser.pending(),
3,
"{path:?} did not bound pending input"
);
let mut long_parser = StreamParser::new(limits);
let messages = long_parser.push(&long).expect("one complete message");
assert_eq!(
messages.len(),
1,
"{path:?} did not frame exactly one message"
);
assert_eq!(messages[0].body().as_ref(), b"abcd");
assert_eq!(
long_parser.pending(),
1,
"{path:?} read the next message's byte into this body"
);
}
TransportKind::Ws | TransportKind::Wss => {
assert!(
matches!(
parse_one(Bytes::copy_from_slice(&short), &limits),
Err(WsFramingError::Shape {
complete: 0,
trailing: 3
})
),
"{path:?} did not type the short-frame refusal"
);
assert!(
matches!(
parse_one(Bytes::copy_from_slice(&long), &limits),
Err(WsFramingError::Shape {
complete: 1,
trailing: 1
})
),
"{path:?} accepted bytes beyond the declared body"
);
}
TransportKind::Quic => {
panic!(
"QUIC is bounded by its one-stream/one-message reader, not this RFC 3261 table"
)
}
}
}
}
#[test]
fn a_sip_message_travels_as_text() {
assert!(matches!(
frame_for(Bytes::copy_from_slice(OPTIONS.as_bytes())),
Frame::Text(_)
));
}
#[test]
fn a_binary_body_travels_as_binary() {
assert!(matches!(
frame_for(Bytes::from_static(b"MESSAGE sip:a SIP/2.0\r\n\r\n\xff\xfe")),
Frame::Binary(_)
));
}
#[test]
fn the_subprotocol_is_found_however_it_is_offered() {
let mut headers = HeaderMap::new();
headers.append(PROTOCOL_HEADER, HeaderValue::from_static("sip"));
assert!(offers_sip(&headers));
let mut listed = HeaderMap::new();
listed.append(PROTOCOL_HEADER, HeaderValue::from_static("chat, SIP, echo"));
assert!(
offers_sip(&listed),
"comma-separated, and case is not part of it"
);
let mut repeated = HeaderMap::new();
repeated.append(PROTOCOL_HEADER, HeaderValue::from_static("chat"));
repeated.append(PROTOCOL_HEADER, HeaderValue::from_static("sip"));
assert!(offers_sip(&repeated), "repeated headers are one list");
let mut other = HeaderMap::new();
other.append(PROTOCOL_HEADER, HeaderValue::from_static("chat, sipx"));
assert!(!offers_sip(&other), "a longer token is a different token");
assert!(
!offers_sip(&HeaderMap::new()),
"offering none is not offering sip"
);
}
#[test]
fn the_upgrade_asks_for_the_resource_it_was_given() {
let request = upgrade_request("127.0.0.1:8088", "/ws", false).expect("a request");
assert_eq!(request.uri().to_string(), "ws://127.0.0.1:8088/ws");
assert_eq!(request.uri().path(), "/ws");
assert_eq!(
request.headers().get("Host").expect("a Host"),
"127.0.0.1:8088"
);
}
#[test]
fn the_root_is_still_what_a_caller_naming_nothing_asks_for() {
let request = upgrade_request("127.0.0.1:5060", "/", false).expect("a request");
assert_eq!(request.uri().to_string(), "ws://127.0.0.1:5060/");
}
#[test]
fn a_secure_upgrade_keeps_the_resource() {
let request = upgrade_request("sipx.test:443", "/ws", true).expect("a request");
assert_eq!(request.uri().to_string(), "wss://sipx.test:443/ws");
}
#[test]
fn a_query_string_survives_the_handshake() {
let request = upgrade_request("127.0.0.1:8088", "/ws?token=abc", false).expect("a request");
assert_eq!(request.uri().path(), "/ws");
assert_eq!(request.uri().query(), Some("token=abc"));
}
#[test]
fn an_invented_sent_by_is_unresolvable_and_unique() {
let one = invented_sent_by();
assert!(one.ends_with(".invalid"), "{one}");
assert_ne!(one, invented_sent_by());
}
}