use eyre::{Context, Result, bail, eyre};
use serde::Serialize;
use serde::de::DeserializeOwned;
use tracing::*;
#[cfg(feature = "ws-client")]
use nago_wss::conn::Connection;
#[cfg(feature = "ws-client")]
use nago_wss::proto::message::{CloseFrame as NagoCloseFrame, Limits, Message as NagoMessage};
#[cfg(feature = "ws-client")]
use nago_wss::stream::{ByteStream, StreamExt};
#[cfg(feature = "ws-client")]
use nagoya::reactor::{Addr, Handle, TcpStream, connect_any, resolve};
#[cfg(feature = "ws-client")]
use std::sync::atomic::{AtomicU64, Ordering};
use crate::libs::log::LogLevel;
use crate::libs::ws::WireMessage as Message;
#[cfg(feature = "ws-client")]
use crate::libs::ws::{CloseFrame, Utf8Bytes};
use crate::libs::ws::{WsLogResponse, WsRequest, WsRequestGeneric, WsResponseGeneric};
#[derive(Debug, Clone, Copy, Default)]
#[cfg(feature = "ws-client")]
pub enum WsVersionMode {
#[default]
Http1Only,
#[deprecated(note = "HTTP/2 extended CONNECT was removed; this connects over HTTP/1.1")]
Http2Only,
#[deprecated(note = "HTTP/2 extended CONNECT was removed; this connects over HTTP/1.1")]
Auto,
}
#[cfg(feature = "ws-client")]
pub struct WsConnectResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub protocol: Option<String>,
}
enum WsStream {
#[cfg(feature = "ws-client")]
Plain(Box<Connection<TcpStream>>),
#[cfg(all(feature = "ws-client", feature = "ws-client-tls"))]
Secure(Box<Connection<nago_wss::tls::TlsStream<TcpStream>>>),
Message(Box<dyn crate::libs::ws::MessageStream>),
}
pub struct WsClient {
stream: WsStream,
seq: u32,
}
impl WsClient {
#[cfg(feature = "ws-client")]
pub async fn new(
connect_addr: &str,
protocol_header: &str,
headers: Option<Vec<(&'static str, &'static str)>>,
handle: &Handle,
) -> Result<(Self, WsConnectResponse)> {
let mut builder = WsClientBuilder::new().protocol_header(protocol_header);
if let Some(headers) = headers {
builder = builder.headers(headers);
}
builder.build(connect_addr, handle).await
}
pub fn from_stream(stream: Box<dyn crate::libs::ws::MessageStream>) -> Self {
Self {
stream: WsStream::Message(stream),
seq: 0,
}
}
async fn stream_send(&mut self, msg: Message) -> Result<()> {
match &mut self.stream {
#[cfg(feature = "ws-client")]
WsStream::Plain(c) => send_to(c, msg).await?,
#[cfg(all(feature = "ws-client", feature = "ws-client-tls"))]
WsStream::Secure(c) => send_to(c, msg).await?,
WsStream::Message(s) => s
.send(msg)
.await
.map_err(|err| eyre!("message stream send failed: {err}"))?,
}
Ok(())
}
async fn stream_next(&mut self) -> Option<Result<Message>> {
match &mut self.stream {
#[cfg(feature = "ws-client")]
WsStream::Plain(c) => next_from(c).await,
#[cfg(all(feature = "ws-client", feature = "ws-client-tls"))]
WsStream::Secure(c) => next_from(c).await,
WsStream::Message(s) => s
.recv()
.await
.map(|res| res.map_err(|err| eyre!("message stream recv failed: {err}"))),
}
}
async fn stream_close(&mut self) -> Result<()> {
match &mut self.stream {
#[cfg(feature = "ws-client")]
WsStream::Plain(c) => close_to(c).await?,
#[cfg(all(feature = "ws-client", feature = "ws-client-tls"))]
WsStream::Secure(c) => close_to(c).await?,
WsStream::Message(s) => {
let _ = s.send(Message::Close(None)).await;
}
}
Ok(())
}
pub async fn send_req(&mut self, method: u32, params: impl Serialize) -> Result<()> {
self.seq += 1;
let req = serde_json::to_string(&WsRequestGeneric {
method,
seq: self.seq,
params,
})?;
debug!(
method,
seq = self.seq,
bytes = req.len(),
"sending WebSocket request"
);
self.stream_send(Message::Text(req.into())).await
}
pub async fn send_raw(&mut self, request_bytes: &[u8]) -> Result<()> {
let text = std::str::from_utf8(request_bytes).context("Invalid UTF-8 in request bytes")?;
self.stream_send(Message::Text(text.into())).await
}
pub async fn recv_raw(&mut self) -> Result<serde_json::Value> {
let msg = self
.stream_next()
.await
.ok_or(eyre!("Connection closed"))??;
let text = match msg {
Message::Text(text) => text,
other => {
#[allow(unreachable_patterns)]
let kind = match &other {
Message::Text(_) => "text",
Message::Binary(_) => "binary",
Message::Ping(_) => "ping",
Message::Pong(_) => "pong",
Message::Close(_) => "close",
_ => "unknown",
};
bail!("Expected text message, got {kind}")
}
};
debug!(bytes = text.len(), "received raw WebSocket response");
let resp: serde_json::Value = serde_json::from_str(&text)?;
Ok(resp)
}
pub async fn recv_resp<T: DeserializeOwned>(&mut self) -> Result<T> {
loop {
let msg = self
.stream_next()
.await
.ok_or(eyre!("Connection closed"))??;
match msg {
Message::Text(text) => {
debug!(bytes = text.len(), "received WebSocket response");
let resp: WsResponseGeneric<T> = serde_json::from_str(&text)?;
match resp {
WsResponseGeneric::Immediate(resp) if resp.seq == self.seq => {
return Ok(resp.params);
}
WsResponseGeneric::Immediate(resp) => {
bail!("Seq mismatch this: {} got: {}", self.seq, resp.seq)
}
WsResponseGeneric::Stream(_) => {
debug!("expect immediate response, got stream")
}
WsResponseGeneric::Forwarded(_) => {
debug!("expect immediate response, got forwarded")
}
WsResponseGeneric::Close => {
bail!("unreachable")
}
WsResponseGeneric::Log(WsLogResponse {
log_id,
level,
message,
..
}) => match level {
LogLevel::Error => error!(?log_id, "{}", message),
LogLevel::Warn => warn!(?log_id, "{}", message),
LogLevel::Info => info!(?log_id, "{}", message),
LogLevel::Debug => debug!(?log_id, "{}", message),
LogLevel::Trace => trace!(?log_id, "{}", message),
LogLevel::Detail => trace!(?log_id, "{}", message),
LogLevel::Off => {}
},
WsResponseGeneric::Error(err) => {
bail!("Error: {} {:?}", err.code, err.params)
}
}
}
Message::Close(_) => {
self.stream_close().await?;
bail!("Connection closed")
}
_ => {}
}
}
}
pub async fn request<T: WsRequest>(&mut self, params: T) -> Result<T::Response> {
self.send_req(T::METHOD_ID, params).await?;
self.recv_resp().await
}
pub async fn close(mut self) -> Result<()> {
self.stream_close().await
}
}
#[cfg(feature = "ws-client")]
async fn send_to<S: ByteStream + StreamExt>(
conn: &mut Connection<S>,
message: Message,
) -> Result<()> {
conn.write(into_nago(message))
.await
.map_err(|err| eyre!("websocket write failed: {err}"))
}
#[cfg(feature = "ws-client")]
async fn next_from<S: ByteStream + StreamExt>(conn: &mut Connection<S>) -> Option<Result<Message>> {
match conn.read().await {
Ok(None) => None,
Ok(Some(NagoMessage::Ping(payload))) => {
if let Err(err) = conn.pong(payload.clone()).await {
return Some(Err(eyre!("failed to answer ping: {err}")));
}
Some(Ok(Message::Ping(payload)))
}
Ok(Some(other)) => Some(Ok(from_nago(other))),
Err(err) => Some(Err(eyre!("websocket read failed: {err}"))),
}
}
#[cfg(feature = "ws-client")]
async fn close_to<S: ByteStream + StreamExt>(conn: &mut Connection<S>) -> Result<()> {
conn.close(None)
.await
.map_err(|err| eyre!("websocket close failed: {err}"))
}
#[cfg(feature = "ws-client")]
fn from_nago(message: NagoMessage) -> Message {
match message {
NagoMessage::Text(payload) => {
Message::Text(unsafe { Utf8Bytes::from_bytes_unchecked(payload) })
}
NagoMessage::Binary(payload) => Message::Binary(payload),
NagoMessage::Ping(payload) => Message::Ping(payload),
NagoMessage::Pong(payload) => Message::Pong(payload),
NagoMessage::Close(frame) => Message::Close(frame.map(|frame| CloseFrame {
code: frame.code.0,
reason: unsafe { Utf8Bytes::from_bytes_unchecked(frame.reason) },
})),
}
}
#[cfg(feature = "ws-client")]
fn into_nago(message: Message) -> NagoMessage {
match message {
Message::Text(text) => NagoMessage::Text(text.into_bytes()),
Message::Binary(payload) => NagoMessage::Binary(payload),
Message::Ping(payload) => NagoMessage::Ping(payload),
Message::Pong(payload) => NagoMessage::Pong(payload),
Message::Close(frame) => NagoMessage::Close(frame.map(|frame| NagoCloseFrame {
code: nago_wss::CloseCode(frame.code),
reason: frame.reason.into_bytes(),
})),
}
}
#[cfg(feature = "ws-client")]
pub struct WsTarget {
secure: bool,
host: String,
host_header: String,
path: String,
addrs: Vec<Addr>,
}
#[cfg(feature = "ws-client")]
impl WsTarget {
pub fn resolve(url: &str) -> Result<Self> {
let url = nago_wss::client::Url::parse(url)
.map_err(|err| eyre!("invalid WebSocket URL {url}: {err}"))?;
#[cfg(not(feature = "ws-client-tls"))]
if url.secure {
bail!(
"wss:// needs the `ws-client-tls` feature; this build speaks plain ws:// only \
(the fleet terminates TLS at the edge)"
);
}
let host_header = url.host_header();
debug!(host = %url.host, port = url.port, secure = url.secure, "resolving WebSocket host");
let addrs = resolve(&url.host, url.port)
.map_err(|err| eyre!("DNS resolution failed for {}: {err}", url.host))?;
debug!(?addrs, "resolved WebSocket host");
Ok(Self {
secure: url.secure,
host: url.host,
host_header,
path: url.path,
addrs,
})
}
pub fn is_secure(&self) -> bool {
self.secure
}
}
#[cfg(feature = "ws-client")]
pub struct WsClientBuilder {
protocol_header: String,
headers: Vec<(&'static str, &'static str)>,
#[cfg_attr(not(feature = "ws-client-tls"), allow(dead_code))]
danger_accept_invalid_certs: bool,
}
#[cfg(feature = "ws-client")]
impl WsClientBuilder {
pub fn new() -> Self {
Self {
protocol_header: String::new(),
headers: Vec::new(),
danger_accept_invalid_certs: false,
}
}
#[allow(deprecated)]
pub fn mode(self, mode: WsVersionMode) -> Self {
if !matches!(mode, WsVersionMode::Http1Only) {
warn!(
?mode,
"HTTP/2 extended CONNECT was removed from this client; connecting over HTTP/1.1"
);
}
self
}
pub fn protocol_header(mut self, protocol: impl Into<String>) -> Self {
self.protocol_header = protocol.into();
self
}
pub fn header(mut self, key: &'static str, value: &'static str) -> Self {
self.headers.push((key, value));
self
}
pub fn headers(mut self, headers: Vec<(&'static str, &'static str)>) -> Self {
self.headers.extend(headers);
self
}
pub fn danger_accept_invalid_certs(mut self) -> Self {
self.danger_accept_invalid_certs = true;
self
}
pub async fn build(
self,
connect_addr: &str,
handle: &Handle,
) -> Result<(WsClient, WsConnectResponse)> {
let target = WsTarget::resolve(connect_addr)?;
self.connect(&target, handle).await
}
pub async fn connect(
self,
target: &WsTarget,
handle: &Handle,
) -> Result<(WsClient, WsConnectResponse)> {
let protocols: Vec<&str> = if self.protocol_header.is_empty() {
Vec::new()
} else {
vec![self.protocol_header.as_str()]
};
let headers: Vec<(&str, &str)> = self.headers.iter().map(|(k, v)| (*k, *v)).collect();
debug!(
host = %target.host,
secure = target.secure,
has_protocol_header = !protocols.is_empty(),
additional_header_count = headers.len(),
"connecting WebSocket client"
);
if target.secure {
#[cfg(feature = "ws-client-tls")]
{
return connect_secure(
target,
handle,
&protocols,
&headers,
self.danger_accept_invalid_certs,
)
.await;
}
#[cfg(not(feature = "ws-client-tls"))]
{
bail!("wss:// needs the `ws-client-tls` feature");
}
}
let stream = connect_any(&target.addrs, handle)
.await
.map_err(|err| eyre!("TCP connect failed for {:?}: {err}", target.addrs))?;
let (connection, protocol) = nago_wss::upgrade::connect(
stream,
&target.path,
&target.host_header,
&protocols,
&headers,
handshake_entropy(),
Limits::default(),
)
.await
.map_err(|err| eyre!("WebSocket upgrade failed: {err}"))?;
Ok((
WsClient {
stream: WsStream::Plain(Box::new(connection)),
seq: 0,
},
connect_response(protocol),
))
}
}
#[cfg(feature = "ws-client")]
impl Default for WsClientBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "ws-client")]
fn connect_response(protocol: Option<String>) -> WsConnectResponse {
let headers = match &protocol {
Some(protocol) => vec![("sec-websocket-protocol".to_string(), protocol.clone())],
None => Vec::new(),
};
WsConnectResponse {
status: 101,
headers,
protocol,
}
}
#[cfg(feature = "ws-client")]
fn handshake_entropy() -> [u8; 16] {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |value| value.as_nanos() as u64);
let counted = COUNTER.fetch_add(1, Ordering::Relaxed);
let local = 0u8;
let address = core::ptr::addr_of!(local) as u64;
let mut out = [0u8; 16];
out[..8].copy_from_slice(&nanos.to_ne_bytes());
out[8..].copy_from_slice(&(counted ^ address).to_ne_bytes());
out
}
#[cfg(all(feature = "ws-client", feature = "ws-client-tls"))]
async fn connect_secure(
target: &WsTarget,
handle: &Handle,
protocols: &[&str],
headers: &[(&str, &str)],
danger_accept_invalid_certs: bool,
) -> Result<(WsClient, WsConnectResponse)> {
use nago_wss::tls::rustls_pki_types::ServerName;
use nago_wss::tls::{TlsStream, rustls};
use std::sync::Arc;
let stream = connect_any(&target.addrs, handle)
.await
.map_err(|err| eyre!("TCP connect failed for {:?}: {err}", target.addrs))?;
let config: Arc<rustls::ClientConfig> = if danger_accept_invalid_certs {
Arc::new(make_dangerous_tls_config())
} else {
nago_wss::tls::default_client_config()
};
let name = ServerName::try_from(target.host.clone())
.map_err(|_| eyre!("invalid TLS server name: {}", target.host))?;
let session = rustls::ClientConnection::new(config, name)
.map_err(|err| eyre!("TLS session setup failed: {err}"))?;
let mut tls = TlsStream::client(stream, session);
tls.handshake()
.await
.map_err(|err| eyre!("TLS handshake failed: {err}"))?;
let (connection, protocol) = nago_wss::upgrade::connect(
tls,
&target.path,
&target.host_header,
protocols,
headers,
handshake_entropy(),
Limits::default(),
)
.await
.map_err(|err| eyre!("WebSocket upgrade failed: {err}"))?;
Ok((
WsClient {
stream: WsStream::Secure(Box::new(connection)),
seq: 0,
},
connect_response(protocol),
))
}
#[cfg(all(feature = "ws-client", feature = "ws-client-tls"))]
fn make_dangerous_tls_config() -> nago_wss::tls::rustls::ClientConfig {
use nago_wss::tls::rustls;
use std::sync::Arc;
rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(AcceptAllVerifier))
.with_no_client_auth()
}
#[derive(Debug)]
#[cfg(all(feature = "ws-client", feature = "ws-client-tls"))]
struct AcceptAllVerifier;
#[cfg(all(feature = "ws-client", feature = "ws-client-tls"))]
impl nago_wss::tls::rustls::client::danger::ServerCertVerifier for AcceptAllVerifier {
fn verify_server_cert(
&self,
_end_entity: &nago_wss::tls::rustls_pki_types::CertificateDer<'_>,
_intermediates: &[nago_wss::tls::rustls_pki_types::CertificateDer<'_>],
_server_name: &nago_wss::tls::rustls_pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: nago_wss::tls::rustls_pki_types::UnixTime,
) -> std::result::Result<
nago_wss::tls::rustls::client::danger::ServerCertVerified,
nago_wss::tls::rustls::Error,
> {
Ok(nago_wss::tls::rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &nago_wss::tls::rustls_pki_types::CertificateDer<'_>,
_dss: &nago_wss::tls::rustls::DigitallySignedStruct,
) -> std::result::Result<
nago_wss::tls::rustls::client::danger::HandshakeSignatureValid,
nago_wss::tls::rustls::Error,
> {
Ok(nago_wss::tls::rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &nago_wss::tls::rustls_pki_types::CertificateDer<'_>,
_dss: &nago_wss::tls::rustls::DigitallySignedStruct,
) -> std::result::Result<
nago_wss::tls::rustls::client::danger::HandshakeSignatureValid,
nago_wss::tls::rustls::Error,
> {
Ok(nago_wss::tls::rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<nago_wss::tls::rustls::SignatureScheme> {
use nago_wss::tls::rustls::SignatureScheme;
vec![
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::ECDSA_NISTP384_SHA384,
SignatureScheme::RSA_PSS_SHA256,
SignatureScheme::RSA_PSS_SHA384,
SignatureScheme::RSA_PSS_SHA512,
SignatureScheme::RSA_PKCS1_SHA256,
SignatureScheme::RSA_PKCS1_SHA384,
SignatureScheme::RSA_PKCS1_SHA512,
]
}
}