Skip to content
This repository was archived by the owner on Feb 10, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions grammers-client/src/peer/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Channel> for ChannelKind {
Expand Down
12 changes: 12 additions & 0 deletions grammers-client/src/peer/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Group> for PeerInfo {
Expand Down
3 changes: 2 additions & 1 deletion grammers-mtsender/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"] }
Expand Down
4 changes: 4 additions & 0 deletions grammers-mtsender/DEPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
98 changes: 93 additions & 5 deletions grammers-mtsender/src/net/tcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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),
Expand All @@ -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),
Expand Down