33. 乾貨系列從零用Rust編寫正反向代理,關於HTTP使用者端代理的原始碼實現

2023-12-12 09:00:46

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使用者端已有代理請求的存取能力,可以實現通過代理請求資料,下一章我們將探討如何通過自動化測試來增加系統的穩定性。

點選 [關注][在看][點贊] 是對作者最大的支援