use ferrotunnel_common::{
Result, TunnelError, DEFAULT_HTTP_PORT, DEFAULT_LOCAL_ADDR, DEFAULT_TUNNEL_PORT,
};
use std::net::SocketAddr;
#[cfg(feature = "http3")]
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub server_addr: String,
pub token: String,
pub local_addr: String,
pub tunnel_id: Option<String>,
pub auto_reconnect: bool,
pub reconnect_delay: Duration,
}
impl ClientConfig {
pub fn validate(&self) -> Result<()> {
if self.server_addr.is_empty() {
return Err(TunnelError::Config("server_addr is required".into()));
}
if self.token.is_empty() {
return Err(TunnelError::Config("token is required".into()));
}
if self.local_addr.is_empty() {
return Err(TunnelError::Config("local_addr is required".into()));
}
Ok(())
}
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
server_addr: String::new(),
token: String::new(),
local_addr: DEFAULT_LOCAL_ADDR.to_string(),
tunnel_id: None,
auto_reconnect: true,
reconnect_delay: Duration::from_secs(5),
}
}
}
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub bind_addr: SocketAddr,
pub http_bind_addr: SocketAddr,
#[cfg(feature = "http3")]
pub http3_bind_addr: Option<SocketAddr>,
#[cfg(feature = "http3")]
pub http3_cert_path: Option<PathBuf>,
#[cfg(feature = "http3")]
pub http3_key_path: Option<PathBuf>,
pub token: String,
}
impl ServerConfig {
pub fn validate(&self) -> Result<()> {
if self.token.is_empty() {
return Err(TunnelError::Config("token is required".into()));
}
#[cfg(feature = "http3")]
if self.http3_bind_addr.is_some()
&& (self.http3_cert_path.is_none() || self.http3_key_path.is_none())
{
return Err(TunnelError::Config(
"HTTP/3 ingress requires certificate and key paths".into(),
));
}
Ok(())
}
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
bind_addr: ([0, 0, 0, 0], DEFAULT_TUNNEL_PORT).into(),
http_bind_addr: ([0, 0, 0, 0], DEFAULT_HTTP_PORT).into(),
#[cfg(feature = "http3")]
http3_bind_addr: None,
#[cfg(feature = "http3")]
http3_cert_path: None,
#[cfg(feature = "http3")]
http3_key_path: None,
token: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct TunnelInfo {
pub session_id: Option<uuid::Uuid>,
pub public_url: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_config_default() {
let config = ClientConfig::default();
assert!(config.server_addr.is_empty());
assert!(config.token.is_empty());
assert_eq!(config.local_addr, "127.0.0.1:8080");
assert!(config.auto_reconnect);
assert_eq!(config.reconnect_delay, Duration::from_secs(5));
}
#[test]
fn test_client_config_validate_success() {
let config = ClientConfig {
server_addr: "localhost:7835".to_string(),
token: "secret-token".to_string(),
local_addr: "127.0.0.1:8080".to_string(),
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn test_client_config_validate_missing_server_addr() {
let config = ClientConfig {
server_addr: String::new(),
token: "secret".to_string(),
local_addr: "127.0.0.1:8080".to_string(),
..Default::default()
};
let err = config.validate().unwrap_err();
assert!(err.to_string().contains("server_addr"));
}
#[test]
fn test_client_config_validate_missing_token() {
let config = ClientConfig {
server_addr: "localhost:7835".to_string(),
token: String::new(),
local_addr: "127.0.0.1:8080".to_string(),
..Default::default()
};
let err = config.validate().unwrap_err();
assert!(err.to_string().contains("token"));
}
#[test]
fn test_client_config_validate_missing_local_addr() {
let config = ClientConfig {
server_addr: "localhost:7835".to_string(),
token: "secret".to_string(),
local_addr: String::new(),
..Default::default()
};
let err = config.validate().unwrap_err();
assert!(err.to_string().contains("local_addr"));
}
#[test]
fn test_server_config_default() {
let config = ServerConfig::default();
assert_eq!(config.bind_addr, SocketAddr::from(([0, 0, 0, 0], 7835)));
assert_eq!(
config.http_bind_addr,
SocketAddr::from(([0, 0, 0, 0], 8080))
);
assert!(config.token.is_empty());
}
#[test]
fn test_server_config_validate_success() {
let config = ServerConfig {
token: "secret-token".to_string(),
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn test_server_config_validate_missing_token() {
let config = ServerConfig::default();
let err = config.validate().unwrap_err();
assert!(err.to_string().contains("token"));
}
#[test]
fn test_tunnel_info_none_values() {
let info = TunnelInfo {
session_id: None,
public_url: None,
};
assert!(info.session_id.is_none());
assert!(info.public_url.is_none());
}
#[test]
fn test_tunnel_info_with_values() {
let uuid = uuid::Uuid::new_v4();
let info = TunnelInfo {
session_id: Some(uuid),
public_url: Some("https://tunnel.example.com".to_string()),
};
assert_eq!(info.session_id, Some(uuid));
assert_eq!(
info.public_url,
Some("https://tunnel.example.com".to_string())
);
}
}