use bytes::{Buf, BufMut, BytesMut};
use core::fmt;
use futures_util::SinkExt;
use std::io;
use tokio_stream::StreamExt;
use std::pin::Pin;
use std::task::Poll;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::{TcpStream, ToSocketAddrs};
use tokio_util::codec::{Decoder, Encoder, Framed};
use crate::{SOCKS5_RESERVED, SOCKS5_USERNAME_AUTH_VER, SOCKS5_VERSION};
use super::{
AuthMethod, Socks5Address, Socks5Command, Socks5Request, Socks5Response, Socks5Status,
decode_res,
};
struct ClientCodec {
expecting: ResponseType,
}
#[derive(Clone, Copy)]
enum ResponseType {
MethodSelection,
AuthResponse,
ConnectResponse,
}
impl ClientCodec {
fn new() -> Self {
Self {
expecting: ResponseType::MethodSelection,
}
}
}
enum ClientRequest {
Handshake(AuthMethod),
UserPassAuth((String, String)),
Connect(Socks5Request),
}
impl fmt::Display for ClientRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClientRequest::Handshake(auth_method) => {
write!(f, "ClientRequest::Handshake({auth_method})")
}
ClientRequest::UserPassAuth((username, _)) => {
write!(
f,
"ClientRequest::UserPassAuth(username: {username}, password: ***)"
)
}
ClientRequest::Connect(_socks5_request) => {
write!(f, "ClientRequest::Connect")
}
}
}
}
enum ClientResponse {
MethodSelected(AuthMethod),
ConnectResponse(Socks5Response),
AuthResponse(bool),
}
impl fmt::Display for ClientResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClientResponse::MethodSelected(auth_method) => {
write!(f, "ClientResponse::MethodSelected({auth_method})")
}
ClientResponse::ConnectResponse(_socks5_response) => {
write!(f, "ClientResponse::ConnectResponse")
}
ClientResponse::AuthResponse(success) => {
write!(f, "ClientResponse::AuthResponse(success: {})", success)
}
}
}
}
impl Decoder for ClientCodec {
type Item = ClientResponse;
type Error = io::Error;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
match self.expecting {
ResponseType::MethodSelection => {
if src.len() < 2 {
return Ok(None);
}
let version = src[0];
let method = src[1];
if version != SOCKS5_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Invalid SOCKS version: expected {}, got {}",
SOCKS5_VERSION, version
),
));
}
src.advance(2);
let auth_method = AuthMethod::try_from(method)?;
match auth_method {
AuthMethod::NoAuth => {
self.expecting = ResponseType::ConnectResponse;
}
AuthMethod::UsernamePassword => {
self.expecting = ResponseType::AuthResponse;
}
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unimplemented method requested {auth_method}"),
));
}
}
Ok(Some(ClientResponse::MethodSelected(auth_method)))
}
ResponseType::AuthResponse => {
if src.len() < 2 {
return Ok(None);
}
let version = src[0];
let status = src[1];
if version != SOCKS5_USERNAME_AUTH_VER {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Invalid auth version: expected {}, got {}",
SOCKS5_USERNAME_AUTH_VER, version
),
));
}
src.advance(2);
self.expecting = ResponseType::ConnectResponse;
Ok(Some(ClientResponse::AuthResponse(status == 0)))
}
ResponseType::ConnectResponse => {
match decode_res(src)? {
Some((response, address, port)) => {
Ok(Some(ClientResponse::ConnectResponse(Socks5Response {
address,
port,
response,
})))
}
None => Ok(None), }
}
}
}
}
impl Encoder<ClientRequest> for ClientCodec {
type Error = io::Error;
fn encode(&mut self, item: ClientRequest, dst: &mut BytesMut) -> Result<(), Self::Error> {
match item {
ClientRequest::UserPassAuth((username, password)) => {
let uname = username.as_bytes();
let passwd = password.as_bytes();
if uname.len() > 255 || passwd.len() > 255 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Username or password too long (max 255 bytes)",
));
}
dst.reserve(3 + uname.len() + passwd.len());
dst.put_u8(SOCKS5_USERNAME_AUTH_VER);
dst.put_u8(uname.len() as u8);
dst.extend_from_slice(uname);
dst.put_u8(passwd.len() as u8);
dst.extend_from_slice(passwd);
}
ClientRequest::Handshake(auth_method) => {
dst.reserve(3);
dst.extend_from_slice(&[SOCKS5_VERSION, 0x01, auth_method as u8]);
}
ClientRequest::Connect(req) => {
dst.reserve(22); dst.extend_from_slice(&[SOCKS5_VERSION, req.command as u8, SOCKS5_RESERVED]);
req.address.to_bytes(dst);
dst.extend_from_slice(&req.port.to_be_bytes());
}
}
Ok(())
}
}
pub struct Socks5Client {
framed: Framed<TcpStream, ClientCodec>,
credentials: Option<(String, String)>,
}
impl Socks5Client {
pub async fn connect(
proxy_addr: impl ToSocketAddrs,
credentials: Option<(String, String)>,
) -> io::Result<Self> {
let stream = TcpStream::connect(proxy_addr).await?;
if let Some((ref username, ref password)) = credentials {
if username.is_empty()
|| username.len() > 255
|| password.is_empty()
|| password.len() > 255
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Username and password length should be in range [1-255]",
));
}
}
let mut client = Socks5Client {
framed: Framed::new(stream, ClientCodec::new()),
credentials,
};
client.handshake().await?;
Ok(client)
}
async fn authenticate(&mut self) -> io::Result<()> {
let creds = self
.credentials
.clone()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "No credentials provided"))?;
self.framed.send(ClientRequest::UserPassAuth(creds)).await?;
self.framed.flush().await?;
match self.framed.next().await.transpose()? {
Some(ClientResponse::AuthResponse(true)) => Ok(()),
Some(ClientResponse::AuthResponse(false)) => Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Authentication failed: wrong username/password",
)),
Some(response) => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unexpected response during authentication: {}", response),
)),
None => Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Connection closed during authentication",
)),
}
}
async fn handshake(&mut self) -> io::Result<()> {
let auth_method = match self.credentials {
Some(_) => AuthMethod::UsernamePassword,
None => AuthMethod::NoAuth,
};
self.framed
.send(ClientRequest::Handshake(auth_method))
.await?;
self.framed.flush().await?;
match self.framed.next().await.transpose()? {
Some(ClientResponse::MethodSelected(selected_method)) => match selected_method {
AuthMethod::NoAuth => {
if self.credentials.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Server selected NoAuth but client provided credentials",
));
}
Ok(())
}
AuthMethod::UsernamePassword => {
if self.credentials.is_none() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Server selected UsernamePassword but client provided no credentials",
));
}
self.authenticate().await
}
other => Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("Server selected unsupported auth method: {}", other),
)),
},
Some(response) => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unexpected response to handshake: {}", response),
)),
None => Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"No handshake response from server",
)),
}
}
pub async fn request_connect(
mut self,
dest_addr: Socks5Address,
dest_port: u16,
) -> io::Result<Socks5ClientStream> {
let request = Socks5Request {
address: dest_addr,
port: dest_port,
command: Socks5Command::Connect,
};
self.framed.send(ClientRequest::Connect(request)).await?;
self.framed.flush().await?;
match self.framed.next().await.transpose()? {
Some(ClientResponse::ConnectResponse(response)) => match response.response {
Socks5Status::RequestGranted => Ok(Socks5ClientStream(self.framed.into_inner())),
other => Err(io::Error::new(
io::ErrorKind::ConnectionRefused,
format!("SOCKS5 request failed with status: {:?}", other),
)),
},
Some(response) => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unexpected response to connect request: {}", response),
)),
None => Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"No response from server to connect request",
)),
}
}
}
pub struct Socks5ClientStream(TcpStream);
impl AsyncRead for Socks5ClientStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.0).poll_read(cx, buf)
}
}
impl AsyncWrite for Socks5ClientStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.0).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut std::task::Context) -> Poll<io::Result<()>> {
Pin::new(&mut self.0).poll_flush(cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.0).poll_shutdown(cx)
}
}