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客户端代理的源码实现的更多相关文章

  1. [Spark内核] 第33课:Spark Executor内幕彻底解密:Executor工作原理图、ExecutorBackend注册源码解密、Executor实例化内幕、Executor具体工作内幕

    本課主題 Spark Executor 工作原理图 ExecutorBackend 注册源码鉴赏和 Executor 实例化内幕 Executor 具体是如何工作的 [引言部份:你希望读者看完这篇博客 ...

  2. 编写轻量ajax组件03-实现(附源码)

    前言 通过前两篇的介绍,我们知道要执行页面对象的方法,核心就是反射,是从请求获取参数并执行指定方法的过程.实际上这和asp.net mvc框架的核心思想很类似,它会解析url,从中获取controll ...

  3. 循序渐进做项目系列(4)迷你QQ篇(2)——视频聊天!(附源码)

    一·效果展示 源码派送:MiniQQ1.1 文字聊天的实现参见:循序渐进做项目系列(3):迷你QQ篇(1)——实现客户端互相聊天 二·服务端设计 对于实现视频聊天而言,服务端最核心的工作就是要构造多媒 ...

  4. 深入浅出Mybatis系列(三)---配置详解之properties与environments(mybatis源码篇)

    上篇文章<深入浅出Mybatis系列(二)---配置简介(mybatis源码篇)>我们通过对mybatis源码的简单分析,可看出,在mybatis配置文件中,在configuration根 ...

  5. 基于Socket通讯(C#)和WebSocket协议(net)编写的两种聊天功能(文末附源码下载地址)

    今天我们来盘一盘Socket通讯和WebSocket协议在即时通讯的小应用——聊天. 理论大家估计都知道得差不多了,小编也通过查阅各种资料对理论知识进行了充电,发现好多demo似懂非懂,拷贝回来又运行 ...

  6. openlayers4 入门开发系列之前端动态渲染克里金插值 kriging 篇(附源码下载)

    前言 openlayers4 官网的 api 文档介绍地址 openlayers4 api,里面详细的介绍 openlayers4 各个类的介绍,还有就是在线例子:openlayers4 官网在线例子 ...

  7. SSM 三大框架系列:Spring 5 + Spring MVC 5 + MyBatis 3.5 整合(附源码)

    之前整理了一下新版本的 SSM 三大框架,这篇文章是关于它的整合过程和项目源码,版本号分别为:Spring 5.2.2.RELEASE.SpringMVC 5.2.2.RELEASE.MyBatis ...

  8. 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% ...

  9. Mybatis源码详解系列(四)--你不知道的Mybatis用法和细节

    简介 这是 Mybatis 系列博客的第四篇,我本来打算详细讲解 mybatis 的配置.映射器.动态 sql 等,但Mybatis官方中文文档对这部分内容的介绍已经足够详细了,有需要的可以直接参考. ...

  10. 手牵手,从零学习Vue源码 系列二(变化侦测篇)

    系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 陆续更新中... 预计八月中旬更新完毕. 1 概述 Vue最大的特点之一就是数据驱动视 ...

随机推荐

  1. IDA的使用2

    IDA的使用2 string类型的选择 Rename 要注意如果再namelist和public name里面是不能重名 操作数 这个主要和开发结合精密, change sign-改变符号 bitwi ...

  2. Redis专题-队列

    Redis专题-队列 首先,想一想 Redis 适合做消息队列吗? 1.消息队列的消息存取需求是什么?redis中的解决方案是什么? 无非就是下面这几点: 0.数据可以顺序读取 1.支持阻塞等待拉取消 ...

  3. 搞懂 Vue3 中的各种 ref:toRef,toRefs,isRef,unref...

    在 Vue3 中,有许多与响应式相关的函数,例如 toRef.toRefs.isRef.unref 等等.合理地使用这些函数可以在实际开发中大大提高效率.本文将详细介绍这些函数的用法,让我们在实际开发 ...

  4. 《SQL与数据库基础》22. 分库分表(二)

    目录 分库分表(二) MyCat分片规则 范围分片 取模分片 一致性hash分片 枚举分片 应用指定算法 固定分片hash算法 字符串hash解析 按天分片 自然月分片 MyCat原理 MyCat管理 ...

  5. 交换机通过SFTP进行文件操作

    组网图形  通过SFTP进行文件操作简介 配置设备作为SFTP服务器,用户可以在终端通过SFTP通信方式,利用SSH协议提供的安全通道与远端设备进行安全连接.通过SFTP进行文件操作的方式对数据进行了 ...

  6. 文心一言 VS 讯飞星火 VS chatgpt (91)-- 算法导论8.3 4题

    四.用go语言,说明如何在 O(n) 时间内,对 0 到 $n^3-1$ 区间内的n个整数进行排序. 文心一言: 在 O(n) 时间内对 0 到 n^3-1 区间内的 n 个整数进行排序,可以使用基数 ...

  7. C++ 学习笔记、01 | 开发简单职工管理系统遇到的一些问题

    记录开发简单职工管理系统遇到的一些问题,黑马教程 https://www.bilibili.com/video/BV1et411b73Z P147 ~ P166 头文件与源文件 头文件只声明,源文件来 ...

  8. assembleDebug太慢的问题调查以及其他

    Preface 最近在做flutter上的音频和视频方面的探索. 需要用到一些视屏区域截取,视屏导出成序列图等等. 这是昨天晚上到今天早上解决的一些问题的汇总,可能先后顺序之类的会记错: 此文目的用于 ...

  9. Selenium+dddocr轻松解决Web自动化验证码识别

    大家好,我是狂师,今天给大家推荐一款验证码识别神器:dddocr. 1.介绍 dddocr是一个基于深度学习的OCR(Optical Character Recognition,光学字符识别)库,用于 ...

  10. Vue3搭建后台管理系统模板

    搭建后台管理系统模板 2.1项目初始化 今天来带大家从0开始搭建一个vue3版本的后台管理系统.一个项目要有统一的规范,需要使用eslint+stylelint+prettier来对我们的代码质量做检 ...