From 998d8025f857837f86009e972840f2112b673b5e Mon Sep 17 00:00:00 2001 From: MeexReay Date: Sat, 19 Apr 2025 15:50:03 +0300 Subject: [PATCH] socks5 proxy and some sugoma-specified stuff --- Cargo.lock | 34 +++++++++ Cargo.toml | 3 +- src/chat/config.rs | 3 + src/chat/ctx.rs | 8 ++- src/chat/gui.rs | 34 ++++++++- src/chat/mod.rs | 28 +++++--- src/main.rs | 4 +- src/proto.rs | 175 +++++++++++++++++++++++++++++++++------------ src/util.rs | 16 +++++ 9 files changed, 243 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd9cfda..fc3dd5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,7 @@ dependencies = [ "serde", "serde_default", "serde_yml", + "socks", ] [[package]] @@ -1173,6 +1174,17 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1360,6 +1372,28 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows" version = "0.57.0" diff --git a/Cargo.toml b/Cargo.toml index bedf867..33e13da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,5 @@ clap = { version = "4.5.36", features = ["derive"] } serde = { version = "1.0.219", features = ["serde_derive"] } gtk4 = { version = "0.9.6", features = [ "v4_10" ] } chrono = "0.4.40" -serde_default = "0.2.0" \ No newline at end of file +serde_default = "0.2.0" +socks = "0.3.4" \ No newline at end of file diff --git a/src/chat/config.rs b/src/chat/config.rs index 35ef54c..4ba890b 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 proxy: Option, } pub fn get_config_path() -> PathBuf { @@ -113,12 +114,14 @@ pub struct Args { #[arg(long)] pub chunked_enabled: Option, #[arg(long)] pub formatting_enabled: Option, #[arg(long)] pub commands_enabled: Option, + #[arg(long)] pub proxy: Option, } impl Args { pub fn patch_config(&self, config: &mut Config) { if let Some(v) = self.host.clone() { config.host = v } if let Some(v) = self.name.clone() { config.name = Some(v) } + if let Some(v) = self.proxy.clone() { config.proxy = Some(v) } if let Some(v) = self.message_format.clone() { config.message_format = v } if let Some(v) = self.update_time.clone() { config.update_time = v } if let Some(v) = self.max_messages.clone() { config.max_messages = v } diff --git a/src/chat/ctx.rs b/src/chat/ctx.rs index 15e2912..0d4245c 100644 --- a/src/chat/ctx.rs +++ b/src/chat/ctx.rs @@ -73,5 +73,11 @@ impl Context { #[macro_export] macro_rules! connect_rac { - ($ctx:ident) => { &mut connect(&$ctx.config(|o| o.host.clone()), $ctx.config(|o| o.ssl_enabled))? }; + ($ctx:ident) => { + &mut connect( + &$ctx.config(|o| o.host.clone()), + $ctx.config(|o| o.ssl_enabled), + $ctx.config(|o| o.proxy.clone()) + )? + }; } \ No newline at end of file diff --git a/src/chat/gui.rs b/src/chat/gui.rs index e9f9933..285d3fb 100644 --- a/src/chat/gui.rs +++ b/src/chat/gui.rs @@ -1,4 +1,4 @@ -use std::{sync::{mpsc::{channel, Receiver}, Arc}, time::UNIX_EPOCH}; +use std::sync::{mpsc::{channel, Receiver}, Arc}; use std::cell::RefCell; use std::time::{Duration, SystemTime}; use std::thread; @@ -28,7 +28,7 @@ use gtk::{ }; use super::{config::{default_max_messages, default_update_time, get_config_path, save_config, Config}, -ctx::Context, on_send_message, parse_message, print_message, recv_tick}; +ctx::Context, on_send_message, parse_message, print_message, recv_tick, sanitize_message}; struct UiModel { chat_box: GtkBox, @@ -104,6 +104,20 @@ fn open_settings(ctx: Arc, app: &Application) { vbox.append(&message_format_hbox); + let proxy_hbox = GtkBox::new(Orientation::Horizontal, 5); + + proxy_hbox.append(&Label::builder() + .label("Socks5 Proxy") + .build()); + + let proxy_entry = Entry::builder() + .text(&ctx.config(|o| o.proxy.clone()).unwrap_or_default()) + .build(); + + proxy_hbox.append(&proxy_entry); + + vbox.append(&proxy_hbox); + let update_time_hbox = GtkBox::new(Orientation::Horizontal, 5); update_time_hbox.append(&Label::builder() @@ -264,6 +278,7 @@ fn open_settings(ctx: Arc, app: &Application) { #[weak] chunked_enabled_entry, #[weak] formatting_enabled_entry, #[weak] commands_enabled_entry, + #[weak] proxy_entry, move |_| { let config = Config { host: host_entry.text().to_string(), @@ -305,7 +320,16 @@ fn open_settings(ctx: Arc, app: &Application) { ssl_enabled: ssl_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() + commands_enabled: commands_enabled_entry.is_active(), + proxy: { + let proxy = proxy_entry.text().to_string(); + + if proxy.is_empty() { + None + } else { + Some(proxy) + } + } }; ctx.set_config(&config); save_config(get_config_path(), &config); @@ -332,12 +356,14 @@ fn open_settings(ctx: Arc, app: &Application) { #[weak] chunked_enabled_entry, #[weak] formatting_enabled_entry, #[weak] commands_enabled_entry, + #[weak] proxy_entry, move |_| { let config = Config::default(); ctx.set_config(&config); save_config(get_config_path(), &config); host_entry.set_text(&config.host); name_entry.set_text(&config.name.unwrap_or_default()); + proxy_entry.set_text(&config.proxy.unwrap_or_default()); message_format_entry.set_text(&config.message_format); update_time_entry.set_text(&config.update_time.to_string()); max_messages_entry.set_text(&config.max_messages.to_string()); @@ -718,6 +744,8 @@ fn load_css() { } fn on_add_message(ctx: Arc, ui: &UiModel, message: String) { + let Some(message) = sanitize_message(message) else { return; }; + if message.is_empty() { return; } diff --git a/src/chat/mod.rs b/src/chat/mod.rs index c4e9480..19869bb 100644 --- a/src/chat/mod.rs +++ b/src/chat/mod.rs @@ -79,7 +79,7 @@ pub fn on_command(ctx: Arc, command: &str) -> Result<(), Box return Ok(()) }; - match register_user(connect_rac!(ctx), &ctx.name(), pass) { + match register_user(connect_rac!(ctx), &ctx.name(), pass, !ctx.config(|o| o.ssl_enabled)) { Ok(true) => { add_message(ctx.clone(), "you was registered successfully bro")?; *ctx.registered.write().unwrap() = Some(pass.to_string()); @@ -209,9 +209,9 @@ pub fn on_send_message(ctx: Arc, message: &str) -> Result<(), Box, message: &str) -> Result<(), Box (date, ip, text, (name, color)) -pub fn parse_message(message: String) -> Option<(String, Option, String, Option<(String, String)>)> { +pub fn sanitize_message(message: String) -> Option { let message = sanitize_text(&message); - let message = message - .trim_start_matches("(UNREGISTERED)") - .trim_start_matches("(UNAUTHORIZED)") - .trim_start_matches("(UNAUTHENTICATED)") - .trim() - .to_string()+" "; + let message = message.trim().to_string(); + Some(message) +} + +/// message -> (date, ip, text, (name, color)) +pub fn parse_message(message: String) -> Option<(String, Option, String, Option<(String, String)>)> { if message.is_empty() { return None } @@ -241,6 +240,13 @@ pub fn parse_message(message: String) -> Option<(String, Option, String, date.get(2)?.as_str().to_string(), ); + let message = message + .trim_start_matches("(UNREGISTERED)") + .trim_start_matches("(UNAUTHORIZED)") + .trim_start_matches("(UNAUTHENTICATED)") + .trim() + .to_string(); + let (ip, message) = if let Some(message) = IP_REGEX.captures(&message) { (Some(message.get(1)?.as_str().to_string()), message.get(2)?.as_str().to_string()) } else { diff --git a/src/main.rs b/src/main.rs index 9b1fbae..0bd0816 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ fn main() { let mut config = load_config(config_path); if args.read_messages { - let mut stream = connect(&config.host, config.ssl_enabled).expect("Error reading message"); + let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone()).expect("Error reading message"); print!("{}", read_messages( &mut stream, @@ -33,7 +33,7 @@ fn main() { } if let Some(message) = &args.send_message { - let mut stream = connect(&config.host, config.ssl_enabled).expect("Error sending message"); + let mut stream = connect(&config.host, config.ssl_enabled, config.proxy.clone()).expect("Error sending message"); send_message( &mut stream, diff --git a/src/proto.rs b/src/proto.rs index 3c13e0b..7e5e286 100644 --- a/src/proto.rs +++ b/src/proto.rs @@ -1,41 +1,82 @@ #![allow(unused)] use std::{error::Error, fmt::Debug, io::{Read, Write}, net::{SocketAddr, TcpStream, ToSocketAddrs}, str::FromStr, time::Duration}; -use native_tls::TlsConnector; +use native_tls::{TlsConnector, TlsStream}; +use socks::Socks5Stream; -pub trait RacStream: Read + Write + Unpin + Send + Sync + Debug {} -impl RacStream for T {} +use crate::util::parse_socks5_url; + +pub trait RacStream: Read + Write + Unpin + Send + Sync + Debug { + fn set_read_timeout(&self, timeout: Duration); + fn set_write_timeout(&self, timeout: Duration); +} + +impl RacStream 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 { + 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 { + 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> { + 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); } +} /// 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 -pub fn connect(host: &str, ssl: bool) -> Result, Box> { +/// proxy - socks5 proxy (host, (user, pass)) +pub fn connect(host: &str, ssl: bool, proxy: Option) -> Result, Box> { let host = if host.contains(":") { host.to_string() } else { format!("{host}:42666") }; - if ssl { + 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)?) + } else { + Box::new(Socks5Stream::connect(&proxy, host.as_str())?) + } + } else { + return Err("proxy parse error".into()); + } + } else { + let addr = host.to_socket_addrs()?.next().ok_or::>("addr parse error".into())?; + + Box::new(TcpStream::connect(&addr)?) + }; + + let stream = if ssl { let ip: String = host.split_once(":") .map(|o| o.0.to_string()) .unwrap_or(host.clone()); - return Ok(Box::new(TlsConnector::builder() + Box::new(TlsConnector::builder() .danger_accept_invalid_certs(true) .danger_accept_invalid_hostnames(true) .build()? - .connect(&ip, connect(&host, false)?)?)) - } + .connect(&ip, stream)?) + } else { + stream + }; - let addr = host.to_socket_addrs()?.next().ok_or::>("addr parse error".into())?; - let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(3))?; + stream.set_read_timeout(Duration::from_secs(3)); + stream.set_write_timeout(Duration::from_secs(3)); - stream.set_read_timeout(Some(Duration::from_secs(5))); - stream.set_write_timeout(Some(Duration::from_secs(5))); - - Ok(Box::new(stream)) + Ok(stream) } /// Send message @@ -52,15 +93,29 @@ pub fn send_message(stream: &mut impl Write, message: &str) -> Result<(), Box Result> { +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())?; - let mut buf = vec![0]; - if let Ok(1) = stream.read(&mut buf) { - Ok(buf[0] == 0) + if remove_null { + if let Ok(out) = skip_null(stream) { + Ok(out[0] == 0) + } else { + Ok(true) + } } else { - Ok(true) + let mut buf = vec![0]; + if let Ok(1) = stream.read(&mut buf) { + Ok(buf[0] == 0) + } else { + Ok(true) + } } } @@ -70,18 +125,33 @@ pub fn register_user(stream: &mut (impl Write + Read), name: &str, password: &st /// 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) -> Result> { +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())?; - let mut buf = vec![0]; - if let Ok(1) = stream.read(&mut buf) { - Ok(buf[0]) + if remove_null { + if let Ok(out) = skip_null(stream) { + Ok(out[0]) + } else { + Ok(0) + } } else { - Ok(0) + let mut buf = vec![0]; + if let Ok(1) = stream.read(&mut buf) { + Ok(buf[0]) + } else { + Ok(0) + } } } @@ -91,24 +161,22 @@ pub fn send_message_auth(stream: &mut (impl Write + Read), name: &str, password: /// /// let (name, message) = message.split("> ") else { return send_message(stream, message) } /// if send_message_auth(name, name, message) != 0 { -/// let name = "\x1f" + "name" +/// 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) -> Result<(), Box> { +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) }; - stream.write_all(format!("\x02{name}\n{name}\n{message}").as_bytes())?; - - let mut buf = vec![0; 1]; - if let Ok(_) = stream.read_exact(&mut buf) { - let name = format!("\x1f{name}"); - register_user(stream, &name, &name)?; - let message = format!("{name}> {message}"); - send_message_spoof_auth(stream, &message) - } else { - Ok(()) + 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 @@ -122,6 +190,7 @@ pub fn skip_null(stream: &mut impl Read) -> Result, Box> { } } +/// 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); @@ -133,7 +202,7 @@ pub fn remove_trailing_null(vec: &mut Vec) -> Result<(), Box> { /// /// max_messages - max messages in list /// last_size - last returned packet size -/// start_null - start with skipping null bytes +/// remove_null - start with skipping null bytes /// chunked - is chunked reading enabled /// /// returns (messages, packet size) @@ -141,15 +210,26 @@ pub fn read_messages( stream: &mut (impl Read + Write), max_messages: usize, last_size: usize, - start_null: bool, + remove_null: bool, chunked: bool ) -> Result, usize)>, Box> { stream.write_all(&[0x00])?; let packet_size = { - let mut data = vec![0; 10]; - let len = stream.read(&mut data)?; - data.truncate(len); + 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)) @@ -168,13 +248,20 @@ pub fn read_messages( packet_size - last_size }; - let packet_data = { + 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)?; - - String::from_utf8_lossy(&data).to_string() + 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 }) diff --git a/src/util.rs b/src/util.rs index ef55b39..1c8c00f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -10,4 +10,20 @@ 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