use crate::legion::{LegionError, LegionResult};
use legion_protocol::{Capability, IronSession, IronVersion};
use phalanx_crypto::{Identity, PublicKey};
use std::time::{SystemTime, UNIX_EPOCH, Duration};
use std::collections::HashSet;
use tokio::sync::RwLock;
const MAX_IDLE_TIME: Duration = Duration::from_secs(30 * 60);
#[derive(Debug)]
pub struct LegionSession {
client_id: String,
identity: RwLock<Identity>,
legion_session: RwLock<IronSession>,
capabilities: HashSet<Capability>,
created_at: SystemTime,
last_activity: RwLock<SystemTime>,
joined_channels: RwLock<HashSet<String>>,
legion_active: RwLock<bool>,
}
impl LegionSession {
pub async fn new(client_id: String, capabilities: Vec<Capability>) -> LegionResult<Self> {
let identity = Identity::generate();
let mut legion_session = IronSession::new();
let has_legion = capabilities.iter().any(|cap| {
matches!(cap, Capability::LegionProtocolV1 | Capability::IronProtocolV1)
});
if has_legion {
if capabilities.contains(&Capability::LegionProtocolV1) {
legion_session.set_version(IronVersion::V1);
} else if capabilities.contains(&Capability::IronProtocolV1) {
legion_session.set_version(IronVersion::V1);
}
}
let now = SystemTime::now();
Ok(Self {
client_id,
identity: RwLock::new(identity),
legion_session: RwLock::new(legion_session),
capabilities: capabilities.into_iter().collect(),
created_at: now,
last_activity: RwLock::new(now),
joined_channels: RwLock::new(HashSet::new()),
legion_active: RwLock::new(has_legion),
})
}
pub fn supports_legion(&self) -> bool {
self.capabilities.contains(&Capability::LegionProtocolV1) ||
self.capabilities.contains(&Capability::IronProtocolV1)
}
pub async fn is_legion_active(&self) -> bool {
*self.legion_active.read().await
}
pub async fn activate_legion(&self) -> LegionResult<()> {
if !self.supports_legion() {
return Err(LegionError::Session(
"Client does not support Legion Protocol".to_string()
));
}
let mut legion_session = self.legion_session.write().await;
legion_session.complete_negotiation();
*self.legion_active.write().await = true;
self.update_activity().await;
tracing::info!("Activated Legion Protocol for client: {}", self.client_id);
Ok(())
}
pub async fn identity(&self) -> LegionResult<Identity> {
Ok(self.identity.read().await.clone())
}
pub async fn public_key(&self) -> LegionResult<PublicKey> {
let identity = self.identity.read().await;
Ok(identity.public_key())
}
pub async fn update_activity(&self) {
*self.last_activity.write().await = SystemTime::now();
}
pub fn is_inactive(&self) -> bool {
if let Ok(last_activity) = self.last_activity.try_read() {
if let Ok(elapsed) = last_activity.elapsed() {
return elapsed > MAX_IDLE_TIME;
}
}
false
}
pub fn age(&self) -> Duration {
self.created_at.elapsed().unwrap_or(Duration::ZERO)
}
pub async fn join_channel(&self, channel_name: String) -> LegionResult<()> {
if !legion_protocol::utils::is_legion_encrypted_channel(&channel_name) {
return Err(LegionError::Channel(format!("Not a Legion channel: {}", channel_name)));
}
let mut channels = self.joined_channels.write().await;
channels.insert(channel_name.clone());
if *self.legion_active.read().await {
let mut legion_session = self.legion_session.write().await;
legion_session.add_encrypted_channel(channel_name);
}
self.update_activity().await;
Ok(())
}
pub async fn leave_channel(&self, channel_name: &str) -> LegionResult<()> {
let mut channels = self.joined_channels.write().await;
channels.remove(channel_name);
if *self.legion_active.read().await {
let mut legion_session = self.legion_session.write().await;
legion_session.remove_encrypted_channel(channel_name);
}
self.update_activity().await;
Ok(())
}
pub async fn joined_channels(&self) -> HashSet<String> {
self.joined_channels.read().await.clone()
}
pub async fn is_in_channel(&self, channel_name: &str) -> bool {
self.joined_channels.read().await.contains(channel_name)
}
pub fn capabilities(&self) -> &HashSet<Capability> {
&self.capabilities
}
pub fn client_id(&self) -> &str {
&self.client_id
}
pub async fn stats(&self) -> SessionStats {
let channels = self.joined_channels.read().await;
let last_activity = *self.last_activity.read().await;
let legion_active = *self.legion_active.read().await;
SessionStats {
client_id: self.client_id.clone(),
created_at: self.created_at,
last_activity,
age: self.age(),
legion_active,
supports_legion: self.supports_legion(),
joined_channels: channels.len(),
capabilities_count: self.capabilities.len(),
}
}
pub async fn create_handshake(&self, group_id: [u8; 32]) -> LegionResult<phalanx_crypto::protocol::HandshakeMessage> {
let identity = self.identity.read().await;
let capabilities = vec![
"legion-protocol/v1".to_string(),
"phalanx/v1".to_string(),
];
let client_info = format!("centurion-client/{}", env!("CARGO_PKG_VERSION"));
phalanx_crypto::protocol::HandshakeMessage::new(
&*identity,
group_id,
capabilities,
client_info,
).map_err(|e| LegionError::Session(format!("Handshake creation failed: {}", e)))
}
pub async fn process_handshake(
&self,
handshake: phalanx_crypto::protocol::HandshakeMessage
) -> LegionResult<phalanx_crypto::protocol::HandshakePayload> {
handshake.verify_and_decrypt()
.map_err(|e| LegionError::Session(format!("Handshake processing failed: {}", e)))
}
}
#[derive(Debug, Clone)]
pub struct SessionStats {
pub client_id: String,
pub created_at: SystemTime,
pub last_activity: SystemTime,
pub age: Duration,
pub legion_active: bool,
pub supports_legion: bool,
pub joined_channels: usize,
pub capabilities_count: usize,
}
impl SessionStats {
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"client_id": self.client_id,
"created_at": self.created_at.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
"last_activity": self.last_activity.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
"age_seconds": self.age.as_secs(),
"legion_active": self.legion_active,
"supports_legion": self.supports_legion,
"joined_channels": self.joined_channels,
"capabilities_count": self.capabilities_count
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_session_creation() {
let capabilities = vec![Capability::LegionProtocolV1, Capability::MessageTags];
let session = LegionSession::new("test_client".to_string(), capabilities).await.unwrap();
assert!(session.supports_legion());
assert_eq!(session.client_id(), "test_client");
assert!(!session.is_inactive());
}
#[tokio::test]
async fn test_channel_management() {
let capabilities = vec![Capability::LegionProtocolV1];
let session = LegionSession::new("test_client".to_string(), capabilities).await.unwrap();
session.activate_legion().await.unwrap();
session.join_channel("!encrypted".to_string()).await.unwrap();
assert!(session.is_in_channel("!encrypted").await);
assert_eq!(session.joined_channels().await.len(), 1);
session.leave_channel("!encrypted").await.unwrap();
assert!(!session.is_in_channel("!encrypted").await);
assert_eq!(session.joined_channels().await.len(), 0);
}
#[tokio::test]
async fn test_legion_activation() {
let capabilities = vec![Capability::LegionProtocolV1];
let session = LegionSession::new("test_client".to_string(), capabilities).await.unwrap();
assert!(!session.is_legion_active().await);
session.activate_legion().await.unwrap();
assert!(session.is_legion_active().await);
}
}