33. 干货系列从零用Rust编写正反向代理,关于HTTP客户端代理的源码实现

发布时间 2023-12-12 07:46:26作者: 问蒙服务框架

wmproxy

wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子

项目地址

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

客户端代理

客户端代理常见的为http/https代理及socks代理,我们通常利用代理来隐藏客户端地址,或者通过代理来访问某些不可达的资源。

定义类

/// 客户端代理类
#[derive(Debug, Clone)]
pub enum ProxyScheme {
    Http {
        addr: SocketAddr,
        auth: Option<(String, String)>,
    },
    Https {
        addr: SocketAddr,
        auth: Option<(String, String)>,
    },
    Socks5 {
        addr: SocketAddr,
        auth: Option<(String, String)>,
    },
}

将字符串转成类,我们根据url的scheme来确定是何种类型,然后根据url中的用户密码来确定验证的用户密码

impl TryFrom<&str> for ProxyScheme {
    type Error = ProtError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let url = Url::try_from(value)?;
        let (addr, auth) = if let Some(connect) = url.get_connect_url() {
            let addr = connect
                .parse::<SocketAddr>()
                .map_err(|_| ProtError::Extension("unknow parse"))?;
            let auth = if url.username.is_some() && url.password.is_some() {
                Some((url.username.unwrap(), url.password.unwrap()))
            } else {
                None
            };
            (addr, auth)
        } else {
            return Err(ProtError::Extension("unknow addr"))
        };
        match &url.scheme {
            webparse::Scheme::Http => Ok(ProxyScheme::Http {
                addr, auth
            }),
            webparse::Scheme::Https => Ok(ProxyScheme::Https {
                addr, auth
            }),
            webparse::Scheme::Extension(s) if s == "socks5" => {
                Ok(ProxyScheme::Socks5 { addr, auth })
            }
            _ => Err(ProtError::Extension("unknow scheme")),
        }
    }
}

与原来的区别

原来的访问方式,访问百度的网站

let url = "http://www.baidu.com";
let req = Request::builder().method("GET").url(url).body("").unwrap();
let client = Client::builder()
    .connect(url).await.unwrap();
let (mut recv, _sender) = client.send2(req.into_type()).await?;
let res = recv.recv().await;

那么我们添加代理可以用环境变量模式,以上代码保持不动,程序会自动读取环境变量数据自动访问代理

export HTTP_PROXY="http://127.0.0.1:8090"

在我们的代码中添加代理地址:

let url = "http://www.baidu.com";
let req = Request::builder().method("GET").url(url).body("").unwrap();
let client = Client::builder()
    .add_proxy("http://127.0.0.1:8090")?
    .connect(url).await.unwrap();
let (mut recv, _sender) = client.send2(req.into_type()).await?;
let res = recv.recv().await;

程序将会访问代理地址,如果访问失败,则请求失败。

源码实现

我们将改造connect函数来支持我们代理请求,本质上原来没有经过代理的是一个TcpStream直接连接到目标网址,现在将是一个TcpStream连接到代理的地址,并进行相应的预处理函数,完全后将该TcpStream直接给http的客户端处理,代理端将进行双向绑定,不再处理内容数据的处理。

我们改造后的源码:

pub async fn connect<U>(self, url: U) -> ProtResult<Client>
where
    U: TryInto<Url>,
{
    let url = TryInto::<Url>::try_into(url)
        .map_err(|_e| ProtError::Extension("unknown connection url"))?;

    if self.inner.proxies.len() > 0 {
        for p in self.inner.proxies.iter() {
            match p.connect(&url).await? {
                Some(tcp) => {
                    
                    if url.scheme.is_https() {
                        return self.connect_tls_by_stream(tcp, url).await;
                    } else {
                        return Ok(Client::new(self.inner, MaybeHttpsStream::Http(tcp)))
                    }
                },
                None => continue,
            }
        }
        return Err(ProtError::Extension("not proxy error!"));
    } else {
        if !ProxyScheme::is_no_proxy(url.domain.as_ref().unwrap_or(&String::new())) {
            let proxies = ProxyScheme::get_env_proxies();
            for p in proxies.iter() {
                match p.connect(&url).await? {
                    Some(tcp) => {
                        if url.scheme.is_https() {
                            return self.connect_tls_by_stream(tcp, url).await;
                        } else {
                            return Ok(Client::new(self.inner, MaybeHttpsStream::Http(tcp)))
                        }
                    },
                    None => continue,
                }
            }
        }
        if url.scheme.is_https() {
            let connect = url.get_connect_url();
            let stream = self.inner_connect(&connect.unwrap()).await?;
            self.connect_tls_by_stream(stream, url).await
        } else {
            let tcp = self.inner_connect(url.get_connect_url().unwrap()).await?;
            Ok(Client::new(self.inner, MaybeHttpsStream::Http(tcp)))
        }
    }
}

通常配置代理相关的环境变量有如下变量

# 设置请求http代理
export http_proxy="http://127.0.0.1:8090"
# 设置请求https代理
export https_proxy="http://127.0.0.1:8090"
# 设置哪些相关的网址或者ip不经过代理
export no_proxy="localhost, 127.0.0.1, ::1"
变量名 含义 示例
http_proxy http的请求代理,如访问http://www.baidu.com时触发 http://127.0.0.1:8090
socks5://127.0.0.1:8090
https_proxy http的请求代理,如访问https://www.baidu.com时触发 http://127.0.0.1:8090
socks5://127.0.0.1:8090
all_proxy 两者都通用的代理地址 同上
no_proxy 配置哪些域名或者地址不经过代理,可配置泛域名 localhost
127.0.0.1
::1
*.qq.com

如何高效的读取环境变量数据

环境变量通过随着程序运行后就不会再发生变化,那么我们整个程序的运行周期内只需要完整的读取一次环境变量即可以,完成后我们可以将期保存下来,且我们还可以利用到使用才调用的原理,利用惰性的原理进行缓读,我们利用静态变量来存储其结构,源码


pub fn get_env_proxies() -> &'static Vec<ProxyScheme> {
    lazy_static! {
        static ref ENV_PROXIES: Vec<ProxyScheme> = get_from_environment();
    }
    &ENV_PROXIES
}

fn get_from_environment() -> Vec<ProxyScheme> {
    let mut proxies = vec![];

    if !insert_from_env(&mut proxies, Scheme::Http, "HTTP_PROXY") {
        insert_from_env(&mut proxies, Scheme::Http, "http_proxy");
    }

    if !insert_from_env(&mut proxies, Scheme::Https, "HTTPS_PROXY") {
        insert_from_env(&mut proxies, Scheme::Https, "https_proxy");
    }

    if !(insert_from_env(&mut proxies, Scheme::Http, "ALL_PROXY")
        && insert_from_env(&mut proxies, Scheme::Https, "ALL_PROXY"))
    {
        insert_from_env(&mut proxies, Scheme::Http, "all_proxy");
        insert_from_env(&mut proxies, Scheme::Https, "all_proxy");
    }

    proxies
}


fn insert_from_env(proxies: &mut Vec<ProxyScheme>, scheme: Scheme, key: &str) -> bool {
    if let Ok(val) = env::var(key) {
        if let Ok(proxy) = ProxyScheme::try_from(&*val) {
            if scheme.is_http() {
                if let Ok(proxy) = proxy.trans_http() {
                    proxies.push(proxy);
                    return true;
                }
            } else {
                if let Ok(proxy) = proxy.trans_https() {
                    proxies.push(proxy);
                    return true;
                }
            }
        }
    }
    false
}
http请求的转化

在http请求时,代理会将我们的所有数据完整的转发到远程端,我们无需做任何的TcpStream的预处理,只需将数据一样的进行发送即可。

https请求的转化

在https请求中,因为要保证https的私密性也保证代理服务器无法嗅探其中的内容,所以代理先必须收到connect协议,确认和远程端做好双向绑定后,由客户端自行与远程端握手

CONNECT www.baidu.com:443 HTTP/1.1\r\n
Host: www.baidu.com:443\r\n\r\n

且代理服务器必须返回200,之后就和远端进行双向绑定,代理服务器不在处理相关内容。


async fn tunnel<T>(
    mut conn: T,
    host: String,
    port: u16,
    user_agent: Option<HeaderValue>,
    auth: Option<HeaderValue>,
) -> ProtResult<T>
where
    T: AsyncRead + AsyncWrite + Unpin,
{
    use tokio::io::{AsyncReadExt, AsyncWriteExt};

    let mut buf = format!(
        "\
         CONNECT {0}:{1} HTTP/1.1\r\n\
         Host: {0}:{1}\r\n\
         ",
        host, port
    )
    .into_bytes();

    // user-agent
    if let Some(user_agent) = user_agent {
        buf.extend_from_slice(b"User-Agent: ");
        buf.extend_from_slice(user_agent.as_bytes());
        buf.extend_from_slice(b"\r\n");
    }

    // proxy-authorization
    if let Some(value) = auth {
        log::debug!("tunnel to {}:{} using basic auth", host, port);
        buf.extend_from_slice(b"Proxy-Authorization: ");
        buf.extend_from_slice(value.as_bytes());
        buf.extend_from_slice(b"\r\n");
    }

    // headers end
    buf.extend_from_slice(b"\r\n");

    conn.write_all(&buf).await?;

    let mut buf = [0; 8192];
    let mut pos = 0;

    loop {
        let n = conn.read(&mut buf[pos..]).await?;

        if n == 0 {
            return Err(ProtError::Extension("eof error"));
        }
        pos += n;

        let recvd = &buf[..pos];
        if recvd.starts_with(b"HTTP/1.1 200") || recvd.starts_with(b"HTTP/1.0 200") {
            if recvd.ends_with(b"\r\n\r\n") {
                return Ok(conn);
            }
            if pos == buf.len() {
                return Err(ProtError::Extension("proxy headers too long for tunnel"));
            }
        // else read more
        } else if recvd.starts_with(b"HTTP/1.1 407") {
            return Err(ProtError::Extension("proxy authentication required"));
        } else {
            return Err(ProtError::Extension("unsuccessful tunnel"));
        }
    }
}
socks5请求的转化

socks5是一种比较通用的代理服务器的能力,相对来说也都能实现http的代理请求,但是需要将其的数据做预处理,即做完认证交互等功能,会相应的多耗一些握手时间。

async fn socks5_connect<T>(
    mut conn: T,
    url: &Url,
    auth: &Option<(String, String)>,
) -> ProtResult<T>
where
    T: AsyncRead + AsyncWrite + Unpin,
{
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    use webparse::BufMut;
    let mut binary = BinaryMut::new();
    let mut data = vec![0;1024];
    if let Some(_auth) = auth {
        conn.write_all(&[5, 1, 2]).await?;
    } else {
        conn.write_all(&[5, 0]).await?;
    }

    conn.read_exact(&mut data[..2]).await?;
    if data[0] != 5 {
        return Err(ProtError::Extension("socks5 error"));
    }
    match data[1] {
        2 => {
            let (user, pass) = auth.as_ref().unwrap();
            binary.put_u8(1);
            binary.put_u8(user.as_bytes().len() as u8);
            binary.put_slice(user.as_bytes());
            binary.put_u8(pass.as_bytes().len() as u8);
            binary.put_slice(pass.as_bytes());
            conn.write_all(binary.as_slice()).await?;

            conn.read_exact(&mut data[..2]).await?;
            if data[0] != 1 || data[1] != 0 {
                return Err(ProtError::Extension("user password error"));
            }

            binary.clear();
        }
        0 => {},
        _ => {
            return Err(ProtError::Extension("no method for auth"));
        }
    }

    binary.put_slice(&[5, 1, 0, 3]);
    let domain = url.domain.as_ref().unwrap();
    let port = url.port.unwrap_or(80);
    binary.put_u8(domain.as_bytes().len() as u8);
    binary.put_slice(domain.as_bytes());
    binary.put_u16(port);
    conn.write_all(&binary.as_slice()).await?;
    conn.read_exact(&mut data[..10]).await?;
    if data[0] != 5 {
        return Err(ProtError::Extension("socks5 error"));
    }
    if data[1] != 0 {
        return Err(ProtError::Extension("network error"));
    }
    Ok(conn)
}

小结

至此,此时的http客户端已有代理请求的访问能力,可以实现通过代理请求数据,下一章我们将探讨如何通过自动化测试来增加系统的稳定性。

点击 [关注][在看][点赞] 是对作者最大的支持