diff --git a/grammers-client/src/peer/channel.rs b/grammers-client/src/peer/channel.rs index 633e282d..02a00cdb 100644 --- a/grammers-client/src/peer/channel.rs +++ b/grammers-client/src/peer/channel.rs @@ -214,6 +214,11 @@ impl Channel { None => None, } } + + /// Return whether this channel requires join requests. + pub fn requires_join_request(&self) -> bool { + self.raw.join_request + } } impl TryFrom for ChannelKind { diff --git a/grammers-client/src/peer/group.rs b/grammers-client/src/peer/group.rs index 4570f913..e0589d02 100644 --- a/grammers-client/src/peer/group.rs +++ b/grammers-client/src/peer/group.rs @@ -201,6 +201,18 @@ impl Group { C::Channel(_) | C::ChannelForbidden(_) => true, } } + + /// Return whether this group requires join requests. + pub fn requires_join_request(&self) -> bool { + use tl::enums::Chat; + + match &self.raw { + Chat::Empty(_) | Chat::Chat(_) | Chat::Forbidden(_) | Chat::ChannelForbidden(_) => { + false + } + Chat::Channel(channel) => channel.join_request, + } + } } impl From for PeerInfo { diff --git a/grammers-mtsender/Cargo.toml b/grammers-mtsender/Cargo.toml index 30c46bd9..52723e55 100644 --- a/grammers-mtsender/Cargo.toml +++ b/grammers-mtsender/Cargo.toml @@ -14,7 +14,7 @@ categories = ["api-bindings", "network-programming"] edition = "2024" [features] -proxy = ["tokio-socks", "hickory-resolver", "url"] +proxy = ["tokio-socks", "hickory-resolver", "url", "base64"] [dependencies] bytes = "1.10.1" @@ -29,6 +29,7 @@ tokio = { version = "1.47.1", default-features = false, features = ["io-util", " tokio-socks = { version = "0.5.2", optional = true } hickory-resolver = { version = "0.25.2", optional = true } url = { version = "2.5.7", optional = true } +base64 = { version = "0.22", optional = true } [dev-dependencies] simple_logger = { version = "5.0.0", default-features = false, features = ["colors"] } diff --git a/grammers-mtsender/DEPS.md b/grammers-mtsender/DEPS.md index 98b1bc15..28696e6c 100644 --- a/grammers-mtsender/DEPS.md +++ b/grammers-mtsender/DEPS.md @@ -65,6 +65,10 @@ Used to look up the IP address of the proxy host if a domain is provided. SOCKS5 proxy support. +## base64 + +Used to encode credentials for HTTP CONNECT proxy authentication. + ## socks5-server Used to test for SOCKS5 proxy support. diff --git a/grammers-mtsender/src/net/tcp.rs b/grammers-mtsender/src/net/tcp.rs index 76845d40..fc59bafb 100644 --- a/grammers-mtsender/src/net/tcp.rs +++ b/grammers-mtsender/src/net/tcp.rs @@ -49,6 +49,7 @@ impl NetStream { }; use hickory_resolver::Resolver; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; use url::Host; let proxy = url::Url::parse(proxy_url) @@ -64,15 +65,15 @@ impl NetStream { ))?; let username = proxy.username(); let password = proxy.password().unwrap_or(""); - let socks_addr = match host { + let proxy_addr = match host { Host::Domain(domain) => { let resolver = Resolver::builder_tokio().unwrap().build(); let response = resolver.lookup_ip(domain).await?; - let socks_ip_addr = response.into_iter().next().ok_or(io::Error::new( + let ip_addr = response.into_iter().next().ok_or(io::Error::new( ErrorKind::NotFound, format!("proxy host did not return any ip address: {}", domain), ))?; - SocketAddr::new(socks_ip_addr, port) + SocketAddr::new(ip_addr, port) } Host::Ipv4(v4) => SocketAddr::new(IpAddr::from(v4), port), Host::Ipv6(v6) => SocketAddr::new(IpAddr::from(v6), port), @@ -82,20 +83,107 @@ impl NetStream { "socks5" => { if username.is_empty() { Ok(NetStream::ProxySocks5( - tokio_socks::tcp::Socks5Stream::connect(socks_addr, addr) + tokio_socks::tcp::Socks5Stream::connect(proxy_addr, addr) .await .map_err(|err| io::Error::new(ErrorKind::ConnectionAborted, err))?, )) } else { Ok(NetStream::ProxySocks5( tokio_socks::tcp::Socks5Stream::connect_with_password( - socks_addr, addr, username, password, + proxy_addr, addr, username, password, ) .await .map_err(|err| io::Error::new(ErrorKind::ConnectionAborted, err))?, )) } } + "socks4" => { + let mut stream = TcpStream::connect(proxy_addr).await?; + + // SOCKS4 CONNECT: VER(04) CMD(01) DSTPORT(2B) DSTIP(4B) USERID NULL + let ip = match addr.ip() { + std::net::IpAddr::V4(v4) => v4, + _ => { + return Err(io::Error::new( + ErrorKind::InvalidInput, + "SOCKS4 does not support IPv6", + )); + } + }; + let mut req = vec![0x04, 0x01]; + req.extend_from_slice(&addr.port().to_be_bytes()); + req.extend_from_slice(&ip.octets()); + // userid (optional, use username if provided) + req.extend_from_slice(username.as_bytes()); + req.push(0x00); // null terminator + + stream.write_all(&req).await?; + + // Response 8 bytes: VN(00) CD DSTPORT(2B) DSTIP(4B) + let mut resp = [0u8; 8]; + stream.read_exact(&mut resp).await?; + + if resp[0] != 0x00 || resp[1] != 0x5A { + return Err(io::Error::new( + ErrorKind::ConnectionAborted, + format!("SOCKS4 CONNECT rejected (code: 0x{:02X})", resp[1]), + )); + } + + Ok(NetStream::Tcp(stream)) + } + "http" => { + use base64::Engine; + + let mut stream = TcpStream::connect(proxy_addr).await?; + + let target = format!("{}:{}", addr.ip(), addr.port()); + let mut request = format!("CONNECT {} HTTP/1.1\r\nHost: {}\r\n", target, target); + + if !username.is_empty() { + let creds = format!("{}:{}", username, password); + let encoded = + base64::engine::general_purpose::STANDARD.encode(creds.as_bytes()); + request.push_str(&format!("Proxy-Authorization: Basic {}\r\n", encoded)); + } + request.push_str("\r\n"); + stream.write_all(request.as_bytes()).await?; + + // Read response headers byte by byte until \r\n\r\n + let mut buf = Vec::with_capacity(512); + let mut byte = [0u8; 1]; + loop { + stream.read_exact(&mut byte).await?; + buf.push(byte[0]); + if buf.ends_with(b"\r\n\r\n") { + break; + } + if buf.len() > 4096 { + return Err(io::Error::new( + ErrorKind::InvalidData, + "HTTP CONNECT response too large", + )); + } + } + + let response = String::from_utf8_lossy(&buf); + let status_line = response.lines().next().ok_or_else(|| { + io::Error::new( + ErrorKind::InvalidData, + "HTTP CONNECT response has no status line", + ) + })?; + let parts: Vec<&str> = status_line.splitn(3, ' ').collect(); + if parts.len() < 2 || parts[1] != "200" { + return Err(io::Error::new( + ErrorKind::ConnectionAborted, + format!("HTTP CONNECT failed: {}", status_line), + )); + } + + // After tunnel is established, the underlying TCP acts as a direct connection + Ok(NetStream::Tcp(stream)) + } scheme => Err(io::Error::new( ErrorKind::ConnectionAborted, format!("proxy scheme not supported: {}", scheme),