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. Sealos 国内集群正式上线,可一键运行 LLama2 中文版大模型!

    2023 年 7 月 19 日,MetaAI 宣布开源旗下的 LLama2 大模型,Meta 首席科学家.图灵奖得主 Yann LeCun 在推特上表示 Meta 此举可能将改变大模型行业的竞争格局. ...

  2. ATtiny88初体验(二):呼吸灯

    ATtiny88初体验(二):呼吸灯 前面的"点灯"实验实现了间隔点亮/熄灭LED,但是间隔时间和亮度都没法控制,为了解决这个问题,可以使用ATtiny88的定时器模块. ATti ...

  3. GAN!生成对抗网络GAN全维度介绍与实战

    本文为生成对抗网络GAN的研究者和实践者提供全面.深入和实用的指导.通过本文的理论解释和实际操作指南,读者能够掌握GAN的核心概念,理解其工作原理,学会设计和训练自己的GAN模型,并能够对结果进行有效 ...

  4. 谁家面试往死里问 Swagger 啊?

    大家好,我是小富- 前言 说个挺奇葩的事,有个老铁给我发私信吐槽了一下它的面试经历,他去了个国企单位面试,然后面试官跟他就Swagger的问题聊了半个多小时.额- 面试嘛这些都不稀奇,总能遇到是千奇百 ...

  5. 多层前馈神经网络及BP算法

    一.多层前馈神经网络 首先说下多层前馈神经网络,BP算法,BP神经网络之间的关系.多层前馈[multilayer feed-forward]神经网络由一个输入层.一个或多个隐藏层和一个输出层组成,后向 ...

  6. Google Hacking语法总结

    Google Hacking语法总结 Google Hacking是利用谷歌搜索的强大,来在浩瀚的互联网中搜索到我们需要的信息.轻量级的搜索可以搜素出一些遗留后门,不想被发现的后台入口,中量级的搜索出 ...

  7. Haproxy搭建 Web 群集实现负载均衡

    Haproxy搭建 Web 群集实现负载均衡 1 Haproxy HAProxy是可提供高可用性.负载均衡以及基于TCP和HTTP应用的代理,是免费.快速并且可靠的一种解决方案.HAProxy非常适用 ...

  8. 使用js开发一个快速打开前端项目的alfred插件

    使用js开发一个快速打开前端项目的插件 目录 前言 使用的技术栈 步骤 问题发现 待优化 前言 一直以来开发都是先打开vscode,然后选择项目,在项目多的情况下会觉得挺繁琐:如果同时打开了许多vsc ...

  9. Vitess全局唯一ID生成的实现方案

    为了标识一段数据,通常我们会为其指定一个唯一id,比如利用MySQL数据库中的自增主键. 但是当数据量非常大时,仅靠数据库的自增主键是远远不够的,并且对于分布式数据库只依赖MySQL的自增id无法满足 ...

  10. Flask框架——Flask脚本、flask知识点补充

    文章目录 Flask_脚本 1 集成Python shell 1.1 flask-script的用法: 1.1.1 实例:flask-script的简单实现 1.1.1命令添加方式: 第一种(无参命令 ...