use std::{net::SocketAddr, sync::Arc, time::Duration};
use chia_protocol::ChiaProtocolMessage;
use chia_traits::Streamable;
use futures_util::{SinkExt, StreamExt};
use tokio::{
net::TcpStream,
sync::{mpsc, oneshot, Mutex},
task::JoinHandle,
};
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
use tracing::{debug, warn};
use crate::{
rate_limit::{Admission, OpcodeRateLimiter, OpcodeRateLimits},
request_map::RequestMap,
Bytes, DigMessage, LinkError,
};
#[cfg(any(feature = "native-tls", feature = "rustls"))]
use tokio_tungstenite::Connector;
const INBOUND_CHANNEL_CAPACITY: usize = 32;
const RATE_LIMIT_BACKOFF: Duration = Duration::from_secs(1);
const RATE_LIMIT_WINDOW_SECONDS: u64 = 60;
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct LinkOptions {
pub rate_limit_factor: f64,
pub send_timeout: Duration,
pub request_timeout: Duration,
}
impl Default for LinkOptions {
fn default() -> Self {
Self {
rate_limit_factor: 0.6,
send_timeout: Duration::from_secs(RATE_LIMIT_WINDOW_SECONDS * 2),
request_timeout: Duration::from_secs(60),
}
}
}
type BoxedSink =
Box<dyn futures_util::Sink<tungstenite::Message, Error = tungstenite::Error> + Send + Unpin>;
type BoxedStream = Box<
dyn futures_util::Stream<Item = Result<tungstenite::Message, tungstenite::Error>>
+ Send
+ Unpin,
>;
#[derive(Debug, Clone)]
pub struct DigLink(Arc<LinkInner>);
struct LinkInner {
sink: Mutex<BoxedSink>,
inbound_handle: JoinHandle<()>,
requests: Arc<RequestMap>,
socket_addr: SocketAddr,
outbound_rate_limiter: Mutex<OpcodeRateLimiter>,
options: LinkOptions,
}
impl std::fmt::Debug for LinkInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DigLink")
.field("socket_addr", &self.socket_addr)
.finish_non_exhaustive()
}
}
impl Drop for LinkInner {
fn drop(&mut self) {
self.inbound_handle.abort();
}
}
impl DigLink {
#[cfg(any(feature = "native-tls", feature = "rustls"))]
pub async fn connect(
socket_addr: SocketAddr,
connector: Connector,
options: LinkOptions,
) -> Result<(Self, mpsc::Receiver<DigMessage>), LinkError> {
Self::connect_full_uri(&format!("wss://{socket_addr}/ws"), connector, options).await
}
#[cfg(any(feature = "native-tls", feature = "rustls"))]
pub async fn connect_full_uri(
uri: &str,
connector: Connector,
options: LinkOptions,
) -> Result<(Self, mpsc::Receiver<DigMessage>), LinkError> {
let (ws, _) =
tokio_tungstenite::connect_async_tls_with_config(uri, None, false, Some(connector))
.await?;
Self::from_websocket(ws, options)
}
pub fn from_websocket(
ws: WebSocketStream<MaybeTlsStream<TcpStream>>,
options: LinkOptions,
) -> Result<(Self, mpsc::Receiver<DigMessage>), LinkError> {
let socket_addr = peer_addr_of(&ws)?;
let (sink, stream) = ws.split();
Ok(Self::from_parts(
Box::new(sink),
Box::new(stream),
socket_addr,
options,
))
}
pub fn from_server_websocket<S>(
ws: WebSocketStream<S>,
socket_addr: SocketAddr,
options: LinkOptions,
) -> (Self, mpsc::Receiver<DigMessage>)
where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
{
let (sink, stream) = ws.split();
Self::from_parts(Box::new(sink), Box::new(stream), socket_addr, options)
}
fn from_parts(
sink: BoxedSink,
stream: BoxedStream,
socket_addr: SocketAddr,
options: LinkOptions,
) -> (Self, mpsc::Receiver<DigMessage>) {
let (sender, receiver) = mpsc::channel(INBOUND_CHANNEL_CAPACITY);
let requests = Arc::new(RequestMap::new());
let requests_for_reader = requests.clone();
let inbound_handle = tokio::spawn(async move {
if let Err(error) = read_inbound(stream, sender, requests_for_reader).await {
debug!("dig link inbound loop ended: {error}");
}
});
let link = Self(Arc::new(LinkInner {
sink: Mutex::new(sink),
inbound_handle,
requests,
socket_addr,
outbound_rate_limiter: Mutex::new(OpcodeRateLimiter::new(
RATE_LIMIT_WINDOW_SECONDS,
options.rate_limit_factor,
OpcodeRateLimits::default(),
)),
options,
}));
(link, receiver)
}
#[must_use]
pub fn socket_addr(&self) -> SocketAddr {
self.0.socket_addr
}
pub async fn send<T>(&self, body: T) -> Result<(), LinkError>
where
T: Streamable + ChiaProtocolMessage,
{
self.send_message(DigMessage::new(
opcode_of::<T>()?,
None,
body.to_bytes()?.into(),
))
.await
}
pub async fn send_dig(&self, opcode: u8, data: Bytes) -> Result<(), LinkError> {
self.send_message(DigMessage::new(opcode, None, data)).await
}
pub async fn send_message(&self, message: DigMessage) -> Result<(), LinkError> {
let deadline = tokio::time::Instant::now() + self.0.options.send_timeout;
loop {
match self.0.outbound_rate_limiter.lock().await.admit(&message) {
Admission::Admitted => break,
Admission::Unsendable => {
return Err(LinkError::Unsendable(message.msg_type, message.data.len()))
}
Admission::Deferred => {}
}
if tokio::time::Instant::now() + RATE_LIMIT_BACKOFF > deadline {
return Err(LinkError::SendTimeout(message.msg_type));
}
tokio::time::sleep(RATE_LIMIT_BACKOFF).await;
}
self.0
.sink
.lock()
.await
.send(tungstenite::Message::Binary(message.to_bytes()))
.await?;
Ok(())
}
pub async fn request_raw<T>(&self, body: T) -> Result<DigMessage, LinkError>
where
T: Streamable + ChiaProtocolMessage,
{
self.request_message(opcode_of::<T>()?, body.to_bytes()?.into())
.await
}
pub async fn request_dig(&self, opcode: u8, data: Bytes) -> Result<DigMessage, LinkError> {
self.request_message(opcode, data).await
}
pub async fn request_infallible<T, B>(&self, body: B) -> Result<T, LinkError>
where
T: Streamable + ChiaProtocolMessage,
B: Streamable + ChiaProtocolMessage,
{
let expected = opcode_of::<T>()?;
let message = self.request_raw(body).await?;
if message.msg_type != expected {
return Err(LinkError::InvalidResponse(vec![expected], message.msg_type));
}
Ok(T::from_bytes(&message.data)?)
}
pub async fn request_fallible<T, E, B>(&self, body: B) -> Result<Result<T, E>, LinkError>
where
T: Streamable + ChiaProtocolMessage,
E: Streamable + ChiaProtocolMessage,
B: Streamable + ChiaProtocolMessage,
{
let (accepted, rejected) = (opcode_of::<T>()?, opcode_of::<E>()?);
let message = self.request_raw(body).await?;
if message.msg_type == accepted {
Ok(Ok(T::from_bytes(&message.data)?))
} else if message.msg_type == rejected {
Ok(Err(E::from_bytes(&message.data)?))
} else {
Err(LinkError::InvalidResponse(
vec![accepted, rejected],
message.msg_type,
))
}
}
async fn request_message(&self, opcode: u8, data: Bytes) -> Result<DigMessage, LinkError> {
let (sender, receiver) = oneshot::channel();
let id = self.0.requests.insert(sender).await;
if let Err(error) = self
.send_message(DigMessage::new(opcode, Some(id), data))
.await
{
self.0.requests.remove(id).await;
return Err(error);
}
match tokio::time::timeout(self.0.options.request_timeout, receiver).await {
Ok(received) => Ok(received?),
Err(_) => {
self.0.requests.remove(id).await;
Err(LinkError::RequestTimeout(opcode))
}
}
}
pub async fn close(&self) -> Result<(), LinkError> {
self.0.sink.lock().await.close().await?;
Ok(())
}
}
fn opcode_of<T: ChiaProtocolMessage>() -> Result<u8, LinkError> {
T::msg_type()
.to_bytes()?
.first()
.copied()
.ok_or(LinkError::MalformedOpcode)
}
fn peer_addr_of(ws: &WebSocketStream<MaybeTlsStream<TcpStream>>) -> Result<SocketAddr, LinkError> {
let addr = match ws.get_ref() {
#[cfg(feature = "native-tls")]
MaybeTlsStream::NativeTls(tls) => tls.get_ref().get_ref().get_ref().peer_addr()?,
#[cfg(feature = "rustls")]
MaybeTlsStream::Rustls(tls) => tls.get_ref().0.peer_addr()?,
MaybeTlsStream::Plain(plain) => plain.peer_addr()?,
_ => return Err(LinkError::UnsupportedTls),
};
Ok(addr)
}
async fn read_inbound(
mut stream: BoxedStream,
sender: mpsc::Sender<DigMessage>,
requests: Arc<RequestMap>,
) -> Result<(), LinkError> {
use tungstenite::Message::{Binary, Close, Frame, Ping, Pong, Text};
while let Some(frame) = stream.next().await {
match frame? {
Close(..) => break,
Ping(..) | Pong(..) | Frame(..) => {}
Text(text) => warn!("dig link received an unexpected text frame: {text}"),
Binary(binary) => {
let Some(message) = DigMessage::from_bytes_owned(binary) else {
warn!("dig link skipped a malformed frame");
continue;
};
let unmatched = match message.id {
Some(id) => match requests.remove(id).await {
Some(waiter) => {
waiter.send(message);
continue;
}
None => message,
},
None => message,
};
if let Err(mpsc::error::TrySendError::Full(dropped)) = sender.try_send(unmatched) {
warn!(
"dig link dropped an inbound frame (opcode {}): the application is not \
keeping up",
dropped.msg_type
);
}
}
}
}
Ok(())
}