Skip to main content

legion_protocol/
iron.rs

1//! Legion Protocol extensions and handling (legacy Iron Protocol support)
2//!
3//! This module contains Legion Protocol-specific functionality that extends
4//! beyond standard IRC/IRCv3, including encrypted channels and protocol 
5//! negotiation between Legion-capable clients and servers.
6//!
7//! Note: This module maintains backward compatibility with Iron Protocol
8//! but has been updated to use Legion Protocol as the primary branding.
9
10use crate::{ChannelType, IronError, Result};
11use crate::utils::get_channel_type;
12use crate::capabilities::Capability;
13
14/// Legion Protocol version information
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum IronVersion {
17    V1,
18}
19
20/// Legion Protocol version information (current naming)
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum LegionVersion {
23    V1,
24}
25
26impl LegionVersion {
27    /// Get the capability string for this Legion version
28    pub fn as_capability(&self) -> &'static str {
29        match self {
30            LegionVersion::V1 => "+legion-protocol/v1",
31        }
32    }
33}
34
35impl IronVersion {
36    /// Get the capability string for this Iron version (legacy support)
37    pub fn as_capability(&self) -> &'static str {
38        match self {
39            IronVersion::V1 => "+iron-protocol/v1",
40        }
41    }
42    
43    /// Convert to Legion Protocol version
44    pub fn to_legion_version(&self) -> LegionVersion {
45        match self {
46            IronVersion::V1 => LegionVersion::V1,
47        }
48    }
49}
50
51/// Result of Legion Protocol capability negotiation
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum IronNegotiationResult {
54    /// Both client and server support Legion Protocol
55    LegionCapable { version: LegionVersion },
56    /// Both client and server support Iron Protocol (legacy)
57    IronCapable { version: IronVersion },
58    /// Only one side supports Legion/Iron Protocol (fallback to IRC)
59    IrcFallback,
60    /// No Legion/Iron Protocol support
61    NotSupported,
62}
63
64/// Legion Protocol session state
65#[derive(Debug, Clone)]
66pub struct IronSession {
67    iron_version: Option<IronVersion>,      // Legacy support
68    legion_version: Option<LegionVersion>,  // Current version
69    encrypted_channels: Vec<String>,
70    negotiation_complete: bool,
71}
72
73impl IronSession {
74    /// Create a new Legion Protocol session
75    pub fn new() -> Self {
76        Self {
77            iron_version: None,
78            legion_version: None,
79            encrypted_channels: Vec::new(),
80            negotiation_complete: false,
81        }
82    }
83
84    /// Set the negotiated Iron Protocol version (legacy)
85    pub fn set_version(&mut self, version: IronVersion) {
86        self.iron_version = Some(version);
87    }
88    
89    /// Set the negotiated Legion Protocol version
90    pub fn set_legion_version(&mut self, version: LegionVersion) {
91        self.legion_version = Some(version);
92    }
93
94    /// Check if Legion/Iron Protocol is active
95    pub fn is_iron_active(&self) -> bool {
96        (self.legion_version.is_some() || self.iron_version.is_some()) && self.negotiation_complete
97    }
98    
99    /// Check if Legion Protocol specifically is active
100    pub fn is_legion_active(&self) -> bool {
101        self.legion_version.is_some() && self.negotiation_complete
102    }
103
104    /// Get the active Iron Protocol version (legacy)
105    pub fn version(&self) -> Option<IronVersion> {
106        self.iron_version
107    }
108    
109    /// Get the active Legion Protocol version
110    pub fn legion_version(&self) -> Option<LegionVersion> {
111        self.legion_version
112    }
113
114    /// Complete Legion/Iron Protocol negotiation
115    pub fn complete_negotiation(&mut self) {
116        self.negotiation_complete = true;
117    }
118
119    /// Check if a channel is in our encrypted channels list
120    pub fn is_encrypted_channel(&self, channel: &str) -> bool {
121        self.encrypted_channels.iter().any(|c| c == channel)
122    }
123
124    /// Add an encrypted channel to our list
125    pub fn add_encrypted_channel(&mut self, channel: String) {
126        if !self.encrypted_channels.contains(&channel) {
127            self.encrypted_channels.push(channel);
128        }
129    }
130
131    /// Remove an encrypted channel from our list
132    pub fn remove_encrypted_channel(&mut self, channel: &str) {
133        self.encrypted_channels.retain(|c| c != channel);
134    }
135}
136
137impl Default for IronSession {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143/// Handle channel access control for Legion Protocol
144pub struct IronChannelHandler;
145
146impl IronChannelHandler {
147    /// Check if a user can join a channel based on Legion Protocol capabilities
148    pub fn can_join_channel(
149        channel: &str,
150        user_has_legion: bool,
151        server_has_legion: bool,
152    ) -> Result<ChannelJoinResult> {
153        let channel_type = get_channel_type(channel);
154
155        match channel_type {
156            ChannelType::IrcGlobal | ChannelType::IrcLocal => {
157                // Standard IRC channels - anyone can join
158                Ok(ChannelJoinResult::Allowed)
159            }
160            ChannelType::LegionEncrypted => {
161                // Legion encrypted channels require both client and server Legion support
162                if user_has_legion && server_has_legion {
163                    Ok(ChannelJoinResult::AllowedEncrypted)
164                } else {
165                    Ok(ChannelJoinResult::Denied {
166                        reason: IronChannelError::IncompatibleClient,
167                    })
168                }
169            }
170            ChannelType::Invalid => Err(IronError::Parse(format!(
171                "Invalid channel name: {}",
172                channel
173            ))),
174        }
175    }
176
177    /// Generate appropriate error message for IRC users trying to join Legion channels
178    pub fn generate_error_message(channel: &str, error: &IronChannelError) -> String {
179        match error {
180            IronChannelError::IncompatibleClient => {
181                format!(
182                    "Cannot join encrypted channel {} - requires Legion Protocol support. \
183                     Upgrade to a Legion-compatible client or ask channel admin to create \
184                     a standard IRC channel (#{}) for IRC users.",
185                    channel,
186                    &channel[1..] // Remove the ! prefix to suggest # alternative
187                )
188            }
189            IronChannelError::EncryptionRequired => {
190                format!(
191                    "Channel {} requires end-to-end encryption. \
192                     Please use a Legion Protocol-compatible client.",
193                    channel
194                )
195            }
196        }
197    }
198}
199
200/// Result of attempting to join a channel
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub enum ChannelJoinResult {
203    /// User is allowed to join
204    Allowed,
205    /// User is allowed to join with encryption
206    AllowedEncrypted,
207    /// User is denied access
208    Denied { reason: IronChannelError },
209}
210
211/// Reasons why a user might be denied access to a Legion channel
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum IronChannelError {
214    /// Client doesn't support Legion Protocol
215    IncompatibleClient,
216    /// Channel requires encryption but user can't provide it
217    EncryptionRequired,
218}
219
220/// Detect Legion/Iron Protocol support during capability negotiation
221pub fn detect_legion_support(
222    client_caps: &[Capability],
223    server_caps: &[Capability],
224) -> IronNegotiationResult {
225    let client_legion = client_caps
226        .iter()
227        .any(|cap| matches!(cap, Capability::LegionProtocolV1));
228    let server_legion = server_caps
229        .iter()
230        .any(|cap| matches!(cap, Capability::LegionProtocolV1));
231        
232    let client_iron = client_caps
233        .iter()
234        .any(|cap| matches!(cap, Capability::IronProtocolV1));
235    let server_iron = server_caps
236        .iter()
237        .any(|cap| matches!(cap, Capability::IronProtocolV1));
238
239    // Prefer Legion Protocol over Iron Protocol
240    match (client_legion, server_legion) {
241        (true, true) => IronNegotiationResult::LegionCapable {
242            version: LegionVersion::V1,
243        },
244        _ => {
245            // Fall back to Iron Protocol support
246            match (client_iron, server_iron) {
247                (true, true) => IronNegotiationResult::IronCapable {
248                    version: IronVersion::V1,
249                },
250                (true, false) | (false, true) => {
251                    // Check if at least one side supports Legion (mixed capability fallback)
252                    if client_legion || server_legion {
253                        IronNegotiationResult::IrcFallback
254                    } else {
255                        IronNegotiationResult::IrcFallback
256                    }
257                },
258                (false, false) => {
259                    // Check if at least one side supports Legion
260                    if client_legion || server_legion {
261                        IronNegotiationResult::IrcFallback
262                    } else {
263                        IronNegotiationResult::NotSupported
264                    }
265                },
266            }
267        }
268    }
269}
270
271/// Legacy function for backward compatibility
272#[deprecated(note = "Use detect_legion_support instead")]
273pub fn detect_iron_support(
274    client_caps: &[Capability],
275    server_caps: &[Capability],
276) -> IronNegotiationResult {
277    detect_legion_support(client_caps, server_caps)
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_iron_version_capability() {
286        // Test legacy Iron version
287        assert_eq!(IronVersion::V1.as_capability(), "+iron-protocol/v1");
288        
289        // Test new Legion version
290        assert_eq!(LegionVersion::V1.as_capability(), "+legion-protocol/v1");
291        
292        // Test conversion
293        assert_eq!(IronVersion::V1.to_legion_version(), LegionVersion::V1);
294    }
295
296    #[test]
297    fn test_channel_access_control() {
298        // Standard IRC channels - always allowed
299        let result = IronChannelHandler::can_join_channel("#general", false, false).unwrap();
300        assert_eq!(result, ChannelJoinResult::Allowed);
301
302        // Legion encrypted channel with compatible clients
303        let result = IronChannelHandler::can_join_channel("!encrypted", true, true).unwrap();
304        assert_eq!(result, ChannelJoinResult::AllowedEncrypted);
305
306        // Legion encrypted channel with incompatible client
307        let result = IronChannelHandler::can_join_channel("!encrypted", false, true).unwrap();
308        assert!(matches!(
309            result,
310            ChannelJoinResult::Denied {
311                reason: IronChannelError::IncompatibleClient
312            }
313        ));
314    }
315
316    #[test]
317    fn test_legion_detection() {
318        // Test Legion Protocol detection (preferred)
319        let client_caps = vec![Capability::LegionProtocolV1, Capability::MessageTags];
320        let server_caps = vec![Capability::LegionProtocolV1, Capability::Sasl];
321
322        let result = detect_legion_support(&client_caps, &server_caps);
323        assert_eq!(
324            result,
325            IronNegotiationResult::LegionCapable {
326                version: LegionVersion::V1
327            }
328        );
329        
330        // Test Iron Protocol fallback (legacy)
331        let client_caps = vec![Capability::IronProtocolV1, Capability::MessageTags];
332        let server_caps = vec![Capability::IronProtocolV1, Capability::Sasl];
333
334        let result = detect_legion_support(&client_caps, &server_caps);
335        assert_eq!(
336            result,
337            IronNegotiationResult::IronCapable {
338                version: IronVersion::V1
339            }
340        );
341
342        // Test fallback scenario
343        let client_caps = vec![Capability::MessageTags];
344        let result = detect_legion_support(&client_caps, &server_caps);
345        assert_eq!(result, IronNegotiationResult::IrcFallback);
346        
347        // Test backward compatibility
348        #[allow(deprecated)]
349        let result = detect_iron_support(&client_caps, &server_caps);
350        assert_eq!(result, IronNegotiationResult::IrcFallback);
351    }
352
353    #[test]
354    fn test_legion_session() {
355        let mut session = IronSession::new();
356        assert!(!session.is_iron_active());
357        assert!(!session.is_legion_active());
358
359        // Test legacy Iron Protocol support
360        session.set_version(IronVersion::V1);
361        session.complete_negotiation();
362        assert!(session.is_iron_active());
363        assert!(!session.is_legion_active());
364        assert_eq!(session.version(), Some(IronVersion::V1));
365        
366        // Test new Legion Protocol support
367        let mut legion_session = IronSession::new();
368        legion_session.set_legion_version(LegionVersion::V1);
369        legion_session.complete_negotiation();
370        assert!(legion_session.is_iron_active()); // Should be true for either protocol
371        assert!(legion_session.is_legion_active());
372        assert_eq!(legion_session.legion_version(), Some(LegionVersion::V1));
373
374        session.add_encrypted_channel("!secure".to_string());
375        assert!(session.is_encrypted_channel("!secure"));
376        assert!(!session.is_encrypted_channel("!other"));
377    }
378}