diff --git a/Cargo.toml b/Cargo.toml index 725dbf5..46a9916 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ tokio-native-tls = "0.3.1" native-tls = "0.2.11" rand = "0.8.5" toml = "0.8.10" +base64 = "0.22.0" serde = { version = "1.0.197", features = ["derive"] } colored = "2.1.0" futures = "0.3.30" diff --git a/config.toml b/config.toml index 966f19f..1bc452f 100644 --- a/config.toml +++ b/config.toml @@ -2,5 +2,7 @@ server = "irc.supernets.org" port = 6697 use_ssl = true nickname = "g1r" -channel = "#superbowl" - +channel = "#dev" +sasl_username = "g1r" +sasl_password = "fuckyou.lol" +capabilities = ["sasl"] diff --git a/src/main.rs b/src/main.rs index 55c5f18..a3aae51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,8 +14,16 @@ struct Config { use_ssl: bool, nickname: String, channel: String, + sasl_username: Option, + sasl_password: Option, + capabilities: Option> } +mod mods { + pub mod sasl; +} +use mods::sasl::{start_sasl_auth, handle_sasl_messages}; + #[tokio::main(flavor = "multi_thread", worker_threads = 12)] async fn main() -> Result<(), Box> { println!("Loading Config..."); @@ -39,13 +47,12 @@ async fn main() -> Result<(), Box> { let (reader, writer) = split(tls_stream); let (tx, mut rx) = mpsc::channel(1000); - // Spawn a task to handle reading let read_task = tokio::spawn(async move { let mut reader = reader; let mut buf = vec![0; 4096]; loop { let n = match reader.read(&mut buf).await { - Ok(0) => return, // connection was closed + Ok(0) => return, // connection has been killed x.x Ok(n) => n, Err(e) => { eprintln!("Error reading from socket: {:?}", e); @@ -58,19 +65,44 @@ async fn main() -> Result<(), Box> { eprintln!("Error sending message to the channel"); return; } + if msg.contains("AUTHENTICATE") || msg.contains("903") { + handle_sasl_messages(&mut writer, &msg, &config.sasl_username.unwrap(), &config.sasl_password.unwrap()).await.unwrap(); + } } + //let msg = String::from_utf8_lossy(&buf[..n]).to_string(); + //if msg.contains("AUTHENTICATE") || msg.contains("903") { + // handle_sasl_messages(&mut writer, &msg, &sasl_username.unwrap(), &sasl_password.unwrap()).await; + //} }); + // capabilities + let mut capabilities = config.capabilities.clone().unwrap_or_else(Vec::new); + //if config.capabilities.is_some() && config.sasl_password.is_some() && config.sasl_username.is_some() { + if config.sasl_username.is_some() && config.sasl_password.is_some() && !capabilities.contains(&"sasl".to_string()) { + capabilities.push("sasl".to_string()); + } let write_task = tokio::spawn(async move { let mut writer = writer; - writer.write_all(format!("NICK {}\r\n", config.nickname).as_bytes()).await.unwrap(); - writer.write_all(format!("USER {} 0 * :{}\r\n", config.nickname, config.nickname).as_bytes()).await.unwrap(); - writer.write_all(format!("JOIN {}\r\n", config.channel).as_bytes()).await.unwrap(); + + // capabilities + //let capabilities = config.capabilities.clone().unwrap_or_else(Vec::new); + if !capabilities.is_empty() { + let cap_req = format!("CAP REQ :{}\r\n", capabilities.join(" ")); + writer.write_all(cap_req.as_bytes()).await.unwrap(); + // handle that CAP ACK yo + } // proceeding with sasl if creds are availble + if let (Some(sasl_username), Some(sasl_password)) = (&config.sasl_username, &config.sasl_password) { + start_sasl_auth(&mut writer, "PLAIN", &config.nickname, &capabilities).await.unwrap(); + } else { + writer.write_all(format!("NICK {}\r\n", config.nickname).as_bytes()).await.unwrap(); + writer.write_all(format!("USER {} 0 * :{}\r\n", config.nickname, config.nickname).as_bytes()).await.unwrap(); + } writer.flush().await.unwrap(); while let Some(msg) = rx.recv().await { // handle messages better println!("{} {}", "[%] DEBUG:".bold().green(), msg.purple()); + if msg.starts_with("PING") { writer.write_all(format!("PONG {}\r\n", &msg[5..]).as_bytes()).await.unwrap(); } diff --git a/src/mods/sasl.rs b/src/mods/sasl.rs new file mode 100644 index 0000000..020c1ba --- /dev/null +++ b/src/mods/sasl.rs @@ -0,0 +1,54 @@ +// mods/sasl.rs +use base64::Engine; +use tokio::io::AsyncWriteExt; +/// Sends the initial commands to negotiate capabilities and start SASL authentication. +pub async fn start_sasl_auth( +//pub async fn start_sasl_auth(...) -> Result<(), Box> { + writer: &mut W, + mechanism: &str, + nickname: &str, + capabilities: &[String], // Add a parameter for capabilities +) -> Result<(), Box> { + // Request a list of capabilities from the server + writer.write_all(b"CAP LS 302\r\n").await?; + + // Send NICK and USER commands + let nick_cmd = format!("NICK {}\r\n", nickname); + writer.write_all(nick_cmd.as_bytes()).await?; + let user_cmd = format!("USER {} 0 * :{}\r\n", nickname, nickname); + writer.write_all(user_cmd.as_bytes()).await?; + + // Request specific capabilities, including 'sasl' for SASL authentication + if !capabilities.is_empty() { + let cap_req_cmd = format!("CAP REQ :{}\r\n", capabilities.join(" ")); + writer.write_all(cap_req_cmd.as_bytes()).await?; + } else { + // If no specific capabilities are requested, directly request 'sasl' + writer.write_all(b"CAP REQ :sasl\r\n").await?; + } + + writer.flush().await?; + Ok(()) +} + +/// Continues the SASL authentication process based on the server's responses. +//pub async fn handle_sasl_messages(...) -> Result<(), Box> { +pub async fn handle_sasl_messages( + writer: &mut W, + message: &str, + username: &str, + password: &str, +) -> Result<(), Box> { + if message.contains("CAP * ACK :sasl") { + writer.write_all(b"AUTHENTICATE PLAIN\r\n").await?; + } else if message.starts_with("AUTHENTICATE +") { + let auth_string = format!("\0{}\0{}", username, password); + let encoded = base64::engine::general_purpose::STANDARD.encode(auth_string); + writer.write_all(format!("AUTHENTICATE {}\r\n", encoded).as_bytes()).await?; + } else if message.contains("903 * :SASL authentication successful") { + writer.write_all(b"CAP END\r\n").await?; + } + writer.flush().await?; + Ok(()) +} +