use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum TransportAuthError {
#[error("No authentication provided")]
NoAuth,
#[error("Invalid authentication format: {0}")]
InvalidFormat(String),
#[error("Transport not supported")]
UnsupportedTransport,
#[error("Missing required data: {0}")]
MissingData(String),
#[error("Authentication failed: {0}")]
AuthFailed(String),
}
pub type AuthExtractionResult = Result<Option<TransportAuthContext>, TransportAuthError>;
#[derive(Debug, Clone)]
pub struct TransportAuthContext {
pub credential: String,
pub method: String,
pub client_ip: Option<String>,
pub user_agent: Option<String>,
pub metadata: HashMap<String, String>,
pub transport_type: TransportType,
}
impl TransportAuthContext {
pub fn new(credential: String, method: String, transport_type: TransportType) -> Self {
Self {
credential,
method,
client_ip: None,
user_agent: None,
metadata: HashMap::new(),
transport_type,
}
}
pub fn with_client_ip(mut self, ip: String) -> Self {
self.client_ip = Some(ip);
self
}
pub fn with_user_agent(mut self, user_agent: String) -> Self {
self.user_agent = Some(user_agent);
self
}
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransportType {
Http,
WebSocket,
Stdio,
Custom(String),
}
#[derive(Debug, Clone)]
pub struct TransportRequest {
pub headers: HashMap<String, String>,
pub query_params: HashMap<String, String>,
pub body: Option<Value>,
pub raw_data: Option<Vec<u8>>,
pub metadata: HashMap<String, Value>,
}
impl TransportRequest {
pub fn new() -> Self {
Self {
headers: HashMap::new(),
query_params: HashMap::new(),
body: None,
raw_data: None,
metadata: HashMap::new(),
}
}
pub fn from_headers(headers: HashMap<String, String>) -> Self {
Self {
headers,
query_params: HashMap::new(),
body: None,
raw_data: None,
metadata: HashMap::new(),
}
}
pub fn with_header(mut self, key: String, value: String) -> Self {
self.headers.insert(key, value);
self
}
pub fn with_query_param(mut self, key: String, value: String) -> Self {
self.query_params.insert(key, value);
self
}
pub fn with_body(mut self, body: Value) -> Self {
self.body = Some(body);
self
}
pub fn get_header(&self, key: &str) -> Option<&String> {
self.headers.get(key)
}
pub fn get_query_param(&self, key: &str) -> Option<&String> {
self.query_params.get(key)
}
}
impl Default for TransportRequest {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
pub trait AuthExtractor: Send + Sync {
async fn extract_auth(&self, request: &TransportRequest) -> AuthExtractionResult;
fn transport_type(&self) -> TransportType;
fn can_handle(&self, _request: &TransportRequest) -> bool {
true
}
async fn validate_auth(
&self,
_context: &TransportAuthContext,
) -> Result<(), TransportAuthError> {
Ok(())
}
}
pub struct AuthUtils;
impl AuthUtils {
pub fn extract_bearer_token(auth_header: &str) -> Result<String, TransportAuthError> {
if !auth_header.starts_with("Bearer ") {
return Err(TransportAuthError::InvalidFormat(
"Authorization header must start with 'Bearer '".to_string(),
));
}
let token = &auth_header[7..]; if token.is_empty() {
return Err(TransportAuthError::InvalidFormat(
"Bearer token cannot be empty".to_string(),
));
}
Ok(token.to_string())
}
pub fn extract_api_key_header(headers: &HashMap<String, String>) -> Option<String> {
headers
.get("X-API-Key")
.or_else(|| headers.get("x-api-key"))
.or_else(|| headers.get("X-Api-Key"))
.cloned()
}
pub fn extract_client_ip(headers: &HashMap<String, String>) -> Option<String> {
headers
.get("X-Forwarded-For")
.or_else(|| headers.get("X-Real-IP"))
.or_else(|| headers.get("X-Client-IP"))
.or_else(|| headers.get("CF-Connecting-IP")) .map(|ip| {
ip.split(',').next().unwrap_or(ip).trim().to_string()
})
}
pub fn extract_user_agent(headers: &HashMap<String, String>) -> Option<String> {
headers
.get("User-Agent")
.or_else(|| headers.get("user-agent"))
.cloned()
}
pub fn validate_api_key_format(api_key: &str) -> Result<(), TransportAuthError> {
if api_key.is_empty() {
return Err(TransportAuthError::InvalidFormat(
"API key cannot be empty".to_string(),
));
}
if api_key.len() < 16 {
return Err(TransportAuthError::InvalidFormat(
"API key too short".to_string(),
));
}
if api_key.len() > 256 {
return Err(TransportAuthError::InvalidFormat(
"API key too long".to_string(),
));
}
if !api_key
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(TransportAuthError::InvalidFormat(
"API key contains invalid characters".to_string(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bearer_token_extraction() {
let valid_header = "Bearer abc123def456";
let token = AuthUtils::extract_bearer_token(valid_header).unwrap();
assert_eq!(token, "abc123def456");
let invalid_header = "Basic abc123";
assert!(AuthUtils::extract_bearer_token(invalid_header).is_err());
let empty_token = "Bearer ";
assert!(AuthUtils::extract_bearer_token(empty_token).is_err());
}
#[test]
fn test_api_key_header_extraction() {
let mut headers = HashMap::new();
headers.insert("X-API-Key".to_string(), "test-key-123".to_string());
let key = AuthUtils::extract_api_key_header(&headers).unwrap();
assert_eq!(key, "test-key-123");
let mut headers2 = HashMap::new();
headers2.insert("x-api-key".to_string(), "test-key-456".to_string());
let key2 = AuthUtils::extract_api_key_header(&headers2).unwrap();
assert_eq!(key2, "test-key-456");
}
#[test]
fn test_client_ip_extraction() {
let mut headers = HashMap::new();
headers.insert(
"X-Forwarded-For".to_string(),
"192.168.1.100, 10.0.0.1".to_string(),
);
let ip = AuthUtils::extract_client_ip(&headers).unwrap();
assert_eq!(ip, "192.168.1.100");
let mut headers2 = HashMap::new();
headers2.insert("X-Real-IP".to_string(), "203.0.113.45".to_string());
let ip2 = AuthUtils::extract_client_ip(&headers2).unwrap();
assert_eq!(ip2, "203.0.113.45");
}
#[test]
fn test_api_key_format_validation() {
assert!(AuthUtils::validate_api_key_format("lmcp_admin_1234567890abcdef").is_ok());
assert!(AuthUtils::validate_api_key_format("short").is_err());
assert!(AuthUtils::validate_api_key_format("key with spaces").is_err());
assert!(AuthUtils::validate_api_key_format("").is_err());
}
#[test]
fn test_transport_request_builder() {
let request = TransportRequest::new()
.with_header("Authorization".to_string(), "Bearer token123".to_string())
.with_query_param("format".to_string(), "json".to_string());
assert_eq!(
request.get_header("Authorization").unwrap(),
"Bearer token123"
);
assert_eq!(request.get_query_param("format").unwrap(), "json");
}
}