Skip to main content

legion_protocol/
lib.rs

1//! # Legion Protocol
2//!
3//! A secure, IRC-compatible communication protocol with E2E encryption support.
4//!
5//! This crate provides comprehensive support for the IRC protocol with particular
6//! emphasis on IRCv3 capabilities and the latest 2024-2025 draft specifications.
7//!
8//! ## Features
9//!
10//! - Full IRCv3 capability negotiation
11//! - Message parsing and serialization with tags support
12//! - SASL authentication mechanisms
13//! - Security validation and DoS protection
14//! - Bleeding-edge 2024-2025 draft features
15//! - Both client and server-side utilities
16//!
17//! ## Examples
18//!
19//! ```rust
20//! use legion_protocol::{IrcMessage, Command, Capability};
21//!
22//! // Parse an IRC message with tags
23//! let msg: IrcMessage = "@id=123;time=2023-01-01T00:00:00.000Z PRIVMSG #channel :Hello world"
24//!     .parse().unwrap();
25//!
26//! // Create a new message
27//! let msg = IrcMessage::new("PRIVMSG")
28//!     .with_params(vec!["#channel".to_string(), "Hello".to_string()])
29//!     .with_tag("id", Some("123".to_string()));
30//! ```
31
32#![warn(missing_docs, rustdoc::missing_crate_level_docs)]
33#![deny(unsafe_code)]
34
35pub mod error;
36pub mod message;
37pub mod command;
38pub mod capabilities;
39pub mod sasl;
40pub mod validation;
41pub mod replies;
42pub mod iron;
43pub mod admin;
44
45#[cfg(feature = "bleeding-edge")]
46pub mod bleeding_edge;
47
48// Re-export main types for convenience
49pub use error::{IronError, Result};
50pub use message::IrcMessage;
51pub use command::Command;
52pub use capabilities::{Capability, CapabilitySet, CapabilityHandler};
53pub use replies::Reply;
54pub use utils::ChannelType;
55pub use iron::{IronSession, IronVersion, IronNegotiationResult, IronChannelHandler, ChannelJoinResult, IronChannelError};
56pub use admin::{AdminOperation, MemberOperation, BanOperation, KeyOperation, MemberRole, ChannelMode, 
57               ChannelSettings, AdminResult, ChannelAdmin, Permission};
58
59#[cfg(feature = "bleeding-edge")]
60pub use bleeding_edge::{MessageReply, MessageReaction, ReactionAction};
61
62/// Protocol constants used throughout the IRC specification
63pub mod constants {
64    /// Maximum length of an IRC message (excluding tags)
65    pub const MAX_MESSAGE_LENGTH: usize = 512;
66    
67    /// Maximum length of message tags section
68    pub const MAX_TAG_LENGTH: usize = 8191;
69    
70    /// Maximum number of parameters in a message
71    pub const MAX_PARAMS: usize = 15;
72    
73    /// Maximum length of a capability name
74    pub const MAX_CAPABILITY_NAME_LENGTH: usize = 64;
75    
76    /// Maximum length of a nickname
77    pub const MAX_NICK_LENGTH: usize = 32;
78    
79    /// Maximum length of a channel name
80    pub const MAX_CHANNEL_LENGTH: usize = 50;
81    
82    /// Default IRC port (plaintext)
83    pub const DEFAULT_IRC_PORT: u16 = 6667;
84    
85    /// Default IRC over TLS port
86    pub const DEFAULT_IRCS_PORT: u16 = 6697;
87}
88
89/// Utility functions for IRC protocol handling
90pub mod utils {
91    use crate::constants::*;
92    
93    /// Channel type enumeration for Legion Protocol
94    #[derive(Debug, Clone, PartialEq, Eq)]
95    pub enum ChannelType {
96        /// Standard IRC global channel (#channel)
97        IrcGlobal,
98        /// Standard IRC local channel (&channel)  
99        IrcLocal,
100        /// Legion Protocol encrypted channel (!channel)
101        LegionEncrypted,
102        /// Invalid/unknown channel type
103        Invalid,
104    }
105    
106    /// Determine the type of a channel based on its prefix
107    pub fn get_channel_type(channel: &str) -> ChannelType {
108        if channel.is_empty() {
109            return ChannelType::Invalid;
110        }
111        
112        match channel.chars().next().unwrap() {
113            '#' => ChannelType::IrcGlobal,
114            '&' => ChannelType::IrcLocal,
115            '!' => ChannelType::LegionEncrypted,
116            _ => ChannelType::Invalid,
117        }
118    }
119    
120    /// Check if a channel is a Legion Protocol encrypted channel
121    pub fn is_legion_encrypted_channel(channel: &str) -> bool {
122        matches!(get_channel_type(channel), ChannelType::LegionEncrypted)
123    }
124    
125    /// Legacy alias for backward compatibility
126    #[deprecated(note = "Use is_legion_encrypted_channel instead")]
127    pub fn is_iron_encrypted_channel(channel: &str) -> bool {
128        is_legion_encrypted_channel(channel)
129    }
130    
131    /// Check if a channel is a standard IRC channel (# or &)
132    pub fn is_standard_irc_channel(channel: &str) -> bool {
133        matches!(get_channel_type(channel), ChannelType::IrcGlobal | ChannelType::IrcLocal)
134    }
135    
136    /// Check if a string is a valid IRC nickname
137    pub fn is_valid_nick(nick: &str) -> bool {
138        if nick.is_empty() || nick.len() > MAX_NICK_LENGTH {
139            return false;
140        }
141        
142        // First character must be letter or special character
143        let first = nick.chars().next().unwrap();
144        if !first.is_ascii_alphabetic() && !matches!(first, '[' | ']' | '\\' | '`' | '_' | '^' | '{' | '|' | '}') {
145            return false;
146        }
147        
148        // Remaining characters can be alphanumeric, hyphen, or special characters
149        nick.chars().skip(1).all(|c| {
150            c.is_ascii_alphanumeric() || matches!(c, '[' | ']' | '\\' | '`' | '_' | '^' | '{' | '|' | '}' | '-')
151        })
152    }
153    
154    /// Check if a string is a valid IRC channel name
155    pub fn is_valid_channel(channel: &str) -> bool {
156        if channel.is_empty() || channel.len() > MAX_CHANNEL_LENGTH {
157            return false;
158        }
159        
160        // Must start with # or &
161        if !channel.starts_with('#') && !channel.starts_with('&') {
162            return false;
163        }
164        
165        // Cannot contain spaces, control characters, or commas
166        !channel.chars().any(|c| c.is_control() || c == ' ' || c == ',' || c == '\x07')
167    }
168    
169    /// Check if a string is a valid Legion Protocol encrypted channel name
170    pub fn is_valid_legion_channel(channel: &str) -> bool {
171        if channel.is_empty() || channel.len() > MAX_CHANNEL_LENGTH {
172            return false;
173        }
174        
175        // Must start with !
176        if !channel.starts_with('!') {
177            return false;
178        }
179        
180        // Cannot contain spaces, control characters, or commas
181        !channel.chars().any(|c| c.is_control() || c == ' ' || c == ',' || c == '\x07')
182    }
183    
184    /// Legacy alias for backward compatibility
185    #[deprecated(note = "Use is_valid_legion_channel instead")]
186    pub fn is_valid_iron_channel(channel: &str) -> bool {
187        is_valid_legion_channel(channel)
188    }
189    
190    /// Check if a string is a valid channel name (IRC or Legion)
191    pub fn is_valid_any_channel(channel: &str) -> bool {
192        is_valid_channel(channel) || is_valid_legion_channel(channel)
193    }
194    
195    /// Escape IRC message text for safe transmission
196    pub fn escape_message(text: &str) -> String {
197        text.replace('\r', "")
198            .replace('\n', " ")
199            .replace('\0', "")
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use super::utils::*;
207    
208    #[test]
209    fn test_valid_nicks() {
210        assert!(is_valid_nick("Alice"));
211        assert!(is_valid_nick("Bob123"));
212        assert!(is_valid_nick("user_name"));
213        assert!(is_valid_nick("[Bot]"));
214        assert!(is_valid_nick("test-user"));
215    }
216    
217    #[test]
218    fn test_invalid_nicks() {
219        assert!(!is_valid_nick(""));
220        assert!(!is_valid_nick("123user")); // Can't start with number
221        assert!(!is_valid_nick("user name")); // No spaces
222        assert!(!is_valid_nick(&"a".repeat(50))); // Too long
223    }
224    
225    #[test]
226    fn test_valid_channels() {
227        assert!(is_valid_channel("#general"));
228        assert!(is_valid_channel("&local"));
229        assert!(is_valid_channel("#test-channel"));
230        assert!(is_valid_channel("#channel123"));
231    }
232    
233    #[test]
234    fn test_invalid_channels() {
235        assert!(!is_valid_channel(""));
236        assert!(!is_valid_channel("general")); // Must start with # or &
237        assert!(!is_valid_channel("#test channel")); // No spaces
238        assert!(!is_valid_channel("#test,channel")); // No commas
239        assert!(!is_valid_channel(&format!("#{}", "a".repeat(60)))); // Too long
240    }
241    
242    #[test]
243    fn test_valid_legion_channels() {
244        assert!(is_valid_legion_channel("!encrypted"));
245        assert!(is_valid_legion_channel("!secure-chat"));
246        assert!(is_valid_legion_channel("!room123"));
247        assert!(is_valid_legion_channel("!test_channel"));
248    }
249    
250    #[test] 
251    fn test_invalid_legion_channels() {
252        assert!(!is_valid_legion_channel(""));
253        assert!(!is_valid_legion_channel("encrypted")); // Must start with !
254        assert!(!is_valid_legion_channel("#encrypted")); // Wrong prefix
255        assert!(!is_valid_legion_channel("!test channel")); // No spaces
256        assert!(!is_valid_legion_channel("!test,channel")); // No commas
257        assert!(!is_valid_legion_channel(&format!("!{}", "a".repeat(60)))); // Too long
258    }
259    
260    #[test]
261    fn test_backward_compatibility_iron_channels() {
262        // Test that the old function still works (with deprecation warning)
263        #[allow(deprecated)]
264        fn test_old_function() {
265            assert!(is_valid_iron_channel("!encrypted"));
266            assert!(!is_valid_iron_channel("#encrypted"));
267        }
268        test_old_function();
269    }
270    
271    #[test]
272    fn test_channel_type_detection() {
273        use utils::*;
274        
275        assert_eq!(get_channel_type("#general"), ChannelType::IrcGlobal);
276        assert_eq!(get_channel_type("&local"), ChannelType::IrcLocal);
277        assert_eq!(get_channel_type("!encrypted"), ChannelType::LegionEncrypted);
278        assert_eq!(get_channel_type("invalid"), ChannelType::Invalid);
279        assert_eq!(get_channel_type(""), ChannelType::Invalid);
280        
281        assert!(is_legion_encrypted_channel("!encrypted"));
282        assert!(!is_legion_encrypted_channel("#general"));
283        
284        // Test backward compatibility
285        #[allow(deprecated)]
286        {
287            assert!(is_iron_encrypted_channel("!encrypted"));
288            assert!(!is_iron_encrypted_channel("#general"));
289        }
290        
291        assert!(is_standard_irc_channel("#general"));
292        assert!(is_standard_irc_channel("&local"));
293        assert!(!is_standard_irc_channel("!encrypted"));
294    }
295}