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


    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::()
    .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(self, url: U) -> ProtResult
    where
    U: TryInto,
    {
    let url = TryInto::::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 {
    lazy_static! {
    static ref ENV_PROXIES: Vec = get_from_environment();
    }
    &ENV_PROXIES
    }
    fn get_from_environment() -> Vec {
    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, 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(
    mut conn: T,
    host: String,
    port: u16,
    user_agent: Option,
    auth: Option,
    ) -> ProtResult
    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(
    mut conn: T,
    url: &Url,
    auth: &Option<(String, String)>,
    ) -> ProtResult
    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客户端已有代理请求的访问能力,可以实现通过代理请求数据,下一章我们将探讨如何通过自动化测试来增加系统的稳定性。

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

  • 相关阅读:
    ChatGPT消息发不出去了?我找到解决方案了
    2023年中国胶布床垫行业市场规模及发展中存在的问题分析[图]
    音视频+AI,中关村科金助力某银行探索发展新路径 | 案例研究
    [附源码]java毕业设计校园爱心支愿管理系统
    React给方法使用useState
    中国节日主题网站设计 红色建军节HTML+CSS 红色中国文化主题网站设计 HTML学生作业网页
    每天一个前端小知识
    vue状态管理工具
    typescript里面一个问号(?)两个问号(??)一个感叹号(!)两个感叹号(!!)的用法和区别。
    HCIP---MPLS
  • 原文地址:https://www.cnblogs.com/wmproxy/p/wmproxy33.html