diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa04883 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +Cargo.lock +/target + diff --git a/Cargo.toml b/Cargo.toml index 30275af..d5cc9b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,16 +6,15 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -openssl = "0.10.45" -async-openai = "0.6.1" -tokio = { version = "1.23.0", features = ["full"] } -rand = "0.8.4" -regex = "1.7.1" -toml = "0.7.2" -serde = "1.0.152" -tokio-openssl = "0.6.3" -colored = "2" +tokio = { version = "1.36.0", features = ["full"] } tokio-socks = "0.5.1" -socks = "0.3.4" -random_word = "0.3.0" -#leetspeak = "0.2.0" +tokio-rustls = "0.25.0" +tokio-native-tls = "0.3.1" +native-tls = "0.2.11" +rand = "0.8.5" +toml = "0.8.10" +base64 = "0.22.0" +regex = "1.10.3" +serde = { version = "1.0.197", features = ["derive"] } +colored = "2.1.0" +futures = "0.3.30" diff --git a/config.toml b/config.toml index 88ec755..6e1a529 100644 --- a/config.toml +++ b/config.toml @@ -1,18 +1,26 @@ -nick = "g1r" -server = "ircd.chat" +#[server] +server = "198.98.52.138" #"irc.supernets.org" port = 6697 -password = "" -channels = [ "#tcpdirect", "#macros", "#b0tsh0p" ] #add key access -admin_users = ["s4d", "s4d[m]", "g"] # add host identification -ignore_users = ["maple", "aibird", "professorOak", "van"] -openai = "sk-" -model = "text-davinci-003" -accents = "funny, completely insane, and hyperactive" -personalities = "GIR from Invader Zim" +use_ssl = true -proxy_server = "127.0.0.1" -proxy_port = 9050 +#[user] +nickname = "g1r" +realname = "git.supernets.org/sad/g1r" +channels = ["#dev", "#superbowl", "#5000"] +sasl_username = "" +sasl_password = "" +capabilities = ["sasl"] + +#[proxy] +use_proxy = false +proxy_type = "socks5" +proxy_addr = "127.0.0.1" +proxy_port = 1080 +proxy_username = "" +proxy_password = "" + +#[features] +kickrejoin = true +ascii_art = "./ircart/ircart" +pump_delay = 0 # in milliseconds -# INVADER SETTINGS -invaders = ["d1b", "z1m", "t4k", "m3mbr4n3", "g4z", "4l4n", "m1yuk1", "purpl3", "r3d", "5p0rk", "t3nn", "l4rb", "ch1nn", "d00ky", "3l", "fl0rb3", "g00ch", "gr4p4", "gr00t", "k00t", "k1m", "krunk", "l4dn4r", "n3n", "p3st0", "p00t"] -proxy_list = "./proxies.txt" diff --git a/proxies.txt b/proxies.txt deleted file mode 100644 index 290dda7..0000000 --- a/proxies.txt +++ /dev/null @@ -1,78 +0,0 @@ -184.185.2.12:4145 -174.138.33.62:59166 -206.220.175.2:4145 -96.126.113.216:59166 -205.240.77.164:4145 -174.75.211.222:4145 -72.210.252.137:4145 -192.111.129.145:16894 -68.71.254.6:4145 -159.203.13.82:59166 -107.170.81.141:59166 -159.65.220.89:59166 -192.241.143.216:59166 -149.56.247.67:59166 -157.245.223.201:59166 -203.57.114.228:59166 -192.111.135.18:18301 -192.252.216.81:4145 -68.183.90.210:59166 -123.171.245.162:1080 -72.195.34.58:4145 -208.111.40.144:59166 -98.162.25.29:31679 -192.252.214.20:15864 -95.213.228.10:59166 -142.93.77.151:59166 -72.195.114.169:4145 -192.111.135.17:18302 -75.119.157.170:59166 -75.127.13.195:59166 -167.71.10.234:59166 -51.255.219.244:59166 -51.178.82.49:59166 -184.178.172.14:4145 -68.71.247.130:4145 -192.111.137.37:18762 -176.58.125.165:59166 -205.186.162.190:59166 -47.243.95.228:10080 -142.54.228.193:4145 -168.196.160.61:59166 -138.68.124.120:59166 -174.138.63.144:59166 -74.119.147.209:4145 -68.183.20.254:59166 -67.205.181.126:59166 -139.144.31.132:59166 -159.89.49.172:59166 -43.226.26.120:59166 -23.253.253.26:59166 -206.189.157.253:59166 -184.170.248.5:4145 -159.203.78.174:59166 -46.101.37.189:59166 -184.170.249.65:4145 -192.111.137.34:18765 -167.71.190.131:59166 -174.64.199.79:4145 -184.178.172.25:15291 -45.79.46.53:59166 -40.87.45.45:59166 -199.58.184.97:4145 -198.8.94.170:4145 -192.252.208.67:14287 -184.170.245.148:4145 -69.164.224.53:59166 -68.71.249.153:48606 -143.198.229.170:59166 -165.227.153.96:59166 -192.252.220.89:4145 -138.197.185.192:59166 -151.80.45.47:59166 -69.194.181.6:7497 -170.210.156.33:59166 -209.94.62.123:59166 -198.8.94.174:39078 -184.178.172.23:4145 -72.221.232.155:4145 \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 857d908..5094c33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,151 +1,270 @@ -use std::io::prelude::*; -use std::net::TcpStream; -use std::io::Write; -use openssl::ssl::{SslMethod, SslConnector}; -use toml::Value; +use tokio::io::{split, AsyncRead, AsyncWrite, AsyncReadExt, AsyncWriteExt, BufReader, AsyncBufReadExt}; +use tokio::net::TcpStream; +use tokio_native_tls::native_tls::TlsConnector as NTlsConnector; +use tokio_native_tls::TlsConnector; +use tokio::sync::mpsc; use serde::Deserialize; +use std::fs; + +use std::sync::atomic::{AtomicBool, Ordering}; use colored::*; +use tokio_socks::tcp::Socks5Stream; - -mod modules { - pub trait Command { - fn handle(&self, message: &str) -> Vec; - } - pub mod ping; - pub mod kill; - pub mod ai; - pub mod invade; - pub mod test; - //pub mod ai_invade; -} - -use modules::ai::Ai; // FIX THIS BS -use modules::ping::PingCommand; -use modules::invade::InvadeCommand; -//use modules::ai_invade::AiInvadeCommand; -use modules::kill::KillCommand; // ... -use crate::modules::Command; - -#[derive(Deserialize)] +#[derive(Deserialize, Clone)] struct Config { server: String, port: u16, - nick: String, - password: String, + use_ssl: bool, + nickname: String, + realname: Option, channels: Vec, - admin_users: Vec, - ignore_users: Vec, - + sasl_username: Option, + sasl_password: Option, + capabilities: Option>, + + reconnect_delay: u64, + reconnect_attempts: u64, + + // Proxy + use_proxy: bool, + // proxy_type: Option, + proxy_addr: Option, + proxy_port: Option, + proxy_username: Option, + proxy_password: Option, + + ascii_art: Option, + pump_delay: u64, } -fn main() { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); +mod mods { + pub mod proxy; + pub mod tls; + pub mod handler; + pub mod sasl; + pub mod sed; + pub mod ascii; + pub mod vomit; +// pub mod invade; +} +use mods::proxy::proxy_exec; +use mods::tls::tls_exec; +use mods::handler::handler; +use mods::sasl::{start_sasl_auth, handle_sasl_messages}; +use mods::sed::{SedCommand, MessageBuffer}; +use mods::ascii::handle_ascii_command; +use mods::vomit::handle_vomit_command; +//use mods::invade::{handle_invade_command}; - // LOAD CONFIG - let config_str = std::fs::read_to_string("config.toml").unwrap(); - let config_value = config_str.parse::().unwrap(); - let config: Config = config_value.try_into().unwrap(); - // GIVE THE SERVER A SLOPPPY SPAM OF RETARDEDNESS - let stream = TcpStream::connect(format!("{}:{}", config.server, config.port)).unwrap(); - let connector = SslConnector::builder(SslMethod::tls()).unwrap().build(); - // DONT DO DRUGS YOU WILL END UP LIKE ME - let mut ssl_stream = connector.connect(&config.server, stream).unwrap(); - let nick_command = format!("NICK {}_\r\n", config.nick); //setup passwords - let user_command = format!("USER {} 0 * :{}\r\n", config.nick, config.nick); - ssl_stream.write_all(nick_command.as_bytes()).unwrap(); - ssl_stream.write_all(user_command.as_bytes()).unwrap(); - let identify_command = format!("PRIVMSG NickServ :IDENTIFY {} {}\r\n", config.nick, config.password); - ssl_stream.write(identify_command.as_bytes()).unwrap(); - let channels = config.channels.join(","); - let join_command = format!("JOIN {}\r\n", channels); - - let admin_users = config.admin_users; // ADMINS - let ignored_users = config.ignore_users; // IGNORED -// ... - ssl_stream.write_all(join_command.as_bytes()).unwrap(); +#[tokio::main(flavor = "multi_thread", worker_threads = 12)] +async fn main() -> Result<(), Box> { + println!("Loading Config..."); + let config = loaded_config().expect("Error parsing config.toml"); + println!("Config loaded!"); + let mut reconnect_attempts = 0; - let mut buf = [0; 512]; - loop { - match ssl_stream.read(&mut buf) { - Ok(0) => break, - Ok(n) => { - let received = String::from_utf8_lossy(&buf[0..n]); - let message = received.trim(); + while reconnect_attempts < config.reconnect_attempts { + let configc = config.clone(); - //debug chat - println!("{} {}","[%] DEBUG:".bold().green(), received.purple()); - - - // RESPOND TO PINGS - if message.starts_with("PING") { - println!("{} {}","[%] PONG:".bold().green(), config.nick.blue()); // DEBUG - ssl_stream.write_all("PONG ircd.chat\r\n".as_bytes()).unwrap(); - continue; // skip processing the PING message further + let server = format!("{}:{}", configc.server, configc.port); + let connection_result = tokio::spawn(async move { + let config = configc.clone(); + let tcp_stream = if config.use_proxy { + match proxy_exec(&config).await { + Ok(stream) => stream, + Err(e) => { + println!("Error connecting to proxy: {}", e); + return Ok::<(), Box>(()); + } } - - // MODULES - let ping_command = PingCommand; - let kill_command = KillCommand; - let invade_command = InvadeCommand; - //let ai_invade_command = AiInvadeCommand; - - //let test_command = TestCommand; - let ai = Ai; - - // ADMIN MODULES - if message.starts_with(":") && message.contains(" :%") { - let parts: Vec<&str> = message.splitn(2, ' ').collect(); // Check if user is admin_user - let username = parts[0].trim_start_matches(':').split("!").next().unwrap(); - if !admin_users.contains(&username.to_string()) { - println!("{} {}","[!] UNAUTHORIZED:".bold().clear().on_red(), username.red().bold()); - continue; // ... + } else { + match TcpStream::connect(server).await { + Ok(stream) => stream, + Err(e) => { + println!("Error connecting to server: {}", e); + return Ok::<(), Box>(()); } - if message.contains(":%ping") { - for response in ping_command.handle(message) { - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - } else if message.contains(":%kill") { - for response in kill_command.handle(message) { - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - } else if message.contains(":%invade") { - for response in invade_command.handle(message) { - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - } //else if message.contains(":%aiinvade") { - // for response in ai_invade_command.handle(message) { - // ssl_stream.write_all(response.as_bytes()).unwrap(); - // } - //} } + }; - // Check if the message is user and respond via ai - else if message.starts_with(":") && message.contains("PRIVMSG ") && message.contains(&config.nick) { //modify for on mention - let channel = message.split("PRIVMSG ").nth(1).and_then(|s| s.splitn(2, ' ').next()).unwrap(); - if !channels.contains(&channel) { - continue; + if config.use_ssl { + println!("Connecting to SSL server..."); + match tls_exec(&config, tcp_stream).await { + Ok(tls_stream) => handler(tls_stream, config).await.unwrap(), + Err(e) => { + println!("Error establishing TLS connection: {}", e); + return Ok::<(), Box>(()); } - // extract the username from the first part and check if ignored - let parts: Vec<&str> = message.splitn(2, ' ').collect(); // split the message into two parts at the first space - let username = parts[0].trim_start_matches(':').split("!").next().unwrap(); - if ignored_users.contains(&username.to_string()) { - println!("[!] IGNORED: {}", username.red()); - continue; - } - for response in ai.handle(message, ) { - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - } + } else { + println!("Connecting to Non-SSL server..."); + handler(tcp_stream, config).await.unwrap(); + } + Ok::<(), Box>(()) + }).await.unwrap(); + match connection_result { + Ok(_) => { + println!("Connection established successfully!"); + reconnect_attempts = 0; }, Err(e) => { - println!("[!] ERROR: {}", e); - break; - }, + println!("Error handling connection: {}", e); + reconnect_attempts += 1; + tokio::time::sleep(tokio::time::Duration::from_secs(config.reconnect_delay)).await; + } + } + } + println!("Reconnect attempts exceeded. Exiting..."); + Ok(()) +} + +/// Load the config file +fn loaded_config() -> Result> { + let config_contents = fs::read_to_string("config.toml")?; + let config: Config = toml::from_str(&config_contents)?; + Ok(config) +} + +/// Read messages from the server +async fn readmsg(mut reader: tokio::io::ReadHalf, tx: tokio::sync::mpsc::Sender) where S: AsyncRead + Unpin { + let mut buf = vec![0; 4096]; + while let Ok (n) = reader.read(&mut buf).await { + if n == 0 { break; } + let msg_list = String::from_utf8_lossy(&buf[..n]).to_string(); + for lines in msg_list.lines() { + let msg = lines.to_string(); + println!("{}{}{} {}{} {}", "[".green().bold(), ">".yellow().bold(), "]".green().bold(), "DEBUG:".bold().yellow(), ":".bold().green(), msg.trim().purple()); + tx.send(msg).await.unwrap(); + if buf.len() == n { + buf.resize(buf.len() * 2, 0); + } } } } + +static SASL_AUTH: AtomicBool = AtomicBool::new(false); + +/// Write messages to the server +async fn writemsg(mut writer: tokio::io::WriteHalf, mut rx: tokio::sync::mpsc::Receiver, config: &Config, mut message_buffer: MessageBuffer) where S: AsyncWrite + Unpin { + let username = config.sasl_username.clone().unwrap(); + let password = config.sasl_password.clone().unwrap(); + let nickname = config.nickname.clone(); + let realname = config.realname.clone().unwrap_or(nickname.clone()); + if !password.is_empty() && !SASL_AUTH.load(Ordering::Relaxed) { + let capabilities = config.capabilities.clone(); + println!("Starting SASL auth..."); + start_sasl_auth(&mut writer, "PLAIN", &nickname, &realname, capabilities).await.unwrap(); + writer.flush().await.unwrap(); + SASL_AUTH.store(true, Ordering::Relaxed); + } else { + nickme(&mut writer, &nickname, &realname).await.unwrap(); + writer.flush().await.unwrap(); + } + + while let Some(msg) = rx.recv().await { + let msg = msg.trim(); + if msg.is_empty() { + continue; + } + let parts = msg.split(' ').collect::>(); + let serv = parts.first().unwrap_or(&""); + let cmd = parts.get(1).unwrap_or(&""); + + println!("{} {} {} {} {}", "DEBUG:".bold().yellow(), "serv:".bold().green(), serv.purple(), "cmd:".bold().green(), cmd.purple()); + if *serv == "PING" { + let response = msg.replace("PING", "PONG") + "\r\n"; + println!("{} {} {}","[%] PONG:".bold().green(), nickname.blue(), response.purple()); + writer.write_all(response.as_bytes()).await.unwrap(); + writer.flush().await.unwrap(); + continue; + } + if (*cmd == "CAP" || msg.starts_with("AUTHENTICATE +") || *cmd == "903") && SASL_AUTH.load(Ordering::Relaxed) { + println!("Handling SASL messages..."); + handle_sasl_messages(&mut writer, msg.trim(), &username, &password, &nickname).await.unwrap(); + writer.flush().await.unwrap(); + } + if *cmd == "001" { + println!("Setting mode"); + writer.write_all(format!("MODE {} +B\r\n", nickname).as_bytes()).await.unwrap(); + writer.flush().await.unwrap(); + } + + if *cmd == "376" { + println!("Joining channels"); + for channel in &config.channels { + writer.write_all(format!("JOIN {}\r\n", channel).as_bytes()).await.unwrap(); + writer.flush().await.unwrap(); + } + } + if *cmd == "KICK" { + let channel = parts.get(2).unwrap_or(&""); + let userme = parts.get(3).unwrap_or(&""); + if *userme == nickname { + writer.write_all(format!("JOIN {}\r\n", channel).as_bytes()).await.unwrap(); + writer.flush().await.unwrap(); + } + } + if *cmd == "PRIVMSG" { + let channel = &parts.get(2).to_owned().unwrap_or(&""); + let user = parts[0].strip_prefix(':') + .and_then(|user_with_host| user_with_host.split('!').next()) + .unwrap_or("unknown_user"); + let host = parts[0].split('@').nth(1).unwrap_or("unknown_host"); + let msg_content = if parts.len() > 3 { + let remainder = &parts[3..].join(" "); + if let Some(pos) = remainder.find(':') { + let (first_part, last_part) = remainder.split_at(pos); + format!("{}{}", first_part, &last_part[1..]) + } else { + remainder.to_string() + } + } else { + "".to_string() + }; + println!("{} {} {} {} {} {} {} {} {}", "DEBUG:".bold().yellow(), "channel:".bold().green(), channel.purple(), "user:".bold().green(), user.purple(), "host:".bold().green(), host.purple(), "msg:".bold().green(), msg_content.yellow()); + + // sed + if msg_content.starts_with("s/") { + if let Some(sed_command) = SedCommand::parse(&msg_content.clone()) { + if let Some(response) = message_buffer.apply_sed_command(&sed_command) { + writer.write_all(format!("PRIVMSG {} :{}\r\n", channel, response).as_bytes()).await.unwrap(); + writer.flush().await.unwrap(); + } + } + } else { + message_buffer.add_message(msg_content.clone().to_string()); + } + + // ansi art + if msg_content.starts_with("%ascii") { + let _ = handle_ascii_command(&mut writer, config, &msg_content, channel).await; + } + + // vomit + if msg_content.starts_with("%vomit") { + let _ = handle_vomit_command(&mut writer, config, &msg_content, channel).await; + } + + // invade +// if msg_content.starts_with("%invade") { +// let _ = handle_vomit_command(&mut writer, config, &msg_content, channel).await; +// } + // other commands here + } + } +} + + + +async fn nickme(writer: &mut W, nickname: &str, realname: &str) -> Result<(), Box> { + writer.write_all(format!("NICK {}\r\n", nickname).as_bytes()).await?; + writer.flush().await?; + writer.write_all(format!("USER {} 0 * :{}\r\n", nickname, realname).as_bytes()).await?; + writer.flush().await?; + Ok(()) +} + + + + diff --git a/src/mods/ascii.rs b/src/mods/ascii.rs new file mode 100644 index 0000000..8353bc0 --- /dev/null +++ b/src/mods/ascii.rs @@ -0,0 +1,160 @@ +use tokio::io::{AsyncWriteExt, BufReader}; +use tokio::fs::File; +use tokio::time::{self, Duration}; +use std::fs; +use rand::Rng; +use tokio::io::AsyncBufReadExt; +use std::error::Error; +use crate::Config; + +const CHUNK_SIZE: usize = 4096; + +async fn send_ansi_art(writer: &mut W, file_path: &str, pump_delay: u64, channel: &str) -> Result<(), Box> { + let file = File::open(file_path).await?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + + let mut line_count = 0; + let lines_stream = &mut lines; + while let Ok(Some(_)) = lines_stream.next_line().await { + line_count += 1; + } + let mut pump_delay = Duration::from_millis(pump_delay); + if line_count > 500 && pump_delay < Duration::from_millis(100){ + pump_delay = Duration::from_millis(100); + } + let file = File::open(file_path).await?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + + while let Some(line) = lines.next_line().await? { + + if line.len() > CHUNK_SIZE { + for chunk in line.as_bytes().chunks(CHUNK_SIZE) { + writer.write_all(format!("PRIVMSG {} :{}\r\n", channel, String::from_utf8_lossy(chunk)).as_bytes()).await?; + writer.flush().await?; + time::sleep(pump_delay).await; + } + } else { + + writer.write_all(format!("PRIVMSG {} :{}\r\n", channel, line).as_bytes()).await?; + writer.flush().await?; + time::sleep(pump_delay).await; + } + } + Ok(()) +} + +fn select_random_file(dir: &str) -> Option { + let files = fs::read_dir(dir).ok()?.filter_map(|entry| { + let path = entry.ok()?.path(); + if path.is_file() { + path.to_str().map(ToString::to_string) + } else { + None + } + }).collect::>(); + + if files.is_empty() { + None + } else { + let mut rng = rand::thread_rng(); + let index = rng.gen_range(0..files.len()); + files.get(index).cloned() + } +} + +pub async fn handle_ascii_command( + writer: &mut W, + config: &Config, + command: &str, + channel: &str, +) -> Result<(), Box> { + let parts: Vec<&str> = command.split_whitespace().collect(); + let command_type = parts.get(1).unwrap_or(&""); + + if *command_type == "random" && parts.len() == 2 { + handle_random(writer, config, channel).await?; + } else if *command_type == "list"{ + handle_list(writer, config, channel, Some(parts.get(2).unwrap_or(&""))).await?; + } else { + handle_specific_file(writer, config, channel, &parts).await?; + } + + Ok(()) +} + +async fn handle_random( + writer: &mut W, + config: &Config, + channel: &str, +) -> Result<(), Box> { + if let Some(dir) = config.ascii_art.as_ref() { + if let Some(random_file) = select_random_file(dir) { + send_ansi_art(writer, &random_file, config.pump_delay, channel).await?; + } else { + writer.write_all(format!("PRIVMSG {} :No files found\r\n", channel).as_bytes()).await?; + } + } + Ok(()) +} + +async fn handle_list( + writer: &mut W, + config: &Config, + channel: &str, + parts: Option<&str> +) -> Result<(), Box> { + let base_dir = config.ascii_art.clone().unwrap_or_else(|| "ascii_art".to_string()); + + let dir = if let Some(subdir) = parts { + format!("{}/{}", base_dir, subdir) + } else { + base_dir + }; + + let entries = fs::read_dir(&dir) + .map_err(|_| "Failed to read directory")? + .filter_map(|entry| entry.ok()) + .map(|entry| { + let path = entry.path(); + let display_name = path.file_name().unwrap_or_default().to_string_lossy().into_owned(); + if path.is_dir() { + format!("{}/", display_name) + } else { + display_name.strip_suffix(".txt").unwrap_or(&display_name).to_string() + } + }) + .collect::>() + .join(", "); + + if entries.is_empty() { + writer.write_all(format!("PRIVMSG {} :No files or directories found\r\n", channel).as_bytes()).await?; + } else { + writer.write_all(format!("PRIVMSG {} :{}\r\n", channel, entries).as_bytes()).await?; + } + + Ok(()) +} + +async fn handle_specific_file( + writer: &mut W, + config: &Config, + channel: &str, + parts: &[&str], +) -> Result<(), Box> { + println!("{:?}", parts); + let file_name = if parts.len() > 2 { + parts[1..].join(" ").replace(' ', "/") + } else { + parts.get(1).unwrap_or(&"").to_string() + }; + println!("{:?}", file_name); + + let file_path = format!("{}/{}.txt", config.ascii_art.clone().unwrap_or_else(|| "ascii_art".to_string()), file_name); + println!("{:?}", file_path); + + send_ansi_art(writer, &file_path, config.pump_delay, channel).await +} + + diff --git a/src/mods/handler.rs b/src/mods/handler.rs new file mode 100644 index 0000000..4edab29 --- /dev/null +++ b/src/mods/handler.rs @@ -0,0 +1,24 @@ +// mods/handler.rs +use tokio::io::{AsyncRead, AsyncWrite, split}; +use tokio::sync::mpsc; +use crate::{Config, readmsg, writemsg, MessageBuffer}; + +/// Handle the connection to the server +pub async fn handler(stream: S, config: Config) -> Result<(), Box> where S: AsyncRead + AsyncWrite + Unpin + Send + 'static { + let (reader, writer) = split(stream); + let (tx, rx) = mpsc::channel(1000); + + let read_task = tokio::spawn(async move { + readmsg(reader, tx).await; + }); + + let message_buffer = MessageBuffer::new(1000); + + let write_task = tokio::spawn(async move { + writemsg(writer, rx, &config, message_buffer).await; + }); + + //let _ = tokio::try_join!(read_task, write_task); + tokio::try_join!(read_task, write_task).map_err(|e| Box::new(e) as Box)?; + Ok(()) +} diff --git a/src/mods/invade.rs b/src/mods/invade.rs new file mode 100644 index 0000000..85d7028 --- /dev/null +++ b/src/mods/invade.rs @@ -0,0 +1,5 @@ +// mods/invade.rs + + + + diff --git a/src/mods/proxy.rs b/src/mods/proxy.rs new file mode 100644 index 0000000..ca49aa0 --- /dev/null +++ b/src/mods/proxy.rs @@ -0,0 +1,28 @@ +// mods/proxy.rs +use tokio::net::TcpStream; +use tokio_socks::tcp::Socks5Stream; +use crate::Config; + +/// Establish a connection to the proxy +pub async fn proxy_exec(config: &Config) -> Result> { + let proxy_addr = match config.proxy_addr.as_ref() { + Some(addr) => addr, + None => "127.0.0.1", + }; + let proxy_port = config.proxy_port.unwrap_or(9050); + let proxy = format!("{}:{}", proxy_addr, proxy_port); + let server = format!("{}:{}", config.server, config.port); + let proxy_stream = TcpStream::connect(proxy).await.unwrap(); + let username = config.proxy_username.clone().unwrap(); + let password = config.proxy_password.clone().unwrap(); + let tcp_stream = if !&username.is_empty() && !password.is_empty() { + let tcp_stream = Socks5Stream::connect_with_password_and_socket(proxy_stream, server, &username, &password).await.unwrap(); + tcp_stream + } else { + let tcp_stream = Socks5Stream::connect_with_socket(proxy_stream, server).await.unwrap(); + tcp_stream + }; + let tcp_stream = tcp_stream.into_inner(); + + Ok(tcp_stream) +} diff --git a/src/mods/sasl.rs b/src/mods/sasl.rs new file mode 100644 index 0000000..5b94b34 --- /dev/null +++ b/src/mods/sasl.rs @@ -0,0 +1,45 @@ +// mods/sasl.rs +use crate::nickme; +use base64::Engine; +pub async fn start_sasl_auth( + writer: &mut W, + mechanism: &str, + nickname: &str, + realname: &str, + capabilities: Option>) -> Result<(), Box> { + writer.write_all(b"CAP LS 302\r\n").await?; + + nickme(writer, nickname, realname).await?; + + if let Some(caps) = capabilities { + if !caps.is_empty() { + let cap_req_cmd = format!("CAP REQ :{}\r\n", caps.join(" ")); + writer.write_all(cap_req_cmd.as_bytes()).await?; + } + } else { + writer.write_all(b"CAP REQ :sasl\r\n").await?; + } + //println!("Handling SASL messages..."); + writer.flush().await?; + Ok(()) +} + +pub async fn handle_sasl_messages( + writer: &mut W, + message: &str, + username: &str, + password: &str, + nickname: &str, +) -> Result<(), Box> { + if message.contains(format!("CAP {} ACK :sasl", nickname).as_str()) { + 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(()) +} diff --git a/src/mods/sed.rs b/src/mods/sed.rs new file mode 100644 index 0000000..808ad3e --- /dev/null +++ b/src/mods/sed.rs @@ -0,0 +1,70 @@ +use regex::Regex; +use std::collections::VecDeque; + +pub struct SedCommand { + pattern: Regex, + replacement: String, + global: bool, +} + +impl SedCommand { + pub fn parse(command: &str) -> Option { + let parts: Vec<&str> = command.split('/').collect(); + if parts.len() >= 4 { + let pattern = match Regex::new(parts[1]) { + Ok(regex) => regex, + Err(_) => return None, + }; + let replacement = parts[2].to_string(); + let global = parts.get(3).map_or(false, |&flag| flag.contains('g')); + + Some(SedCommand { + pattern, + replacement, + global, + }) + } else { + None + } + } + + pub fn apply_to(&self, message: &str) -> String { + if self.global { + self.pattern.replace_all(message, self.replacement.as_str()).to_string() + } else { + self.pattern.replace(message, self.replacement.as_str()).to_string() + } + } +} + +pub struct MessageBuffer { + buffer: VecDeque, + capacity: usize, +} + +impl MessageBuffer { + pub fn new(capacity: usize) -> Self { + MessageBuffer { + buffer: VecDeque::with_capacity(capacity), + capacity, + } + } + + pub fn add_message(&mut self, message: String) { + if self.buffer.len() == self.capacity { + self.buffer.pop_front(); + } + self.buffer.push_back(message); + } + + pub fn apply_sed_command(&mut self, command: &SedCommand) -> Option { + for message in self.buffer.iter_mut() { + if command.pattern.is_match(message) { + *message = command.apply_to(message); + return Some(message.clone()); + } + } + None + } +} + diff --git a/src/mods/tls.rs b/src/mods/tls.rs new file mode 100644 index 0000000..ddec428 --- /dev/null +++ b/src/mods/tls.rs @@ -0,0 +1,12 @@ +// mods/tls.rs +use tokio::net::TcpStream; +use tokio_native_tls::{TlsConnector, native_tls::TlsConnector as NTlsConnector}; +use crate::Config; + +// Establish a TLS connection to the server +pub async fn tls_exec(config: &Config, tcp_stream: TcpStream) -> Result, Box> { + let tls_builder = NTlsConnector::builder().danger_accept_invalid_certs(true).build().unwrap(); + let tls_connector = TlsConnector::from(tls_builder); + Ok(tls_connector.connect(&config.server, tcp_stream).await.unwrap()) + +} diff --git a/src/mods/vomit.rs b/src/mods/vomit.rs new file mode 100644 index 0000000..13f5889 --- /dev/null +++ b/src/mods/vomit.rs @@ -0,0 +1,77 @@ + +use rand::prelude::*; +use tokio::io::AsyncWriteExt; +use tokio::time; +use crate::Config; + +async fn generate_random_unicode() -> char { + let codepoint: u32 = thread_rng().gen_range(0..=0x10FFFF); + std::char::from_u32(codepoint).unwrap_or(' ') +} + +async fn generate_random_color() -> u8 { + thread_rng().gen_range(0..=15) +} + +async fn generate_random_format() -> char { + match thread_rng().gen_range(0..=2) { + 0 => '\x02', // Bold + 1 => '\x1D', // Italic + _ => '\x1F', // Underline + } +} + +async fn generate_vomit(length: usize) -> String { + let mut irc_text = String::new(); + for _ in 0..length { + let char = generate_random_unicode().await; + let color_code = generate_random_color().await; + let format_code = generate_random_format().await; + irc_text.push_str(&format!("\x03{:02}{}", color_code, format_code)); + irc_text.push(char); + } + irc_text +} + +fn split_into_chunks(s: &str, max_chunk_size: usize) -> Vec { + let mut chunks = Vec::new(); + let mut current_chunk = String::new(); + + for char in s.chars() { + if current_chunk.len() + char.len_utf8() > max_chunk_size { + chunks.push(current_chunk.clone()); + current_chunk.clear(); + } + current_chunk.push(char); + } + + if !current_chunk.is_empty() { + chunks.push(current_chunk); + } + + chunks +} + + +const CHUNK_SIZE: usize = 400; +// Function to handle the vomit command +pub async fn handle_vomit_command( + writer: &mut W, + config: &Config, + command: &str, + channel: &str, +) -> Result<(), Box> { + let parts: Vec<&str> = command.split_whitespace().collect(); + let length = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(1000); + + let vomit = generate_vomit(length).await; // Correctly awaited + let chunks = split_into_chunks(&vomit, CHUNK_SIZE); // Adjust if split_into_chunks is async + + for chunk in chunks { + writer.write_all(format!("PRIVMSG {} :{}\r\n", channel, chunk).as_bytes()).await?; + writer.flush().await?; + time::sleep(tokio::time::Duration::from_secs(config.pump_delay)).await; + } + + Ok(()) +} diff --git a/src/modules/ai.rs b/src/modules/ai.rs deleted file mode 100644 index ba7df92..0000000 --- a/src/modules/ai.rs +++ /dev/null @@ -1,89 +0,0 @@ -// Check if the message is user and respond via ai -use async_openai::{Client, types::{CreateCompletionRequestArgs}}; -use regex::Regex; -use crate::modules::Command; -use toml::Value; -use serde::Deserialize; -use colored::*; - -#[derive(Deserialize)] -struct Config { - nick: String, - openai: String, - model: String, - accents: String, - personalities: String, -} -pub struct Ai; - // setup a prompt and respnse log for training other bots -impl Command for Ai { - fn handle(&self, message: &str) -> Vec { - let mut responses = Vec::new(); - let config_str = std::fs::read_to_string("config.toml").unwrap(); - let config_value = config_str.parse::().unwrap(); - let config: Config = config_value.try_into().unwrap(); // respond to name with and without leet VVV - if message.starts_with(":") && message.contains("PRIVMSG ") && message.contains(&config.nick) { - let channel = message.split("PRIVMSG ").nth(1).and_then(|s| s.splitn(2, ' ').next()).unwrap(); // set the response to varible - let user_message = format!("The following is a chat log:\n{}\nRespond {} as you are chatting as {}: \n\n", - message.split(&format!("PRIVMSG {} :", channel.to_string())).nth(1).unwrap(), - config.accents, - config.personalities - ); - let parts: Vec<&str> = message.splitn(2, ' ').collect(); - let username = parts[0].trim_start_matches(':').split("!").next().unwrap(); - - let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(ai(&user_message, &username, &channel)); - responses.extend(result); - } - - responses - } -} -async fn ai(user_message: &str, username: &str, channel: &str) -> Vec { - let config_str = std::fs::read_to_string("config.toml").unwrap(); - let config_value = config_str.parse::().unwrap(); - let config: Config = config_value.try_into().unwrap(); - let api_key = config.openai; // set this from config - - let client = Client::new().with_api_key(api_key); - println!("{} {} {}: {}", "[?]".on_green().bold(), "PROMPT:".green().bold(), username, user_message); - let chat_request = CreateCompletionRequestArgs::default() - .prompt(user_message) - .max_tokens(40_u16) - .model(config.model) - .build() - .unwrap(); - let chat_response = client - .completions() - .create(chat_request) - .await - .unwrap(); - println!("{} {} {}","[+]".on_green().bold(), "RESPONSE:".green().bold(), chat_response.choices.first().unwrap().text); - //modify regex for varible username ie G1R g1r GIR gir but as handle nick for bots - let response_text = &chat_response.choices.first().unwrap().text; - let regex = Regex::new(r#""|[gG][1iI][rR]:\s"#).unwrap(); // THIS IS FUCKING UP EVERYTHING - //let nick = &config.nick; - //let regex_str = format!( - // r#""|[{}{}{}]|\b[gG][1iI][rR]:\s*|\b[mM][eE]:?\s"#, - // nick.to_lowercase(), - // nick.to_uppercase(), - // nick.chars().map(|c| match c { /// regex magic nick removal in progress - // 'a' => '4', - // 'e' => '3', - // 'i' => '1', - // 'o' => '0', - // 's' => '5', - // _ => c, - // }).collect::(), - //); - //let regex = Regex::new(®ex_str).unwrap(); - let response_text = regex.replace_all(response_text, "").trim().to_string(); - let response_lines = response_text.split("\n").filter(|line| !line.trim().is_empty()); - let mut responses = Vec::new(); - for line in response_lines { - responses.push(format!("PRIVMSG {} :\x0313{}\x0f: {}\r\n", channel, username, line)); - } - - responses -} diff --git a/src/modules/invade copy.rs b/src/modules/invade copy.rs deleted file mode 100644 index 5b7134c..0000000 --- a/src/modules/invade copy.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::modules::Command; - -use std::io::{BufRead, BufReader, Write}; -use std::net::TcpStream; -use openssl::ssl::{SslConnector, SslMethod}; -use serde::Deserialize; -use toml::{Value, to_string}; -use colored::*; - -// add better error handling -// add ai invasion -// add proxy support - -#[derive(Clone, Deserialize)] -struct Config { - //invaders: Vec, - server: String, - port: u16, - - -} - -pub struct InvadeCommand; - -impl Command for InvadeCommand { - fn handle(&self, message: &str) -> Vec { - let mut response = vec![]; - - if message.contains("PRIVMSG") && message.contains(":%invade") { - let parts: Vec<&str> = message.split_whitespace().collect(); - let num_invaders = parts[4].parse::().unwrap_or(1) as usize; - let channel = parts[2]; - let invadechannel = parts[5]; - let scream = if parts.len() > 6 { parts[6] } else { "" }; // read entire message - //message.split(&format!("PRIVMSG {} :", channel.to_string())).nth(1).unwrap(), - //let channel = message.split("PRIVMSG ").nth(1).and_then(|s| s.splitn(2, ' ').next()).unwrap(); - let config_str = std::fs::read_to_string("config.toml").unwrap(); - let config_value = config_str.parse::().unwrap(); - let config: Config = config_value.try_into().unwrap(); - - - for _invader in 1..=num_invaders {//&config.invaders[1..num_invaders] { //1..=20 { - let thread_channel = invadechannel.to_string(); - let config_clone = config.clone(); - let screaming = scream.to_string(); - let command_channel = channel.to_string(); - let thread_invader = random_word::gen(); // change to leetspeak on nick collision - - std::thread::spawn(move || { - let stream = TcpStream::connect((config_clone.server.as_str(), config_clone.port)).unwrap(); - let connector = SslConnector::builder(SslMethod::tls()).unwrap().build(); - let mut ssl_stream = connector.connect(config_clone.server.as_str(), stream).unwrap(); - let nick_command = format!("NICK {}\r\n", thread_invader); - let user_command = format!("USER {} 0 * :{}\r\n", thread_invader, thread_invader); - ssl_stream.write_all(nick_command.as_bytes()).unwrap(); - ssl_stream.write_all(user_command.as_bytes()).unwrap(); - let join_command = format!("JOIN {} \r\n", thread_channel); - let commander = format!("JOIN {} \r\n", command_channel); - ssl_stream.write_all(commander.as_bytes()).unwrap(); - ssl_stream.write_all(join_command.as_bytes()).unwrap(); - let msg = format!("PRIVMSG {} :{}\r\n", thread_channel, screaming); - ssl_stream.write_all(msg.as_bytes()).unwrap(); - - loop { - - - - let mut buf = [0; 512]; - match ssl_stream.ssl_read(&mut buf) { - Ok(0) => break, - Ok(n) => { - let received = String::from_utf8_lossy(&buf[0..n]); - let message = received.trim(); - - //debug chat - println!("{} {} {}","[%] DEBUG:".bold().green(), thread_invader.green(), received.blue()); - - if message.starts_with("PING") { - let response = message.replace("PING", "PONG"); - println!("{} {}","[%] PONG:".bold().green(), thread_invader.blue()); - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - // turn to mods - // setup so these will only run from the server admin to avoid handle/host conflicts - let commandi = format!("PRIVMSG {} :%%",command_channel); // & check for admin and verify with server - - if message.contains(&commandi) && message.contains(":%%") { - if message.contains("PRIVMSG") && message.contains(":%%join") { // fix so commands get picked up faster - let parts: Vec<&str> = message.splitn(3, ":%%join ").collect(); - let invade_channel = parts[1]; - let response = format!("JOIN {} \r\n", invade_channel); - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - if message.contains("PRIVMSG") && message.contains(":%%leave") { - let parts: Vec<&str> = message.splitn(3, ":%%leave ").collect(); - let invade_channel = parts[1]; - let response = format!("PART {} \r\n", invade_channel); - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - if message.contains("PRIVMSG") && message.contains(":%%scream") { - let parts: Vec<&str> = message.splitn(3, ":%%scream ").collect(); - let invade_channel = parts[1]; - if parts.len() == 2 { - let scream = parts[1]; - let response = format!("PRIVMSG {} :{}\r\n", invade_channel, scream); - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - } - } - // ...1 - } - Err(e) => { - eprintln!("{} {}","[!] ERROR FROM SERVER:".on_red(), e); - break; - } - } - } - }); - } - - response.push(format!("PRIVMSG {} :\x0304,01[!] INVADING {} WITH {} INVADERS...\x0f\r\n", channel, invadechannel, num_invaders)); - } - - response - } -} \ No newline at end of file diff --git a/src/modules/invade.rs b/src/modules/invade.rs deleted file mode 100644 index ba612f8..0000000 --- a/src/modules/invade.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::modules::Command; - -use std::io::{BufRead, BufReader, Write}; -use std::net::TcpStream; -use openssl::ssl::{SslConnector, SslMethod}; -use serde::Deserialize; -use toml::{Value, to_string}; -use colored::*; -use socks::*; -// add better error handling -// add ai invasion -// add proxy support - -#[derive(Clone, Deserialize)] -struct Config { - //invaders: Vec, - server: String, - port: u16, - - proxy_server: String, - proxy_port: u16, - -} - -pub struct InvadeCommand; - -impl Command for InvadeCommand { - fn handle(&self, message: &str) -> Vec { - let mut response = vec![]; - - if message.contains("PRIVMSG") && message.contains(":%invade") { - let parts: Vec<&str> = message.split_whitespace().collect(); - let num_invaders = parts[4].parse::().unwrap_or(1) as usize; - let channel = parts[2]; - let invadechannel = parts[5]; - let scream = if parts.len() > 6 { parts[6] } else { "" }; // read entire message - //message.split(&format!("PRIVMSG {} :", channel.to_string())).nth(1).unwrap(), - //let channel = message.split("PRIVMSG ").nth(1).and_then(|s| s.splitn(2, ' ').next()).unwrap(); - let config_str = std::fs::read_to_string("config.toml").unwrap(); - let config_value = config_str.parse::().unwrap(); - let config: Config = config_value.try_into().unwrap(); - - - for _invader in 1..=num_invaders {//&config.invaders[1..num_invaders] { //1..=20 { - let thread_channel = invadechannel.to_string(); - let config_clone = config.clone(); - let screaming = scream.to_string(); - let command_channel = channel.to_string(); - let thread_invader = random_word::gen(); // change to leetspeak on nick collision - - std::thread::spawn(move || { - - let stream = TcpStream::connect((config_clone.server.as_str(), config_clone.port)).unwrap(); - let connector = SslConnector::builder(SslMethod::tls()).unwrap().build(); - let mut ssl_stream = connector.connect(config_clone.server.as_str(), stream).unwrap(); - - let nick_command = format!("NICK {}\r\n", thread_invader); - let user_command = format!("USER {} 0 * :{}\r\n", thread_invader, thread_invader); - ssl_stream.write_all(nick_command.as_bytes()).unwrap(); - ssl_stream.write_all(user_command.as_bytes()).unwrap(); - let join_command = format!("JOIN {} \r\n", thread_channel); - let commander = format!("JOIN {} \r\n", command_channel); - ssl_stream.write_all(commander.as_bytes()).unwrap(); - ssl_stream.write_all(join_command.as_bytes()).unwrap(); - let msg = format!("PRIVMSG {} :{}\r\n", thread_channel, screaming); - ssl_stream.write_all(msg.as_bytes()).unwrap(); - - loop { - - - - let mut buf = [0; 512]; - match ssl_stream.ssl_read(&mut buf) { - Ok(0) => break, - Ok(n) => { - let received = String::from_utf8_lossy(&buf[0..n]); - let message = received.trim(); - - //debug chat - println!("{} {} {}","[%] DEBUG:".bold().green(), thread_invader.green(), received.blue()); - - if message.starts_with("PING") { - let response = message.replace("PING", "PONG"); - println!("{} {}","[%] PONG:".bold().green(), thread_invader.blue()); - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - // turn to mods - // setup so these will only run from the server admin to avoid handle/host conflicts - let commandi = format!("PRIVMSG {} :%%",command_channel); // & check for admin and verify with server - - if message.contains(&commandi) && message.contains(":%%") { - if message.contains("PRIVMSG") && message.contains(":%%join") { // fix so commands get picked up faster - let parts: Vec<&str> = message.splitn(3, ":%%join ").collect(); - let invade_channel = parts[1]; - let response = format!("JOIN {} \r\n", invade_channel); - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - if message.contains("PRIVMSG") && message.contains(":%%leave") { - let parts: Vec<&str> = message.splitn(3, ":%%leave ").collect(); - let invade_channel = parts[1]; - let response = format!("PART {} \r\n", invade_channel); - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - if message.contains("PRIVMSG") && message.contains(":%%scream") { - let parts: Vec<&str> = message.splitn(3, ":%%scream ").collect(); - let invade_channel = parts[1]; - if parts.len() == 2 { - let scream = parts[1]; - let response = format!("PRIVMSG {} :{}\r\n", invade_channel, scream); - ssl_stream.write_all(response.as_bytes()).unwrap(); - } - } - } - // ...1 - } - Err(e) => { - eprintln!("{} {}","[!] ERROR FROM SERVER:".on_red(), e); - break; - } - } - } - }); - } - - response.push(format!("PRIVMSG {} :\x0304,01[!] INVADING {} WITH {} INVADERS...\x0f\r\n", channel, invadechannel, num_invaders)); - } - - response - } -} \ No newline at end of file diff --git a/src/modules/kill.rs b/src/modules/kill.rs deleted file mode 100644 index 2696506..0000000 --- a/src/modules/kill.rs +++ /dev/null @@ -1,17 +0,0 @@ - -use crate::modules::Command; - -pub struct KillCommand; -impl Command for KillCommand { - fn handle(&self, message: &str) -> Vec { - let mut response = vec![]; - - if message.contains("PRIVMSG") && message.contains(":%kill") { - let channel = message.split("PRIVMSG ").nth(1).and_then(|s| s.splitn(2, ' ').next()).unwrap(); - response.push(format!("PRIVMSG {} :SELF DESTRUCTING...\r\n", channel)); - println!("[!] KILLING!"); - std::process::exit(0); - } - response - } -} diff --git a/src/modules/ping.rs b/src/modules/ping.rs deleted file mode 100644 index 80ace04..0000000 --- a/src/modules/ping.rs +++ /dev/null @@ -1,17 +0,0 @@ - -use std::time::{Instant}; -use crate::modules::Command; -pub struct PingCommand; -impl Command for PingCommand { - fn handle(&self, message: &str) -> Vec { - let mut response = vec![]; - - if message.contains("PRIVMSG") && message.contains(":%ping") { - let channel = message.split("PRIVMSG ").nth(1).and_then(|s| s.splitn(2, ' ').next()).unwrap(); - let start = Instant::now(); - let elapsed = start.elapsed(); - response.push(format!("PRIVMSG {} :PONG: {:?}\r\n", channel, elapsed)); - } - response - } -} diff --git a/src/modules/test.rs b/src/modules/test.rs deleted file mode 100644 index e69de29..0000000