diff --git a/Cargo.lock b/Cargo.lock index 7e36ad9..e3331f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,6 +106,7 @@ dependencies = [ "serde_default", "serde_yml", "socks", + "tungstenite", "winapi", "winresource", ] @@ -122,6 +123,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -134,6 +144,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cairo-rs" version = "0.20.7" @@ -264,6 +280,25 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.20.11" @@ -299,6 +334,22 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -499,6 +550,16 @@ dependencies = [ "system-deps", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.3.1" @@ -775,6 +836,23 @@ dependencies = [ "windows", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -1267,6 +1345,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1349,6 +1438,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.20" @@ -1383,12 +1492,41 @@ dependencies = [ "winnow", ] +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 5750717..e6e4473 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ socks = "0.3.4" libnotify = { version = "1.0.3", optional = true } gdk-pixbuf = { version = "0.3.0", optional = true } winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] } +tungstenite = "0.26.2" [features] default = [] diff --git a/src/chat/config.rs b/src/chat/config.rs index 6e338b1..44bd1ea 100644 --- a/src/chat/config.rs +++ b/src/chat/config.rs @@ -26,6 +26,7 @@ pub struct Config { #[serde(default = "default_true")] pub chunked_enabled: bool, #[serde(default = "default_true")] pub formatting_enabled: bool, #[serde(default = "default_true")] pub commands_enabled: bool, + #[serde(default)] pub wrac_enabled: bool, #[serde(default)] pub proxy: Option, #[serde(default = "default_true")] pub notifications_enabled: bool, } @@ -117,6 +118,7 @@ pub struct Args { #[arg(long)] pub formatting_enabled: Option, #[arg(long)] pub commands_enabled: Option, #[arg(long)] pub notifications_enabled: Option, + #[arg(long)] pub wrac_enabled: Option, #[arg(long)] pub proxy: Option, } @@ -136,5 +138,6 @@ impl Args { if let Some(v) = self.formatting_enabled { config.formatting_enabled = v } if let Some(v) = self.commands_enabled { config.commands_enabled = v } if let Some(v) = self.notifications_enabled { config.notifications_enabled = v } + if let Some(v) = self.wrac_enabled { config.wrac_enabled = v } } } \ No newline at end of file diff --git a/src/chat/ctx.rs b/src/chat/ctx.rs index 6f481fd..0b33d6c 100644 --- a/src/chat/ctx.rs +++ b/src/chat/ctx.rs @@ -77,7 +77,8 @@ macro_rules! connect_rac { &mut connect( &$ctx.config(|o| o.host.clone()), $ctx.config(|o| o.ssl_enabled), - $ctx.config(|o| o.proxy.clone()) + $ctx.config(|o| o.proxy.clone()), + $ctx.config(|o| o.wrac_enabled) )? }; } \ No newline at end of file diff --git a/src/chat/gui.rs b/src/chat/gui.rs index e63f364..02314bc 100644 --- a/src/chat/gui.rs +++ b/src/chat/gui.rs @@ -165,6 +165,7 @@ fn open_settings(ctx: Arc, app: &Application) { let show_other_ip_entry = gui_checkbox_setting!("Show Other IP", show_other_ip, ctx, vbox); let auth_enabled_entry = gui_checkbox_setting!("Fake Auth Enabled", auth_enabled, ctx, vbox); let ssl_enabled_entry = gui_checkbox_setting!("SSL Enabled", ssl_enabled, ctx, vbox); + let wrac_enabled_entry = gui_checkbox_setting!("WRAC Enabled", wrac_enabled, ctx, vbox); let chunked_enabled_entry = gui_checkbox_setting!("Chunked Enabled", chunked_enabled, ctx, vbox); let formatting_enabled_entry = gui_checkbox_setting!("Formatting Enabled", formatting_enabled, ctx, vbox); let commands_enabled_entry = gui_checkbox_setting!("Commands Enabled", commands_enabled, ctx, vbox); @@ -191,6 +192,7 @@ fn open_settings(ctx: Arc, app: &Application) { #[weak] formatting_enabled_entry, #[weak] commands_enabled_entry, #[weak] notifications_enabled_entry, + #[weak] wrac_enabled_entry, #[weak] proxy_entry, move |_| { let config = Config { @@ -231,6 +233,7 @@ fn open_settings(ctx: Arc, app: &Application) { show_other_ip: show_other_ip_entry.is_active(), auth_enabled: auth_enabled_entry.is_active(), ssl_enabled: ssl_enabled_entry.is_active(), + wrac_enabled: wrac_enabled_entry.is_active(), chunked_enabled: chunked_enabled_entry.is_active(), formatting_enabled: formatting_enabled_entry.is_active(), commands_enabled: commands_enabled_entry.is_active(), @@ -267,6 +270,7 @@ fn open_settings(ctx: Arc, app: &Application) { #[weak] show_other_ip_entry, #[weak] auth_enabled_entry, #[weak] ssl_enabled_entry, + #[weak] wrac_enabled_entry, #[weak] chunked_enabled_entry, #[weak] formatting_enabled_entry, #[weak] commands_enabled_entry, @@ -286,6 +290,7 @@ fn open_settings(ctx: Arc, app: &Application) { show_other_ip_entry.set_active(config.show_other_ip); auth_enabled_entry.set_active(config.auth_enabled); ssl_enabled_entry.set_active(config.ssl_enabled); + wrac_enabled_entry.set_active(config.wrac_enabled); chunked_enabled_entry.set_active(config.chunked_enabled); formatting_enabled_entry.set_active(config.formatting_enabled); commands_enabled_entry.set_active(config.commands_enabled); diff --git a/src/chat/mod.rs b/src/chat/mod.rs index 188bf3e..e7714d4 100644 --- a/src/chat/mod.rs +++ b/src/chat/mod.rs @@ -2,12 +2,9 @@ use std::{ error::Error, sync::Arc, thread, time::{Duration, SystemTime, UNIX_EPOCH} }; -use crate::{connect_rac, proto::{register_user, send_message_auth}}; +use crate::connect_rac; -use super::{ - proto::{connect, read_messages, send_message, send_message_spoof_auth}, - util::sanitize_text -}; +use super::proto::{connect, read_messages, send_message, send_message_spoof_auth, register_user, send_message_auth}; use gui::{add_chat_message, clear_chat_messages}; use lazy_static::lazy_static; @@ -19,15 +16,18 @@ pub use gui::run_main_loop; lazy_static! { - pub static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap(); - pub static ref IP_REGEX: Regex = Regex::new(r"\{(.*?)\} (.*)").unwrap(); + static ref ANSI_REGEX: Regex = Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").unwrap(); + static ref CONTROL_CHARS_REGEX: Regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap(); - pub static ref COLORED_USERNAMES: Vec<(Regex, String)> = vec![ - (Regex::new(r"\u{B9AC}\u{3E70}<(.*?)> (.*)").unwrap(), "green".to_string()), // bRAC - (Regex::new(r"\u{2550}\u{2550}\u{2550}<(.*?)> (.*)").unwrap(), "red".to_string()), // CRAB - (Regex::new(r"\u{00B0}\u{0298}<(.*?)> (.*)").unwrap(), "magenta".to_string()), // Mefidroniy - (Regex::new(r"<(.*?)> (.*)").unwrap(), "cyan".to_string()), // clRAC - ]; + pub static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap(); + pub static ref IP_REGEX: Regex = Regex::new(r"\{(.*?)\} (.*)").unwrap(); + + pub static ref COLORED_USERNAMES: Vec<(Regex, String)> = vec![ + (Regex::new(r"\u{B9AC}\u{3E70}<(.*?)> (.*)").unwrap(), "green".to_string()), // bRAC + (Regex::new(r"\u{2550}\u{2550}\u{2550}<(.*?)> (.*)").unwrap(), "red".to_string()), // CRAB + (Regex::new(r"\u{00B0}\u{0298}<(.*?)> (.*)").unwrap(), "magenta".to_string()), // Mefidroniy + (Regex::new(r"<(.*?)> (.*)").unwrap(), "cyan".to_string()), // clRAC + ]; } @@ -44,6 +44,11 @@ const HELP_MESSAGE: &str = "Help message: /spam n text - send message with text n times /ping - check server ping"; +pub fn sanitize_text(input: &str) -> String { + let without_ansi = ANSI_REGEX.replace_all(input, ""); + let cleaned_text = CONTROL_CHARS_REGEX.replace_all(&without_ansi, ""); + cleaned_text.into_owned() +} pub fn add_message(ctx: Arc, message: &str) -> Result<(), Box> { for i in message.split("\n") diff --git a/src/lib.rs b/src/lib.rs index 8e581b3..b5738fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ #![allow(non_snake_case)] pub mod chat; -pub mod util; pub mod proto; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 82f170d..45f751a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,7 @@ fn main() { let mut config = load_config(config_path); if args.read_messages { - let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone()).expect("Error reading message"); + let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone(), config.wrac_enabled).expect("Error reading message"); print!("{}", read_messages( &mut stream, @@ -35,7 +35,7 @@ fn main() { } if let Some(message) = &args.send_message { - let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone()).expect("Error sending message"); + let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone(), config.wrac_enabled).expect("Error sending message"); send_message( &mut stream, diff --git a/src/proto.rs b/src/proto/mod.rs similarity index 54% rename from src/proto.rs rename to src/proto/mod.rs index 7e5e286..9dbd4f8 100644 --- a/src/proto.rs +++ b/src/proto/mod.rs @@ -1,49 +1,72 @@ -#![allow(unused)] +use std::{error::Error, fmt::Debug, io::{Read, Write}, net::{TcpStream, ToSocketAddrs}, time::Duration}; -use std::{error::Error, fmt::Debug, io::{Read, Write}, net::{SocketAddr, TcpStream, ToSocketAddrs}, str::FromStr, time::Duration}; use native_tls::{TlsConnector, TlsStream}; use socks::Socks5Stream; +use tungstenite::WebSocket; -use crate::util::parse_socks5_url; +pub mod rac; +pub mod wrac; -pub trait RacStream: Read + Write + Unpin + Send + Sync + Debug { +pub trait Stream: Read + Write + Unpin + Send + Sync + Debug { fn set_read_timeout(&self, timeout: Duration); fn set_write_timeout(&self, timeout: Duration); } -impl RacStream for TcpStream { +impl Stream for TcpStream { fn set_read_timeout(&self, timeout: Duration) { let _ = TcpStream::set_read_timeout(&self, Some(timeout)); } fn set_write_timeout(&self, timeout: Duration) { let _ = TcpStream::set_write_timeout(&self, Some(timeout)); } } -impl RacStream for Socks5Stream { +impl Stream for Socks5Stream { fn set_read_timeout(&self, timeout: Duration) { let _ = TcpStream::set_read_timeout(self.get_ref(), Some(timeout)); } fn set_write_timeout(&self, timeout: Duration) { let _ = TcpStream::set_write_timeout(self.get_ref(), Some(timeout)); } } -impl RacStream for TlsStream { +impl Stream for TlsStream { fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); } fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); } } -impl RacStream for TlsStream> { +impl Stream for TlsStream> { fn set_read_timeout(&self, timeout: Duration) { self.get_ref().set_read_timeout(timeout); } fn set_write_timeout(&self, timeout: Duration) { self.get_ref().set_write_timeout(timeout); } } +pub enum RacStream { + WRAC(WebSocket>), + RAC(Box) +} + +/// `socks5://user:pass@127.0.0.1:12345/path -> ("127.0.0.1:12345", ("user", "pass"))` \ +/// `socks5://127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \ +/// `https://127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \ +/// `127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \ +/// `user:pass@127.0.0.1:12345 -> ("127.0.0.1:12345", ("user", "pass"))` +pub fn parse_socks5_url(url: &str) -> Option<(String, Option<(String, String)>)> { + let (_, url) = url.split_once("://").unwrap_or(("", url)); + let (url, _) = url.split_once("/").unwrap_or((url, "")); + if let Some((auth, url)) = url.split_once("@") { + let (user, pass) = auth.split_once(":")?; + Some((url.to_string(), Some((user.to_string(), pass.to_string())))) + } else { + Some((url.to_string(), None)) + } +} + /// Create RAC connection (also you can just TcpStream::connect) /// /// host - host string, example: "example.com:12345", "example.com" (default port is 42666) /// ssl - wrap with ssl client, write false if you dont know what it is /// proxy - socks5 proxy (host, (user, pass)) -pub fn connect(host: &str, ssl: bool, proxy: Option) -> Result, Box> { +/// wrac - to use wrac protocol +pub fn connect(host: &str, ssl: bool, proxy: Option, wrac: bool) -> Result> { let host = if host.contains(":") { host.to_string() } else { format!("{host}:42666") }; - let stream: Box = if let Some(proxy) = proxy { + let stream: Box = if let Some(proxy) = proxy { if let Some((proxy, auth)) = parse_socks5_url(&proxy) { if let Some((user, pass)) = auth { Box::new(Socks5Stream::connect_with_password(&proxy, host.as_str(), &user, &pass)?) @@ -76,16 +99,50 @@ pub fn connect(host: &str, ssl: bool, proxy: Option) -> Result ") else { return send_message(stream, message) } +/// if send_message_auth(name, name, message) != 0 { +/// let name = "\x1f" + name +/// register_user(stream, name, name) +/// send_message_spoof_auth(stream, name + "> " + message) +/// } +pub fn send_message_spoof_auth(stream: &mut RacStream, message: &str, remove_null: bool) -> Result<(), Box> { + let Some((name, message)) = message.split_once("> ") else { return send_message(stream, message) }; + + if let Ok(f) = send_message_auth(stream, &name, &message, &message, remove_null) { + if f != 0 { + let name = format!("\x1f{name}"); + register_user(stream, &name, &name, remove_null)?; + send_message_spoof_auth(stream, &format!("{name}> {message}"), remove_null)?; + } + } + + Ok(()) +} + + /// Send message /// /// stream - any stream that can be written to /// message - message text -pub fn send_message(stream: &mut impl Write, message: &str) -> Result<(), Box> { - stream.write_all(format!("\x01{message}").as_bytes())?; - Ok(()) +pub fn send_message( + stream: &mut RacStream, + message: &str +) -> Result<(), Box> { + match stream { + RacStream::WRAC(websocket) => wrac::send_message(websocket, message), + RacStream::RAC(stream) => rac::send_message(stream, message) + } } /// Register user @@ -97,25 +154,14 @@ pub fn send_message(stream: &mut impl Write, message: &str) -> Result<(), Box Result> { - stream.write_all(format!("\x03{name}\n{password}").as_bytes())?; - if remove_null { - if let Ok(out) = skip_null(stream) { - Ok(out[0] == 0) - } else { - Ok(true) - } - } else { - let mut buf = vec![0]; - if let Ok(1) = stream.read(&mut buf) { - Ok(buf[0] == 0) - } else { - Ok(true) - } + match stream { + RacStream::WRAC(websocket) => wrac::register_user(websocket, name, password), + RacStream::RAC(stream) => rac::register_user(stream, name, password, remove_null) } } @@ -131,73 +177,18 @@ pub fn register_user( /// returns 1 if the user does not exist /// returns 2 if the password is incorrect pub fn send_message_auth( - stream: &mut (impl Write + Read), + stream: &mut RacStream, name: &str, password: &str, message: &str, remove_null: bool ) -> Result> { - stream.write_all(format!("\x02{name}\n{password}\n{message}").as_bytes())?; - - if remove_null { - if let Ok(out) = skip_null(stream) { - Ok(out[0]) - } else { - Ok(0) - } - } else { - let mut buf = vec![0]; - if let Ok(1) = stream.read(&mut buf) { - Ok(buf[0]) - } else { - Ok(0) - } + match stream { + RacStream::WRAC(websocket) => wrac::send_message_auth(websocket, name, password, message), + RacStream::RAC(stream) => rac::send_message_auth(stream, name, password, message, remove_null) } } -/// Send message with fake auth -/// -/// Explaination: -/// -/// let (name, message) = message.split("> ") else { return send_message(stream, message) } -/// if send_message_auth(name, name, message) != 0 { -/// let name = "\x1f" + name -/// register_user(stream, name, name) -/// send_message_spoof_auth(stream, name + "> " + message) -/// } -pub fn send_message_spoof_auth(stream: &mut (impl Write + Read), message: &str, remove_null: bool) -> Result<(), Box> { - let Some((name, message)) = message.split_once("> ") else { return send_message(stream, message) }; - - if let Ok(f) = send_message_auth(stream, &name, &message, &message, remove_null) { - if f != 0 { - let name = format!("\x1f{name}"); - register_user(stream, &name, &name, remove_null); - send_message_spoof_auth(stream, &format!("{name}> {message}"), remove_null); - } - } - - Ok(()) -} - -/// Skip null bytes and return first non-null byte -pub fn skip_null(stream: &mut impl Read) -> Result, Box> { - loop { - let mut buf = vec![0; 1]; - stream.read_exact(&mut buf)?; - if buf[0] != 0 { - break Ok(buf) - } - } -} - -/// remove trailing null bytes in vector -pub fn remove_trailing_null(vec: &mut Vec) -> Result<(), Box> { - while vec.ends_with(&[0]) { - vec.remove(vec.len()-1); - } - Ok(()) -} - /// Read messages /// /// max_messages - max messages in list @@ -207,66 +198,14 @@ pub fn remove_trailing_null(vec: &mut Vec) -> Result<(), Box> { /// /// returns (messages, packet size) pub fn read_messages( - stream: &mut (impl Read + Write), + stream: &mut RacStream, max_messages: usize, last_size: usize, remove_null: bool, chunked: bool ) -> Result, usize)>, Box> { - stream.write_all(&[0x00])?; - - let packet_size = { - let data = if remove_null { - let mut data = skip_null(stream)?; - let mut buf = vec![0; 10]; - let len = stream.read(&mut buf)?; - buf.truncate(len); - data.append(&mut buf); - remove_trailing_null(&mut data)?; - data - } else { - let mut data = vec![0; 10]; - let len = stream.read(&mut data)?; - data.truncate(len); - data - }; - - String::from_utf8(data)? - .trim_matches(char::from(0)) - .parse()? - }; - - if last_size == packet_size { - return Ok(None); + match stream { + RacStream::WRAC(websocket) => wrac::read_messages(websocket, max_messages, last_size, chunked), + RacStream::RAC(stream) => rac::read_messages(stream, max_messages, last_size, remove_null, chunked) } - - let to_read = if !chunked || last_size == 0 { - stream.write_all(&[0x01])?; - packet_size - } else { - stream.write_all(format!("\x02{}", last_size).as_bytes())?; - packet_size - last_size - }; - - let packet_data = if remove_null { - let mut data = skip_null(stream)?; - let mut buf = vec![0; to_read - 1]; - stream.read_exact(&mut buf)?; - data.append(&mut buf); - data - } else { - let mut data = vec![0; to_read]; - stream.read_exact(&mut data)?; - data - }; - - let packet_data = String::from_utf8_lossy(&packet_data).to_string(); - - let lines: Vec<&str> = packet_data.split("\n").collect(); - let lines: Vec = lines.clone().into_iter() - .skip(if lines.len() >= max_messages { lines.len() - max_messages } else { 0 }) - .map(|o| o.to_string()) - .collect(); - - Ok(Some((lines, packet_size))) } \ No newline at end of file diff --git a/src/proto/rac.rs b/src/proto/rac.rs new file mode 100644 index 0000000..39ef908 --- /dev/null +++ b/src/proto/rac.rs @@ -0,0 +1,169 @@ +use std::{error::Error, io::{Read, Write}}; + +/// Send message +/// +/// stream - any stream that can be written to +/// message - message text +pub fn send_message(stream: &mut impl Write, message: &str) -> Result<(), Box> { + stream.write_all(format!("\x01{message}").as_bytes())?; + Ok(()) +} + +/// Register user +/// +/// stream - any stream that can be written to +/// name - user name +/// password - user password +/// remove_null - remove null bytes on reading +/// +/// returns whether the user was registered +pub fn register_user( + stream: &mut (impl Write + Read), + name: &str, + password: &str, + remove_null: bool +) -> Result> { + stream.write_all(format!("\x03{name}\n{password}").as_bytes())?; + if remove_null { + if let Ok(out) = skip_null(stream) { + Ok(out[0] == 0) + } else { + Ok(true) + } + } else { + let mut buf = vec![0]; + if let Ok(1) = stream.read(&mut buf) { + Ok(buf[0] == 0) + } else { + Ok(true) + } + } +} + +/// Send message with auth +/// +/// stream - any stream that can be written to +/// message - message text +/// name - user name +/// password - user password +/// remove_null - remove null bytes on reading +/// +/// returns 0 if the message was sent successfully +/// returns 1 if the user does not exist +/// returns 2 if the password is incorrect +pub fn send_message_auth( + stream: &mut (impl Write + Read), + name: &str, + password: &str, + message: &str, + remove_null: bool +) -> Result> { + stream.write_all(format!("\x02{name}\n{password}\n{message}").as_bytes())?; + + if remove_null { + if let Ok(out) = skip_null(stream) { + Ok(out[0]) + } else { + Ok(0) + } + } else { + let mut buf = vec![0]; + if let Ok(1) = stream.read(&mut buf) { + Ok(buf[0]) + } else { + Ok(0) + } + } +} + +/// Skip null bytes and return first non-null byte +pub fn skip_null(stream: &mut impl Read) -> Result, Box> { + loop { + let mut buf = vec![0; 1]; + stream.read_exact(&mut buf)?; + if buf[0] != 0 { + break Ok(buf) + } + } +} + +/// remove trailing null bytes in vector +pub fn remove_trailing_null(vec: &mut Vec) -> Result<(), Box> { + while vec.ends_with(&[0]) { + vec.remove(vec.len()-1); + } + Ok(()) +} + +/// Read messages +/// +/// max_messages - max messages in list +/// last_size - last returned packet size +/// remove_null - start with skipping null bytes +/// chunked - is chunked reading enabled +/// +/// returns (messages, packet size) +pub fn read_messages( + stream: &mut (impl Read + Write), + max_messages: usize, + last_size: usize, + remove_null: bool, + chunked: bool +) -> Result, usize)>, Box> { + stream.write_all(&[0x00])?; + + let packet_size = { + let data = if remove_null { + let mut data = skip_null(stream)?; + let mut buf = vec![0; 10]; + let len = stream.read(&mut buf)?; + buf.truncate(len); + data.append(&mut buf); + remove_trailing_null(&mut data)?; + data + } else { + let mut data = vec![0; 10]; + let len = stream.read(&mut data)?; + data.truncate(len); + data + }; + + String::from_utf8(data)? + .trim_matches(char::from(0)) + .parse()? + }; + + if last_size == packet_size { + return Ok(None); + } + + let to_read = if !chunked || last_size == 0 { + stream.write_all(&[0x01])?; + packet_size + } else { + stream.write_all(format!("\x02{}", last_size).as_bytes())?; + packet_size - last_size + }; + + let packet_data = if remove_null { + let mut data = skip_null(stream)?; + let mut buf = vec![0; to_read - 1]; + stream.read_exact(&mut buf)?; + data.append(&mut buf); + data + } else { + let mut data = vec![0; to_read]; + stream.read_exact(&mut data)?; + data + }; + + let packet_data = String::from_utf8_lossy(&packet_data).to_string(); + + let lines: Vec<&str> = packet_data.split("\n").collect(); + let lines: Vec = lines.clone().into_iter() + .skip(if lines.len() >= max_messages { lines.len() - max_messages } else { 0 }) + .map(|o| o.to_string()) + .collect(); + + Ok(Some((lines, packet_size))) +} \ No newline at end of file diff --git a/src/proto/wrac.rs b/src/proto/wrac.rs new file mode 100644 index 0000000..2ba7cde --- /dev/null +++ b/src/proto/wrac.rs @@ -0,0 +1,123 @@ +use std::{error::Error, io::{Read, Write}}; +use tungstenite::{WebSocket, Message}; + + +/// Send message +/// +/// stream - any stream that can be written to +/// message - message text +pub fn send_message( + stream: &mut WebSocket, + message: &str +) -> Result<(), Box> { + stream.write(Message::Binary(format!("\x01{message}").as_bytes().to_vec().into()))?; + Ok(()) +} + +/// Register user +/// +/// stream - any stream that can be written to +/// name - user name +/// password - user password +/// +/// returns whether the user was registered +pub fn register_user( + stream: &mut WebSocket, + name: &str, + password: &str +) -> Result> { + stream.write(Message::Binary(format!("\x03{name}\n{password}").as_bytes().to_vec().into()))?; + if let Ok(msg) = stream.read() { + Ok(!msg.is_binary() || msg.into_data().get(0).unwrap_or(&0) == &0) + } else { + Ok(true) + } +} + +/// Send message with auth +/// +/// stream - any stream that can be written to +/// message - message text +/// name - user name +/// password - user password +/// +/// returns 0 if the message was sent successfully +/// returns 1 if the user does not exist +/// returns 2 if the password is incorrect +pub fn send_message_auth( + stream: &mut WebSocket, + name: &str, + password: &str, + message: &str +) -> Result> { + stream.write(Message::Binary(format!("\x02{name}\n{password}\n{message}").as_bytes().to_vec().into()))?; + if let Ok(msg) = stream.read() { + if msg.is_binary() { + Ok(0) + } else { + Ok(*msg.into_data().get(0).unwrap_or(&0)) + } + } else { + Ok(0) + } +} + +/// Read messages +/// +/// max_messages - max messages in list +/// last_size - last returned packet size +/// chunked - is chunked reading enabled +/// +/// returns (messages, packet size) +pub fn read_messages( + stream: &mut WebSocket, + max_messages: usize, + last_size: usize, + chunked: bool +) -> Result, usize)>, Box> { + stream.write(Message::Binary(vec![0x00].into()))?; + + let packet_size = { + let msg = stream.read()?; + if !msg.is_binary() { + return Err("msg is not binary".into()); + } + let len = msg.into_data().to_vec(); + + String::from_utf8(len)? + .trim_matches(char::from(0)) + .parse()? + }; + + if last_size == packet_size { + return Ok(None); + } + + let to_read = if !chunked || last_size == 0 { + stream.write(Message::Binary(vec![0x00, 0x01].into()))?; + packet_size + } else { + stream.write(Message::Binary(format!("\x00\x02{}", last_size).as_bytes().to_vec().into()))?; + packet_size - last_size + }; + + let msg = stream.read()?; + if !msg.is_binary() { + return Err("msg is not binary".into()); + } + let packet_data = msg.into_data().to_vec(); + + if packet_data.len() > to_read { + return Err("too big msg".into()); + } + + let packet_data = String::from_utf8_lossy(&packet_data).to_string(); + + let lines: Vec<&str> = packet_data.split("\n").collect(); + let lines: Vec = lines.clone().into_iter() + .skip(if lines.len() >= max_messages { lines.len() - max_messages } else { 0 }) + .map(|o| o.to_string()) + .collect(); + + Ok(Some((lines, packet_size))) +} \ No newline at end of file diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index 1c8c00f..0000000 --- a/src/util.rs +++ /dev/null @@ -1,29 +0,0 @@ -use lazy_static::lazy_static; -use regex::Regex; - -lazy_static! { - static ref ANSI_REGEX: Regex = Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").unwrap(); - static ref CONTROL_CHARS_REGEX: Regex = Regex::new(r"[\x00-\x1F\x7F]").unwrap(); -} - -pub fn sanitize_text(input: &str) -> String { - let without_ansi = ANSI_REGEX.replace_all(input, ""); - let cleaned_text = CONTROL_CHARS_REGEX.replace_all(&without_ansi, ""); - cleaned_text.into_owned() -} - -/// `socks5://user:pass@127.0.0.1:12345/path -> ("127.0.0.1:12345", ("user", "pass"))` \ -/// `socks5://127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \ -/// `https://127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \ -/// `127.0.0.1:12345 -> ("127.0.0.1:12345", None)` \ -/// `user:pass@127.0.0.1:12345 -> ("127.0.0.1:12345", ("user", "pass"))` -pub fn parse_socks5_url(url: &str) -> Option<(String, Option<(String, String)>)> { - let (_, url) = url.split_once("://").unwrap_or(("", url)); - let (url, _) = url.split_once("/").unwrap_or((url, "")); - if let Some((auth, url)) = url.split_once("@") { - let (user, pass) = auth.split_once(":")?; - Some((url.to_string(), Some((user.to_string(), pass.to_string())))) - } else { - Some((url.to_string(), None)) - } -} \ No newline at end of file