use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use futures_util::{SinkExt, StreamExt, ready};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::tungstenite::Error as WsError;
use tokio_tungstenite::tungstenite::Message;
use crate::error::{ConnectError, ErrorKind};
const MAX_WS_MESSAGE_SIZE: usize = 64 << 20; const MAX_WS_FRAME_SIZE: usize = 16 << 20;
pub struct WsByteStream<S> {
inner: WebSocketStream<S>,
leftover: bytes::Bytes,
eof: bool,
}
impl<S> std::fmt::Debug for WsByteStream<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WsByteStream")
.field("leftover", &self.leftover.len())
.field("eof", &self.eof)
.finish_non_exhaustive()
}
}
impl<S> WsByteStream<S> {
pub fn new(inner: WebSocketStream<S>) -> Self {
Self {
inner,
leftover: bytes::Bytes::new(),
eof: false,
}
}
pub fn get_ref(&self) -> &WebSocketStream<S> {
&self.inner
}
pub fn get_mut(&mut self) -> &mut WebSocketStream<S> {
&mut self.inner
}
pub fn into_inner(self) -> WebSocketStream<S> {
self.inner
}
fn drain_leftover(&mut self, buf: &mut ReadBuf<'_>) -> bool {
if self.leftover.is_empty() {
return false;
}
let n = self.leftover.len().min(buf.remaining());
buf.put_slice(&self.leftover[..n]);
self.leftover = self.leftover.slice(n..);
true
}
}
fn ws_to_io(e: WsError) -> io::Error {
match e {
WsError::Io(io) => io,
other => io::Error::other(other),
}
}
impl<S> AsyncRead for WsByteStream<S>
where
S: AsyncRead + AsyncWrite + Unpin + Send,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let this = self.get_mut();
if this.drain_leftover(buf) {
return Poll::Ready(Ok(()));
}
if this.eof {
return Poll::Ready(Ok(()));
}
loop {
match ready!(this.inner.poll_next_unpin(cx)) {
Some(Ok(Message::Binary(data))) => {
if data.is_empty() {
continue;
}
this.leftover = data;
this.drain_leftover(buf);
return Poll::Ready(Ok(()));
}
Some(Ok(Message::Ping(_)))
| Some(Ok(Message::Pong(_)))
| Some(Ok(Message::Text(_)))
| Some(Ok(Message::Frame(_))) => continue,
Some(Ok(Message::Close(_))) | None => {
this.eof = true;
return Poll::Ready(Ok(()));
}
Some(Err(e)) => return Poll::Ready(Err(ws_to_io(e))),
}
}
}
}
impl<S> AsyncWrite for WsByteStream<S>
where
S: AsyncRead + AsyncWrite + Unpin + Send,
{
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
if buf.is_empty() {
return Poll::Ready(Ok(0));
}
let this = self.get_mut();
ready!(this.inner.poll_ready_unpin(cx)).map_err(ws_to_io)?;
let msg = Message::Binary(bytes::Bytes::copy_from_slice(buf));
match this.inner.start_send_unpin(msg) {
Ok(()) => Poll::Ready(Ok(buf.len())),
Err(e) => Poll::Ready(Err(ws_to_io(e))),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let this = self.get_mut();
this.inner.poll_flush_unpin(cx).map_err(ws_to_io)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let this = self.get_mut();
this.inner.poll_close_unpin(cx).map_err(ws_to_io)
}
}
pub async fn connect_ws<S>(stream: S, url: &str) -> Result<WsByteStream<S>, ConnectError>
where
S: AsyncRead + AsyncWrite + Unpin + Send,
{
use http::Uri;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
let uri: Uri = url.parse().map_err(|e: http::uri::InvalidUri| {
ConnectError::msg(
ErrorKind::ProtocolViolation,
format!("invalid ws url: {url}"),
)
.with_source(e)
})?;
let mut request = uri.into_client_request().map_err(|e| {
ConnectError::msg(ErrorKind::ProtocolViolation, "invalid websocket request")
.with_source(ws_to_io(e))
})?;
request.headers_mut().insert(
http::header::SEC_WEBSOCKET_PROTOCOL,
http::HeaderValue::from_static("amqp"),
);
let config = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
.max_message_size(Some(MAX_WS_MESSAGE_SIZE))
.max_frame_size(Some(MAX_WS_FRAME_SIZE));
let (ws, response) = tokio_tungstenite::client_async_with_config(request, stream, Some(config))
.await
.map_err(|e| match e {
WsError::Io(io) => ConnectError::new(ErrorKind::Io).with_source(io),
other => ConnectError::msg(ErrorKind::ProtocolViolation, "websocket handshake failed")
.with_source(other),
})?;
let agreed = response
.headers()
.get(http::header::SEC_WEBSOCKET_PROTOCOL)
.and_then(|v| v.to_str().ok());
match agreed {
Some(p) if p.eq_ignore_ascii_case("amqp") => {}
other => {
return Err(ConnectError::msg(
ErrorKind::ProtocolViolation,
format!("server did not select the `amqp` websocket subprotocol (got {other:?})"),
));
}
}
Ok(WsByteStream::new(ws))
}