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::<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客户端已有代理请求的访问能力,可以实现通过代理请求数据,下一章我们将探讨如何通过自动化测试来增加系统的稳定性。
点击 [关注],[在看],[点赞] 是对作者最大的支持
33. 干货系列从零用Rust编写正反向代理,关于HTTP客户端代理的源码实现的更多相关文章
- [Spark内核] 第33课:Spark Executor内幕彻底解密:Executor工作原理图、ExecutorBackend注册源码解密、Executor实例化内幕、Executor具体工作内幕
本課主題 Spark Executor 工作原理图 ExecutorBackend 注册源码鉴赏和 Executor 实例化内幕 Executor 具体是如何工作的 [引言部份:你希望读者看完这篇博客 ...
- 编写轻量ajax组件03-实现(附源码)
前言 通过前两篇的介绍,我们知道要执行页面对象的方法,核心就是反射,是从请求获取参数并执行指定方法的过程.实际上这和asp.net mvc框架的核心思想很类似,它会解析url,从中获取controll ...
- 循序渐进做项目系列(4)迷你QQ篇(2)——视频聊天!(附源码)
一·效果展示 源码派送:MiniQQ1.1 文字聊天的实现参见:循序渐进做项目系列(3):迷你QQ篇(1)——实现客户端互相聊天 二·服务端设计 对于实现视频聊天而言,服务端最核心的工作就是要构造多媒 ...
- 深入浅出Mybatis系列(三)---配置详解之properties与environments(mybatis源码篇)
上篇文章<深入浅出Mybatis系列(二)---配置简介(mybatis源码篇)>我们通过对mybatis源码的简单分析,可看出,在mybatis配置文件中,在configuration根 ...
- 基于Socket通讯(C#)和WebSocket协议(net)编写的两种聊天功能(文末附源码下载地址)
今天我们来盘一盘Socket通讯和WebSocket协议在即时通讯的小应用——聊天. 理论大家估计都知道得差不多了,小编也通过查阅各种资料对理论知识进行了充电,发现好多demo似懂非懂,拷贝回来又运行 ...
- openlayers4 入门开发系列之前端动态渲染克里金插值 kriging 篇(附源码下载)
前言 openlayers4 官网的 api 文档介绍地址 openlayers4 api,里面详细的介绍 openlayers4 各个类的介绍,还有就是在线例子:openlayers4 官网在线例子 ...
- SSM 三大框架系列:Spring 5 + Spring MVC 5 + MyBatis 3.5 整合(附源码)
之前整理了一下新版本的 SSM 三大框架,这篇文章是关于它的整合过程和项目源码,版本号分别为:Spring 5.2.2.RELEASE.SpringMVC 5.2.2.RELEASE.MyBatis ...
- Android系统篇之—-编写系统服务并且将其编译到系统源码中【转】
本文转载自:http://www.wjdiankong.cn/android%E7%B3%BB%E7%BB%9F%E7%AF%87%E4%B9%8B-%E7%BC%96%E5%86%99%E7%B3% ...
- Mybatis源码详解系列(四)--你不知道的Mybatis用法和细节
简介 这是 Mybatis 系列博客的第四篇,我本来打算详细讲解 mybatis 的配置.映射器.动态 sql 等,但Mybatis官方中文文档对这部分内容的介绍已经足够详细了,有需要的可以直接参考. ...
- 手牵手,从零学习Vue源码 系列二(变化侦测篇)
系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 陆续更新中... 预计八月中旬更新完毕. 1 概述 Vue最大的特点之一就是数据驱动视 ...
随机推荐
- 一文读懂LockSupport
阅读本文前,需要储备的知识点如下,点击链接直接跳转. java线程详解 Java不能操作内存?Unsafe了解一下 LockSupport介绍 搞java开发的基本都知道J.U.C并发包(即java. ...
- Minio 安装部署
minio 入门 简介 简介 自己看官网 https://min.io/docs/minio/kubernetes/upstream/ 安装部署 (1)Linux服务器安装 minio 可以采用以下几 ...
- 《SQL与数据库基础》07. 约束
目录 约束 常见约束案例 外键约束 删除/更新行为 本文以 MySQL 为例 约束 概念:约束是作用于表中字段上的规则,用于限制存储在表中的数据. 目的:保证数据库中数据的正确性.有效性和完整性. 分 ...
- PHP写一个 Api接口需要注意哪些?考虑哪些?
随着互联网的飞速发展,前后端分离的开发模式越来越流行.编写一个稳定.可靠和易于使用的 API 接口是现代互联网应用程序的关键.本文将介绍在使用 thinkphp6 框架开发 API 接口时需要注意的要 ...
- MySQL高级9-锁
一.简介 锁是计算机协调多个进程或线程并发访问某一资源的机制.在数据库中,除了传统的计算资源(CPU.RAM.i/O)的挣用以外,数据也是一种供许多用户共享的资源.如何保证数据并发访问的一致性,有效性 ...
- Frida环境配置
目录 安装Linux客户端 配置Android服务端 测试运行效果 官方手册 安装Linux客户端 github地址:https://github.com/frida/frida pip instal ...
- java线程的interrup、tUninterruptibles.sleepUninterruptibly和sleep、wait
参考: (1)https://blog.csdn.net/qq_36031640/article/details/116696685 (2)https://blog.csdn.net/liuxiao7 ...
- math 库中常用的数学运算和常量【GO 基础】
〇.关于 math GO 语言的 math 库是一个内置的标准库,其中包含了许多数学函数和常量,用于计算各种数学运算和统计学计算. 日常开发中,计算当然是少不了的,那么今天来梳理下备查. 一.测试示例 ...
- vue2实现数据聚合【scatter-clustering】组件封装
实现如下效果: 效果展示:https://code.juejin.cn/pen/7228568245148581943 如果不会请移步到官网的栗子,请点击查看 直接给大家上代码: 整体代码片段 1 & ...
- Redis系列之——主从复制原理与优化、缓存的使用和优化
@ 目录 一 什么是主从复制 1.1 原理 1.2 主库是否要开启持久化 1.3 辅助配置(主从数据一致性配置) 二 复制的 配置 2.1 slave 命令 2.2 配置文件 四 故障处理 五 复制常 ...