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最大的特点之一就是数据驱动视 ...
随机推荐
- Pandas 使用教程 JSON
目录 JSON 转换为 CSV 简单 JSON 从 URL 中读取 JSON 数据: 字典转化为 DataFrame 数据 内嵌的 JSON 数据 复杂 JSON Pandas 可以很方便的处理 JS ...
- Facade Pattern and Encapsulation—— Structure Class
如果只看代码的话,应该可以说Facade pattern(门面设计模式)是一种最简单的代码结构,不就封装吗!这玩意谁不会! 还是看它背后所蕴含的思想吧,看了之后发现背后的思想也很简单,非常好理解. - ...
- Go,从命名开始!Go的关键字和标识符全列表手册和代码示例!
关注TechLeadCloud,分享互联网架构.云服务技术的全维度知识.作者拥有10+年互联网服务架构.AI产品研发经验.团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师, ...
- Sermant类隔离架构:解决JavaAgent场景类冲突的实践
本文分享自华为云社区<Sermant类隔离架构解析--解决JavaAgent场景类冲突的实践>,作者:华为云开源. Sermant是基于Java字节码增强技术的无代理服务网格,其利用Jav ...
- Dami 本地过程调用框架(主打解耦),v0.24 发布
Dami,专为本地多模块之间通讯解耦而设计(尤其是未知模块.隔离模块.领域模块).零依赖,特适合 DDD. 特点 结合 Bus 与 RPC 的概念,可作事件分发,可作接口调用,可作异步响应. 支持事务 ...
- 使用 OpenTelemetry 构建 .NET 应用可观测性(3):.NET SDK 概览
目录 前言 概览 opentelemetry-dotnet opentelemetry-dotnet-contrib opentelemetry-dotnet-instrumentation SDK ...
- 【Flutter】如何优美地实现一个悬浮NavigationBar
[Flutter]如何优美地实现一个悬浮NavigationBar 最近写代码的时候遇到了一个如下的需求: 整体来说,底部的条是一个浮动的悬浮窗,有如下的三个按钮: 点击左边的要进入"主页& ...
- jmeter生成HTML性能测试报告(非GUI的命令)
非GUI的命令(在cmd执行即可 不需要打开jmeter) 使用命令:jmeter -n -t [jmx file] -l [jtl file] -e -o [report path ...
- 造轮子之集成GraphQL
先简单对比以下GraphQL和WebAPI:GraphQL和Web API(如RESTful API)是用于构建和提供Web服务的不同技术. 数据获取方式: Web API:通常使用RESTful A ...
- ansible-配置文件优化-性能调优
ansible-配置文件详解:ansible默认配置文件为/etc/ansible/ansible.cfg,配置文件中可以对ansible进行各项参数的调整,包括并发线程.用户.模块路径.配置优化等, ...