WIP: hellfire #1

Draft
sad wants to merge 19 commits from hellfire into main
4 changed files with 224 additions and 27 deletions
Showing only changes of commit c70eda96b8 - Show all commits

View File

@ -1,14 +1,26 @@
server = "irc.supernets.org" #[server]
server = "198.98.52.138" #"irc.supernets.org"
port = 6697 port = 6697
use_ssl = true use_ssl = true
#[user]
nickname = "g1r" nickname = "g1r"
channel = "#superbowl" realname = "git.supernets.org/sad/g1r"
channels = ["#dev", "#superbowl", "#5000"]
sasl_username = "" sasl_username = ""
sasl_password = "" sasl_password = ""
capabilities = ["sasl"] capabilities = ["sasl"]
#[proxy]
use_proxy = false use_proxy = false
proxy_type = "socks5" proxy_type = "socks5"
proxy_addr = "127.0.0.1" proxy_addr = "127.0.0.1"
proxy_port = 9050 proxy_port = 1080
proxy_username = "" proxy_username = ""
proxy_password = "" proxy_password = ""
#[features]
kickrejoin = true
ascii_art = "./ircart/ircart"
pump_delay = 0 # in milliseconds

View File

@ -1,10 +1,11 @@
use tokio::io::{split, AsyncRead, AsyncWrite, AsyncReadExt, AsyncWriteExt}; use tokio::io::{split, AsyncRead, AsyncWrite, AsyncReadExt, AsyncWriteExt, BufReader, AsyncBufReadExt};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio_native_tls::native_tls::TlsConnector as NTlsConnector; use tokio_native_tls::native_tls::TlsConnector as NTlsConnector;
use tokio_native_tls::TlsConnector; use tokio_native_tls::TlsConnector;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use serde::Deserialize; use serde::Deserialize;
use std::fs; use std::fs;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use colored::*; use colored::*;
use tokio_socks::tcp::Socks5Stream; use tokio_socks::tcp::Socks5Stream;
@ -15,7 +16,8 @@ struct Config {
port: u16, port: u16,
use_ssl: bool, use_ssl: bool,
nickname: String, nickname: String,
channel: String, realname: Option<String>,
channels: Vec<String>,
sasl_username: Option<String>, sasl_username: Option<String>,
sasl_password: Option<String>, sasl_password: Option<String>,
capabilities: Option<Vec<String>>, capabilities: Option<Vec<String>>,
@ -27,15 +29,19 @@ struct Config {
proxy_port: Option<u16>, proxy_port: Option<u16>,
proxy_username: Option<String>, proxy_username: Option<String>,
proxy_password: Option<String>, proxy_password: Option<String>,
ascii_art: Option<String>,
pump_delay: u64,
} }
mod mods { mod mods {
pub mod sasl; pub mod sasl;
pub mod sed; pub mod sed;
pub mod ascii;
} }
use mods::sasl::{start_sasl_auth, handle_sasl_messages}; use mods::sasl::{start_sasl_auth, handle_sasl_messages};
use mods::sed::{SedCommand, MessageBuffer}; use mods::sed::{SedCommand, MessageBuffer};
use mods::ascii::handle_ascii_command;
#[tokio::main(flavor = "multi_thread", worker_threads = 12)] #[tokio::main(flavor = "multi_thread", worker_threads = 12)]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
@ -106,10 +112,9 @@ async fn proxy_exec(config: &Config) -> Result<TcpStream, Box<dyn std::error::Er
let proxy_stream = TcpStream::connect(proxy).await.unwrap(); let proxy_stream = TcpStream::connect(proxy).await.unwrap();
let username = config.proxy_username.clone().unwrap(); let username = config.proxy_username.clone().unwrap();
let password = config.proxy_password.clone().unwrap(); let password = config.proxy_password.clone().unwrap();
let tcp_stream = if !&username.is_empty() && !password.is_empty() { let mut 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(); let tcp_stream =Socks5Stream::connect_with_password_and_socket(proxy_stream, server, &username, &password).await.unwrap();
tcp_stream tcp_stream
} else { } else {
let tcp_stream = Socks5Stream::connect_with_socket(proxy_stream, server).await.unwrap(); let tcp_stream = Socks5Stream::connect_with_socket(proxy_stream, server).await.unwrap();
tcp_stream tcp_stream
@ -156,8 +161,6 @@ async fn readmsg(mut reader: tokio::io::ReadHalf<tokio_native_tls::TlsStream<Tcp
} }
} }
static SASL_AUTH: AtomicBool = AtomicBool::new(false); static SASL_AUTH: AtomicBool = AtomicBool::new(false);
/// Write messages to the server /// Write messages to the server
@ -166,14 +169,15 @@ async fn writemsg(mut writer: tokio::io::WriteHalf<tokio_native_tls::TlsStream<T
let username = config.sasl_username.clone().unwrap(); let username = config.sasl_username.clone().unwrap();
let password = config.sasl_password.clone().unwrap(); let password = config.sasl_password.clone().unwrap();
let nickname = config.nickname.clone(); let nickname = config.nickname.clone();
let realname = config.realname.clone().unwrap_or(nickname.clone());
if !password.is_empty() && !SASL_AUTH.load(Ordering::Relaxed) { if !password.is_empty() && !SASL_AUTH.load(Ordering::Relaxed) {
let capabilities = config.capabilities.clone(); let capabilities = config.capabilities.clone();
println!("Starting SASL auth..."); println!("Starting SASL auth...");
start_sasl_auth(&mut writer, "PLAIN", &nickname, capabilities).await.unwrap(); start_sasl_auth(&mut writer, "PLAIN", &nickname, &realname, capabilities).await.unwrap();
writer.flush().await.unwrap(); writer.flush().await.unwrap();
SASL_AUTH.store(true, Ordering::Relaxed); SASL_AUTH.store(true, Ordering::Relaxed);
} else { } else {
nickme(&mut writer, &nickname).await.unwrap(); nickme(&mut writer, &nickname, &realname).await.unwrap();
writer.flush().await.unwrap(); writer.flush().await.unwrap();
} }
@ -206,36 +210,59 @@ async fn writemsg(mut writer: tokio::io::WriteHalf<tokio_native_tls::TlsStream<T
} }
if *cmd == "376" { if *cmd == "376" {
println!("Joining channel"); println!("Joining channels");
writer.write_all(format!("JOIN {}\r\n", config.channel).as_bytes()).await.unwrap(); for channel in &config.channels {
writer.write_all(format!("JOIN {}\r\n", channel).as_bytes()).await.unwrap();
writer.flush().await.unwrap(); writer.flush().await.unwrap();
} }
}
if *cmd == "KICK" {
let channel = parts[2];
let userme = parts[3];
if userme == nickname {
writer.write_all(format!("JOIN {}\r\n", channel).as_bytes()).await.unwrap();
writer.flush().await.unwrap();
}
}
if *cmd == "PRIVMSG" { if *cmd == "PRIVMSG" {
let channel = parts[2]; let channel = parts[2];
let user = parts[0].strip_prefix(':').unwrap().split_at(parts[0].find('!').unwrap()).0.strip_suffix('!').unwrap(); let user = parts[0].strip_prefix(':')
let host = parts[0].split_at(parts[0].find('!').unwrap()).1; .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 = parts[3..].join(" ").replace(':', ""); let msg_content = parts[3..].join(" ").replace(':', "");
println!("{} {} {} {} {} {} {}", "DEBUG:".bold().yellow(), "channel:".bold().green(), channel.purple(), "user:".bold().green(), host.purple(), "msg:".bold().green(), msg_content.purple()); 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 // sed
if msg_content.starts_with("s/") { if msg_content.starts_with("s/") {
println!("Sed command detected"); if let Some(sed_command) = SedCommand::parse(&msg_content.clone()) {
if let Some(sed_command) = SedCommand::parse(&msg_content) {
if let Some(response) = message_buffer.apply_sed_command(&sed_command) { if let Some(response) = message_buffer.apply_sed_command(&sed_command) {
writer.write_all(format!("PRIVMSG {} :{}: {}\r\n", channel, user, response).as_bytes()).await.unwrap(); writer.write_all(format!("PRIVMSG {} :{}: {}\r\n", channel, user, response).as_bytes()).await.unwrap();
writer.flush().await.unwrap(); writer.flush().await.unwrap();
} }
} }
} else { } else {
message_buffer.add_message(msg_content); message_buffer.add_message(msg_content.clone());
} }
// ansi art
//
if msg_content.starts_with("%ascii") {
handle_ascii_command(&mut writer, &config, &msg_content, channel).await;
}
// other commands here // other commands here
} }
} }
} }
async fn nickme<W: tokio::io::AsyncWriteExt + Unpin>(writer: &mut W, nickname: &str) -> Result<(), Box<dyn std::error::Error>> {
async fn nickme<W: tokio::io::AsyncWriteExt + Unpin>(writer: &mut W, nickname: &str, realname: &str) -> Result<(), Box<dyn std::error::Error>> {
writer.write_all(format!("NICK {}\r\n", nickname).as_bytes()).await?; writer.write_all(format!("NICK {}\r\n", nickname).as_bytes()).await?;
writer.flush().await?; writer.flush().await?;
writer.write_all(format!("USER {} 0 * :{}\r\n", nickname, nickname).as_bytes()).await?; writer.write_all(format!("USER {} 0 * :{}\r\n", nickname, realname).as_bytes()).await?;
writer.flush().await?; writer.flush().await?;
Ok(()) Ok(())
} }

157
src/mods/ascii.rs Normal file
View File

@ -0,0 +1,157 @@
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<W: AsyncWriteExt + Unpin>(writer: &mut W, file_path: &str, pump_delay: u64, channel: &str) -> Result<(), Box<dyn Error>> {
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<String> {
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::<Vec<String>>();
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<W: AsyncWriteExt + Unpin>(
writer: &mut W,
config: &Config, // Adjust the path as necessary to match your project structure
command: &str,
channel: &str,
) -> Result<(), Box<dyn std::error::Error>> {
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(3).unwrap_or(&""))).await?;
} else {
handle_specific_file(writer, config, channel, &parts).await?;
}
Ok(())
}
async fn handle_random<W: AsyncWriteExt + Unpin>(
writer: &mut W,
config: &Config,
channel: &str,
) -> Result<(), Box<dyn Error>> {
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<W: AsyncWriteExt + Unpin>(
writer: &mut W,
config: &Config,
channel: &str,
subdirectory: Option<&str>,
) -> Result<(), Box<dyn Error>> {
let base_dir = config.ascii_art.clone().unwrap_or_else(|| "ascii_art".to_string());
let dir = if let Some(subdir) = subdirectory {
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::<Vec<String>>()
.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<W: AsyncWriteExt + Unpin>(
writer: &mut W,
config: &Config,
channel: &str,
parts: &[&str],
) -> Result<(), Box<dyn Error>> {
let file_name = if parts.len() >= 3 {
parts[2..].join(" ")
} else {
parts[2].to_string()
};
let file_path = format!("{}/{}.txt", config.ascii_art.clone().unwrap_or_else(|| "ascii_art".to_string()), file_name);
send_ansi_art(writer, &file_path, config.pump_delay, channel).await
}

View File

@ -5,10 +5,11 @@ pub async fn start_sasl_auth<W: tokio::io::AsyncWriteExt + Unpin>(
writer: &mut W, writer: &mut W,
mechanism: &str, mechanism: &str,
nickname: &str, nickname: &str,
realname: &str,
capabilities: Option<Vec<String>>) -> Result<(), Box<dyn std::error::Error>> { capabilities: Option<Vec<String>>) -> Result<(), Box<dyn std::error::Error>> {
writer.write_all(b"CAP LS 302\r\n").await?; writer.write_all(b"CAP LS 302\r\n").await?;
nickme(writer, nickname).await?; nickme(writer, nickname, realname).await?;
if let Some(caps) = capabilities { if let Some(caps) = capabilities {
if !caps.is_empty() { if !caps.is_empty() {