use crate::config::{ClientConfig, TunnelInfo};
use ferrotunnel_common::config::TlsConfig;
use ferrotunnel_common::{Result, TunnelError};
use ferrotunnel_core::transport::{tls::TlsTransportConfig, TransportConfig};
use ferrotunnel_core::TunnelClient;
use ferrotunnel_http::HttpProxy;
use ferrotunnel_protocol::frame::Protocol;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{oneshot, watch};
use tokio::task::JoinHandle;
use tracing::{error, info};
#[derive(Debug)]
pub struct Client {
config: ClientConfig,
transport_config: TransportConfig,
shutdown_tx: Option<watch::Sender<bool>>,
task: Option<JoinHandle<()>>,
}
#[derive(Debug, Default)]
pub struct ClientBuilder {
config: ClientConfig,
transport_config: Option<TransportConfig>,
}
impl Client {
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub async fn start(&mut self) -> Result<TunnelInfo> {
if self.task.is_some() {
return Err(TunnelError::InvalidState("client already started".into()));
}
let config = self.config.clone();
let (shutdown_tx, shutdown_rx) = watch::channel(false);
self.shutdown_tx = Some(shutdown_tx);
let (info_tx, info_rx) = oneshot::channel();
let server_addr = config.server_addr.clone();
let token = config.token.clone();
let local_addr = config.local_addr.clone();
let tunnel_id = config.tunnel_id.clone();
let auto_reconnect = config.auto_reconnect;
let reconnect_delay = config.reconnect_delay;
let transport_config = self.transport_config.clone();
let info_tx = Arc::new(std::sync::Mutex::new(Some(info_tx)));
let task = tokio::spawn(async move {
let proxy = Arc::new(HttpProxy::new(local_addr));
let mut shutdown_rx = shutdown_rx;
loop {
let mut client = TunnelClient::new(server_addr.clone(), token.clone())
.with_transport(transport_config.clone());
if let Some(ref id) = tunnel_id {
client = client.with_tunnel_id(id.clone());
}
let proxy_ref = proxy.clone();
let info_tx = info_tx.clone();
let connect_result = tokio::select! {
result = client.connect_and_run_with_callback(move |stream| {
let proxy = proxy_ref.clone();
async move {
if stream.protocol() == Protocol::GRPC {
proxy.handle_grpc_stream(stream);
} else {
proxy.handle_stream(stream);
}
}
}, move |session_id| {
if let Ok(mut lock) = info_tx.lock() {
if let Some(tx) = lock.take() {
let _ = tx.send(TunnelInfo {
session_id: Some(session_id),
public_url: None,
});
}
}
}) => result,
_ = shutdown_rx.changed() => {
info!("Client shutdown requested");
break;
}
};
match connect_result {
Ok(()) => {
info!("Client finished normally");
break;
}
Err(e) => {
error!("Connection error: {}", e);
if !auto_reconnect {
break;
}
info!("Reconnecting in {:?}...", reconnect_delay);
tokio::time::sleep(reconnect_delay).await;
}
}
}
});
self.task = Some(task);
info_rx
.await
.map_err(|_| TunnelError::Connection("Failed to establish connection".into()))
}
pub async fn shutdown(&mut self) -> Result<()> {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(true);
}
if let Some(task) = self.task.take() {
let _ = task.await;
}
Ok(())
}
pub fn stop(&mut self) {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(true);
}
}
pub fn is_running(&self) -> bool {
if let Some(task) = &self.task {
!task.is_finished()
} else {
false
}
}
pub fn config(&self) -> &ClientConfig {
&self.config
}
}
impl Drop for Client {
fn drop(&mut self) {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(true);
}
}
}
impl ClientBuilder {
#[must_use]
pub fn server_addr(mut self, addr: impl Into<String>) -> Self {
self.config.server_addr = addr.into();
self
}
#[must_use]
pub fn token(mut self, token: impl Into<String>) -> Self {
self.config.token = token.into();
self
}
#[must_use]
pub fn local_addr(mut self, addr: impl Into<String>) -> Self {
self.config.local_addr = addr.into();
self
}
#[must_use]
pub fn tunnel_id(mut self, id: impl Into<String>) -> Self {
self.config.tunnel_id = Some(id.into());
self
}
#[must_use]
pub fn auto_reconnect(mut self, enabled: bool) -> Self {
self.config.auto_reconnect = enabled;
self
}
#[must_use]
pub fn reconnect_delay(mut self, delay: Duration) -> Self {
self.config.reconnect_delay = delay;
self
}
#[must_use]
pub fn tls(mut self, config: &TlsConfig) -> Self {
if let Some(tls) = TlsTransportConfig::from_common(config) {
self.transport_config = Some(TransportConfig::Tls(tls));
}
self
}
pub fn build(self) -> Result<Client> {
self.config.validate()?;
Ok(Client {
config: self.config,
transport_config: self.transport_config.unwrap_or_default(),
shutdown_tx: None,
task: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_builder_success() {
let client = Client::builder()
.server_addr("localhost:7835")
.token("secret-token")
.local_addr("127.0.0.1:9000")
.build();
assert!(client.is_ok());
}
#[test]
fn test_client_builder_with_all_options() {
let client = Client::builder()
.server_addr("tunnel.example.com:7835")
.token("my-token")
.local_addr("127.0.0.1:3000")
.auto_reconnect(false)
.reconnect_delay(Duration::from_secs(10))
.build()
.expect("should build successfully");
assert_eq!(client.config().server_addr, "tunnel.example.com:7835");
assert_eq!(client.config().token, "my-token");
assert_eq!(client.config().local_addr, "127.0.0.1:3000");
assert!(!client.config().auto_reconnect);
assert_eq!(client.config().reconnect_delay, Duration::from_secs(10));
}
#[test]
fn test_client_builder_missing_server_addr() {
let result = Client::builder()
.token("secret")
.local_addr("127.0.0.1:8080")
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("server_addr"));
}
#[test]
fn test_client_builder_missing_token() {
let result = Client::builder()
.server_addr("localhost:7835")
.local_addr("127.0.0.1:8080")
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("token"));
}
#[test]
fn test_client_builder_default_local_addr() {
let client = Client::builder()
.server_addr("localhost:7835")
.token("secret")
.build()
.expect("should use default local_addr");
assert_eq!(client.config().local_addr, "127.0.0.1:8080");
}
#[test]
fn test_client_not_running_initially() {
let client = Client::builder()
.server_addr("localhost:7835")
.token("secret")
.build()
.expect("should build");
assert!(!client.is_running());
}
#[test]
fn test_client_builder_tls_disabled() {
let tls = TlsConfig {
enabled: false,
..Default::default()
};
let client = Client::builder()
.server_addr("localhost:7835")
.token("secret")
.tls(&tls)
.build()
.expect("should build");
assert!(!client.config().server_addr.is_empty());
}
}