blob: 7d47fe530f6611bf81a4797f86a5fc8a7fc4f93b [file]
use crate::binary::binary_client::BinaryClient;
use crate::binary::{BinaryTransport, ClientState};
use crate::client::{AutoLogin, Client, Credentials, PersonalAccessTokenClient, UserClient};
use crate::command::Command;
use crate::diagnostic::DiagnosticEvent;
use crate::error::IggyError;
use crate::quic::config::QuicClientConfig;
use crate::utils::duration::IggyDuration;
use crate::utils::timestamp::IggyTimestamp;
use async_broadcast::{broadcast, Receiver, Sender};
use async_trait::async_trait;
use bytes::Bytes;
use quinn::crypto::rustls::QuicClientConfig as QuinnQuicClientConfig;
use quinn::{ClientConfig, Connection, Endpoint, IdleTimeout, RecvStream, VarInt};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::crypto::CryptoProvider;
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{DigitallySignedStruct, Error, SignatureScheme};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::sleep;
use tracing::{error, info, trace, warn};
const REQUEST_INITIAL_BYTES_LENGTH: usize = 4;
const RESPONSE_INITIAL_BYTES_LENGTH: usize = 8;
const NAME: &str = "Iggy";
/// QUIC client for interacting with the Iggy API.
#[derive(Debug)]
pub struct QuicClient {
pub(crate) endpoint: Endpoint,
pub(crate) connection: Mutex<Option<Connection>>,
pub(crate) config: Arc<QuicClientConfig>,
pub(crate) server_address: SocketAddr,
pub(crate) state: Mutex<ClientState>,
events: (Sender<DiagnosticEvent>, Receiver<DiagnosticEvent>),
connected_at: Mutex<Option<IggyTimestamp>>,
}
unsafe impl Send for QuicClient {}
unsafe impl Sync for QuicClient {}
impl Default for QuicClient {
fn default() -> Self {
QuicClient::create(Arc::new(QuicClientConfig::default())).unwrap()
}
}
#[async_trait]
impl Client for QuicClient {
async fn connect(&self) -> Result<(), IggyError> {
QuicClient::connect(self).await
}
async fn disconnect(&self) -> Result<(), IggyError> {
QuicClient::disconnect(self).await
}
async fn subscribe_events(&self) -> Receiver<DiagnosticEvent> {
self.events.1.clone()
}
}
#[async_trait]
impl BinaryTransport for QuicClient {
async fn get_state(&self) -> ClientState {
*self.state.lock().await
}
async fn set_state(&self, state: ClientState) {
*self.state.lock().await = state;
}
async fn send_with_response<T: Command>(&self, command: &T) -> Result<Bytes, IggyError> {
command.validate()?;
self.send_raw_with_response(command.code(), command.to_bytes())
.await
}
async fn send_raw_with_response(&self, code: u32, payload: Bytes) -> Result<Bytes, IggyError> {
let result = self.send_raw(code, payload.clone()).await;
if result.is_ok() {
return result;
}
let error = result.unwrap_err();
if !matches!(
error,
IggyError::Disconnected | IggyError::EmptyResponse | IggyError::Unauthenticated
) {
return Err(error);
}
if !self.config.reconnection.enabled {
return Err(IggyError::Disconnected);
}
self.disconnect().await?;
info!("Reconnecting to the server...");
self.connect().await?;
self.send_raw(code, payload).await
}
async fn publish_event(&self, event: DiagnosticEvent) {
if let Err(error) = self.events.0.broadcast(event).await {
error!("Failed to send a QUIC diagnostic event: {error}");
}
}
}
impl BinaryClient for QuicClient {}
impl QuicClient {
/// Creates a new QUIC client for the provided client and server addresses.
pub fn new(
client_address: &str,
server_address: &str,
server_name: &str,
validate_certificate: bool,
auto_sign_in: AutoLogin,
) -> Result<Self, IggyError> {
Self::create(Arc::new(QuicClientConfig {
client_address: client_address.to_string(),
server_address: server_address.to_string(),
server_name: server_name.to_string(),
validate_certificate,
auto_login: auto_sign_in,
..Default::default()
}))
}
/// Create a new QUIC client for the provided configuration.
pub fn create(config: Arc<QuicClientConfig>) -> Result<Self, IggyError> {
let server_address = config.server_address.parse::<SocketAddr>()?;
let client_address = if server_address.is_ipv6()
&& config.client_address == QuicClientConfig::default().client_address
{
"[::1]:0"
} else {
&config.client_address
}
.parse::<SocketAddr>()?;
let quic_config = configure(&config)?;
let endpoint = Endpoint::client(client_address);
if endpoint.is_err() {
error!("Cannot create client endpoint");
return Err(IggyError::CannotCreateEndpoint);
}
let mut endpoint = endpoint.unwrap();
endpoint.set_default_client_config(quic_config);
Ok(Self {
config,
endpoint,
server_address,
connection: Mutex::new(None),
state: Mutex::new(ClientState::Disconnected),
events: broadcast(1000),
connected_at: Mutex::new(None),
})
}
async fn handle_response(&self, recv: &mut RecvStream) -> Result<Bytes, IggyError> {
let buffer = recv
.read_to_end(self.config.response_buffer_size as usize)
.await?;
if buffer.is_empty() {
return Err(IggyError::EmptyResponse);
}
let status = u32::from_le_bytes(buffer[..4].try_into().unwrap());
if status != 0 {
error!(
"Received an invalid response with status: {} ({}).",
status,
IggyError::from_code_as_string(status)
);
let length =
u32::from_le_bytes(buffer[4..RESPONSE_INITIAL_BYTES_LENGTH].try_into().unwrap());
let error_message = String::from_utf8_lossy(
&buffer[RESPONSE_INITIAL_BYTES_LENGTH + 4
..RESPONSE_INITIAL_BYTES_LENGTH + length as usize],
)
.to_string();
return Err(IggyError::InvalidResponse(status, length, error_message));
}
let length =
u32::from_le_bytes(buffer[4..RESPONSE_INITIAL_BYTES_LENGTH].try_into().unwrap());
trace!("Status: OK. Response length: {}", length);
if length <= 1 {
return Ok(Bytes::new());
}
Ok(Bytes::copy_from_slice(
&buffer[RESPONSE_INITIAL_BYTES_LENGTH..RESPONSE_INITIAL_BYTES_LENGTH + length as usize],
))
}
async fn connect(&self) -> Result<(), IggyError> {
match self.get_state().await {
ClientState::Connected | ClientState::Authenticating | ClientState::Authenticated => {
trace!("Client is already connected.");
return Ok(());
}
ClientState::Connecting => {
trace!("Client is already connecting.");
return Ok(());
}
_ => {}
}
self.set_state(ClientState::Connecting).await;
if let Some(connected_at) = self.connected_at.lock().await.as_ref() {
let now = IggyTimestamp::now();
let elapsed = now.as_micros() - connected_at.as_micros();
let interval = self.config.reconnection.re_establish_after.as_micros();
trace!(
"Elapsed time since last connection: {}",
IggyDuration::from(elapsed)
);
if elapsed < interval {
let remaining = IggyDuration::from(interval - elapsed);
info!("Trying to connect to the server in: {remaining}",);
sleep(remaining.get_duration()).await;
}
}
let mut retry_count = 0;
let connection;
let remote_address;
loop {
info!(
"{NAME} client is connecting to server: {}...",
self.config.server_address
);
let connection_result = self
.endpoint
.connect(self.server_address, &self.config.server_name)
.unwrap()
.await;
if connection_result.is_err() {
error!(
"Failed to connect to server: {}",
self.config.server_address
);
if !self.config.reconnection.enabled {
warn!("Automatic reconnection is disabled.");
return Err(IggyError::CannotEstablishConnection);
}
let unlimited_retries = self.config.reconnection.max_retries.is_none();
let max_retries = self.config.reconnection.max_retries.unwrap_or_default();
let max_retries_str =
if let Some(max_retries) = self.config.reconnection.max_retries {
max_retries.to_string()
} else {
"unlimited".to_string()
};
let interval_str = self.config.reconnection.interval.as_human_time_string();
if unlimited_retries || retry_count < max_retries {
retry_count += 1;
info!(
"Retrying to connect to server ({retry_count}/{max_retries_str}): {} in: {interval_str}",
self.config.server_address,
);
sleep(self.config.reconnection.interval.get_duration()).await;
continue;
}
self.set_state(ClientState::Disconnected).await;
self.publish_event(DiagnosticEvent::Disconnected).await;
return Err(IggyError::CannotEstablishConnection);
}
connection = connection_result.unwrap();
remote_address = connection.remote_address();
break;
}
let now = IggyTimestamp::now();
info!("{NAME} client has connected to server: {remote_address} at {now}",);
self.set_state(ClientState::Connected).await;
self.connection.lock().await.replace(connection);
self.connected_at.lock().await.replace(now);
self.publish_event(DiagnosticEvent::Connected).await;
match &self.config.auto_login {
AutoLogin::Disabled => {
info!("Automatic sign-in is disabled.");
Ok(())
}
AutoLogin::Enabled(credentials) => {
info!("{NAME} client is signing in...");
self.set_state(ClientState::Authenticating).await;
match credentials {
Credentials::UsernamePassword(username, password) => {
self.login_user(username, password).await?;
self.publish_event(DiagnosticEvent::SignedIn).await;
info!("{NAME} client has signed in with the user credentials, username: {username}",);
Ok(())
}
Credentials::PersonalAccessToken(token) => {
self.login_with_personal_access_token(token).await?;
self.publish_event(DiagnosticEvent::SignedIn).await;
info!("{NAME} client has signed in with a personal access token.",);
Ok(())
}
}
}
}
}
async fn disconnect(&self) -> Result<(), IggyError> {
if self.get_state().await == ClientState::Disconnected {
return Ok(());
}
info!("{NAME} client is disconnecting from server...");
self.set_state(ClientState::Disconnected).await;
self.connection.lock().await.take();
self.endpoint.wait_idle().await;
self.publish_event(DiagnosticEvent::Disconnected).await;
let now = IggyTimestamp::now();
info!("{NAME} client has disconnected from server at: {now}.");
Ok(())
}
async fn send_raw(&self, code: u32, payload: Bytes) -> Result<Bytes, IggyError> {
match self.get_state().await {
ClientState::Disconnected => {
trace!("Cannot send data. Client is not connected.");
return Err(IggyError::NotConnected);
}
ClientState::Connecting => {
trace!("Cannot send data. Client is still connecting.");
return Err(IggyError::NotConnected);
}
_ => {}
}
let connection = self.connection.lock().await;
if let Some(connection) = connection.as_ref() {
let payload_length = payload.len() + REQUEST_INITIAL_BYTES_LENGTH;
let (mut send, mut recv) = connection.open_bi().await?;
trace!("Sending a QUIC request with code: {code}");
send.write_all(&(payload_length as u32).to_le_bytes())
.await?;
send.write_all(&code.to_le_bytes()).await?;
send.write_all(&payload).await?;
send.finish()?;
trace!("Sent a QUIC request with code: {code}, waiting for a response...");
return self.handle_response(&mut recv).await;
}
error!("Cannot send data. Client is not connected.");
Err(IggyError::NotConnected)
}
}
fn configure(config: &QuicClientConfig) -> Result<ClientConfig, IggyError> {
let max_concurrent_bidi_streams = VarInt::try_from(config.max_concurrent_bidi_streams);
if max_concurrent_bidi_streams.is_err() {
error!(
"Invalid 'max_concurrent_bidi_streams': {}",
config.max_concurrent_bidi_streams
);
return Err(IggyError::InvalidConfiguration);
}
let receive_window = VarInt::try_from(config.receive_window);
if receive_window.is_err() {
error!("Invalid 'receive_window': {}", config.receive_window);
return Err(IggyError::InvalidConfiguration);
}
let mut transport = quinn::TransportConfig::default();
transport.initial_mtu(config.initial_mtu);
transport.send_window(config.send_window);
transport.receive_window(receive_window.unwrap());
transport.datagram_send_buffer_size(config.datagram_send_buffer_size as usize);
transport.max_concurrent_bidi_streams(max_concurrent_bidi_streams.unwrap());
if config.keep_alive_interval > 0 {
transport.keep_alive_interval(Some(Duration::from_millis(config.keep_alive_interval)));
}
if config.max_idle_timeout > 0 {
let max_idle_timeout =
IdleTimeout::try_from(Duration::from_millis(config.max_idle_timeout));
if max_idle_timeout.is_err() {
error!("Invalid 'max_idle_timeout': {}", config.max_idle_timeout);
return Err(IggyError::InvalidConfiguration);
}
transport.max_idle_timeout(Some(max_idle_timeout.unwrap()));
}
if CryptoProvider::get_default().is_none() {
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install rustls crypto provider");
}
let mut client_config = match config.validate_certificate {
true => ClientConfig::with_platform_verifier(),
false => {
match QuinnQuicClientConfig::try_from(
rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(SkipServerVerification::new())
.with_no_client_auth(),
) {
Ok(config) => ClientConfig::new(Arc::new(config)),
Err(error) => {
error!("Failed to create QUIC client configuration: {error}");
return Err(IggyError::InvalidConfiguration);
}
}
}
};
client_config.transport_config(Arc::new(transport));
Ok(client_config)
}
#[derive(Debug)]
struct SkipServerVerification;
impl SkipServerVerification {
fn new() -> Arc<Self> {
Arc::new(Self)
}
}
impl ServerCertVerifier for SkipServerVerification {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
// Signature used by the server to sign self-signed certificate (using rcgen)
vec![SignatureScheme::ECDSA_NISTP256_SHA256]
}
}