用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP内网穿透支持修改头信息

项目 ++wmproxy++

gite: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

修改header参数

但凡代理之类,基本上都有修改头参数的需求,就比如要获取客户端的真实IP,需要写入x-forward-for表示客户端的真实IP,要不然经过转发后的HTTP无法获取真实的客户端地址。

所以需要在转发的同时能进行处理头部信息的相关参数。故内网端不能仅做流量转发。而且客户端可能直接以纯HTTP2的协议请求内网的数据,所以同时需要支持HTTP/1.1及HTTP2,由于以上需求,我们把之前的简单的转发逻辑改成以服务端接收客户端请求的模式对数据进行重加工。

新流程如下

以下是数据从外网进入到内网服务器的加工流程

graph TD
A[外网客户端]
B[代理服务端-外网]
C[请求端]
D[新的请求端]
E[代理客户端--内网]
F[内网服务器]
A-->|请求http端口| B
B -->|解析成Request| C
C -->|修改Request中的Header|D
D -->|发送HTTP请求数据给CenterClient|E
E -->|请求内网服务器转发数据|F

以下是内网服务器返回数据给外网客户端的流程

graph TD
A[外网客户端]
B[代理服务端-外网]
C[新的返回端]
D[返回端]
E[代理客户端--内网]
F[内网服务器]
F -->|返回Response|E
E -->|发送HTTP数据给CenterServer|D
D -->|修改头信息加工|C
C -->|将数据转发给|B
B -->|返回数据|A

转发中的注意事项

我们可以获取完整的Request再进行请求吗?

如果我们这么操作,当数据包非常的大的时候例如1G,我们此时在内存中将有完整的1G内存,那么此时只需有数个同一类的请求,将会耗尽我们的内存,所以我们必须不能这么处理。

超大文件下载的转发

超大文件必须将得到的数据及时的转发给客户端,此时在内存中的值才不至于太大,又能及时的传输给客户端,要不然可能大文件下载到中转服务器的时间内客户端得不到任何数据就会空耗掉这时间。

http/1.1中的chunked的处理

因为http/1.1的chunked协议,由RFC 2616定义,

分块编码(Transfer-Encoding: chunked)是超文本传输协议(HTTP)中的一种数据传输机制,允许HTTP由网页服务器发送给客户端的数据可以分成多个部分。分块传输编码只在HTTP协议1.1版本(HTTP/1.1)中提供,如果头部中有该选项,则代表数据包是chunked格式。

数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。

比如我们常看到的

for data in res.chunk() {
}

就是表示的是数据分段接收,对于大数据这个尤为重要。

此种报文的示例

这时,报文中的实体需要改为用一系列分块来传输。

每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的 CRLF。

最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束。

例:

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked a\r\n
01234567890\r\n
1e\r\n
wmproxy is very good nat tool\r\n
0\r\n
\r\n

此种报文中我们必须进行解析,因为客户端可能是keep-alive选项,可以连续进行多发。所以收到的Request和Response都是连续的。必须知道何处结束才能继续解析下一个Request/Response。http2不需要,因为http2自带的data分包机制就有这些数据的处理

header数据的定义

  • header的修改分为两部分,一部分是对请求Request的重写,另一部分是对返回Response的重写。所以我们必须同时支持这两种,且将其区分出来。每条header信息我们将定定义一个可变长的数组,如第一个字符为proxy则表示对Request修改。
  • 关于修改的动作有
    1. 添加,如x-forward-for需要末尾添加,我们用操作符+,比如[proxy, +, x-forward-for, $client_ip]
    2. 删除,我们用操作符+,如[-, hidden]
    3. 设置,设置我们默认不做任何参数,直接以header_name开头,如[custom-key, custom-value]
    4. 默认值,有些值有了参数我们就不将其重写,如果没有我们则设为默认值,我们用操作符?,如[?, server, wmproxy]

所以我们client.yaml的配置新增至如下:

# 连接服务端地址
server: 127.0.0.1:8091
# 连接服务端是否加密
ts: true # 内网映射配置的数组
mappings:
#将localhost的域名转发到本地的127.0.0.1:8080
- name: web
mode: http
local_addr: 127.0.0.1:8080
domain: localhost
headers:
- [proxy, +, x-forward-for, $client_ip]
- [-, hidden]
- [custom-key, custom-value]
- [?, server, wmproxy]

mappings的结构修改

pub struct MappingConfig {
pub name: String,
pub mode: String,
pub local_addr: Option<SocketAddr>,
#[serde(default = "default_domain")]
pub domain: String,
#[serde(default = "default_header")]
pub headers: Vec<Vec<String>>,
}

我们把headers定义成一个动态的数组。根据不同的类型做不同的数据,因为长度有变化所以做不定长参数。

以下是代码解析

pub fn parse<T: Buf>(header: ProtFrameHeader, mut buf: T) -> ProxyResult<ProtMapping> {
must_have!(buf, 2)?;
let len = buf.get_u16() as usize;
let mut mappings = vec![]; for _ in 0..len {
let name = read_short_string(&mut buf)?;
let mode = read_short_string(&mut buf)?;
let domain = read_short_string(&mut buf)?;
let mut headers = vec![];
must_have!(buf, 2)?;
let len = buf.get_u16();
for _ in 0 .. len {
let mut header = vec![];
must_have!(buf, 1)?;
let sub_len = buf.get_u8();
for _ in 0..sub_len {
header.push(read_short_string(&mut buf)?);
}
headers.push(header);
}
mappings.push(MappingConfig::new(name, mode, domain, headers));
}
Ok(ProtMapping {
sock_map: header.sock_map(),
mappings,
})
}

如此解析成一个完整的对应域名的结构,因为服务端用不到local_addr所以不做传输。

核心代码的实现

核心处理代码在trans/http.rs下,外部传入一个可读可写的stream,可能是TcpStream也可能是TlsStream<TcpStream>或者其它,同时把接收的SocketAddr传入,以方便后续获取$client_ip的头文件信息。

预处理

pub async fn process<T>(self, inbound: T, addr: SocketAddr) -> Result<(), ProxyError<T>>
where
T: AsyncRead + AsyncWrite + Unpin + Debug,
{
println!("new process {:?}", inbound);
let build = Client::builder();
let (virtual_sender, virtual_receiver) = channel::<ProtFrame>(10);
let stream = VirtualStream::new(self.sock_map, self.sender.clone(), virtual_receiver);
let mut client = Client::new(build.value().ok().unwrap(), stream);
let (receiver, sender) = client.split().unwrap();
let oper = HttpOper {
receiver,
sender,
sender_work: self.sender_work.clone(),
virtual_sender: Some(virtual_sender),
sock_map: self.sock_map,
mappings: self.mappings.clone(),
http_map: None,
};
let mut server = Server::new(inbound, Some(addr), oper);
tokio::spawn( async move {
let _ = client.wait_operate().await;
});
let _ret = server.incoming(Self::operate).await;
if _ret.is_err() {
println!("ret = {:?}", _ret);
}
Ok(())
}

此时我们创建一个虚拟的Stream来做双边互传,但是此时我们还没有收到任何的Request请求,我们并不知道当前的Host,此时我们还未发送ProtCreate,等真正处理请求的时候做处理,HttpOper是处理每个操作时均会带的参数,我们可以根据自己需要带上该参数。

后续处理,其中我们读和写都用RecvStream,做到读多少数据转发多少数据,以保证数据处理的及时性

async fn inner_operate(
mut req: Request<RecvStream>,
data: Arc<Mutex<HttpOper>>,
) -> ProtResult<Option<Response<RecvStream>>> {
println!("receiver req = {:?}", req.url());
let mut value = data.lock().await;
let sender = value.virtual_sender.take();
// 传在该参数则为第一次, 第一次的时候发送Create创建绑定连接
if sender.is_some() {
let host_name = req.get_host().unwrap_or(String::new());
// 取得相关的host数据,对内网的映射端做匹配,如果未匹配到返回错误,表示不支持
{
let mut config = None;
let mut is_find = false;
{
let read = value.mappings.read().await;
for v in &*read {
if v.domain == host_name {
is_find = true;
config = Some(v.clone());
}
}
}
if !is_find {
return Ok(Some(Response::builder().status(404).body("not found").ok().unwrap().into_type()));
}
value.http_map = config;
} println!("do create prot {}, host = {:?}", value.sock_map, req.get_host()); let create = ProtCreate::new(value.sock_map, Some(req.get_host().unwrap_or(String::new())));
let _ = value.sender_work.send((create, sender.unwrap())).await;
} if let Some(config) = &value.http_map {
// 复写Request的头文件信息
HeaderHelper::rewrite_request(&mut req, &config.headers);
} // 将请求发送出去
value.sender.send(req).await?;
// 等待返回数据的到来
let mut res = value.receiver.recv().await;
if res.is_some() {
if let Some(config) = &value.http_map {
// 复写Response的头文件信息
HeaderHelper::rewrite_response(res.as_mut().unwrap(), &config.headers);
}
return Ok(res);
} else {
return Ok(Some(Response::builder().status(503).body("cant trans").ok().unwrap().into_type()));
}
}

以下是直接HTTP/1.1的请求示例

以下是直接HTTP/1.1升级成HTTP2的请求示例

以下是直接HTTP2的请求示例

请求的返回结果均带上了添加的头部信息,测试正常,至此HTTP的内网穿透数据打通。

10. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP内网穿透支持修改头信息的更多相关文章

  1. Java实战_手把手编写记事本

    Java运用SWT插件编写桌面记事本应用程序 可实现windows系统桌面记事本基本功能.傻瓜式教学,一步一步手把手操作.小白也可自己编写出完整的应用程序. 须要工具:Eclipse(带SWT插件) ...

  2. nginx是一个反向代理的软件

    nginx只是一个反向代理的软件,和语言无关,理论上支持任何Web平台,当然http://Asp.net也不例外,http://51aspx.com就是http://Asp.net开发的,前端暴漏的是 ...

  3. 3 微信开发本地代理环境的搭建--实现将内网ip映射到外网

    微信公众号的开发,要搭建网站,并且随时都有可能修改网站内容进行调试,这就需要临时外网能返回本地开发环境搭建的项目进行测试,即内网映射到公网,但是好多开发者没有自己的域名和服务器,这里我们先来搭建一个本 ...

  4. 手把手教你编写一个具有基本功能的shell(已开源)

    刚接触Linux时,对shell总有种神秘感:在对shell的工作原理有所了解之后,便尝试着动手写一个shell.下面是一个从最简单的情况开始,一步步完成一个模拟的shell(我命名之为wshell) ...

  5. 编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。如:ABCABCABC…… 依次递归

    import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.uti ...

  6. 编写一个算法,将非负的十进制整数转换为其他进制的数输出,10及其以上的数字从‘A’开始的字母表示

    编写一个算法,将非负的十进制整数转换为其他进制的数输出,10及其以上的数字从‘A’开始的字母表示. 要求: 1) 采用顺序栈实现算法: 2)从键盘输入一个十进制的数,输出相应的八进制数和十六进制数. ...

  7. 手把手教你编写一个简单的PHP模块形态的后门

    看到Freebuf 小编发表的用这个隐藏于PHP模块中的rootkit,就能持久接管服务器文章,很感兴趣,苦无作者没留下PoC,自己研究一番,有了此文 0×00. 引言 PHP是一个非常流行的web ...

  8. python练习:编写一个程序,要求用户输入10个整数,然后输出其中最大的奇数,如果用户没有输入奇数,则输出一个消息进行说明。

    python练习:编写一个程序,要求用户输入10个整数,然后输出其中最大的奇数,如果用户没有输入奇数,则输出一个消息进行说明. 重难点:通过input函数输入的行消息为字符串格式,必须转换为整型,否则 ...

  9. Go语言:编写一个 WebsiteRacer 的函数,用来对比请求两个 URL 来「比赛」,并返回先响应的 URL。如果两个 URL 在 10 秒内都未返回结果,返回一个 error。

    问题: 你被要求编写一个叫做 WebsiteRacer 的函数,用来对比请求两个 URL 来「比赛」,并返回先响应的 URL.如果两个 URL 在 10 秒内都未返回结果,那么应该返回一个 error ...

  10. 10.4 android输入系统_框架、编写一个万能模拟输入驱动程序、reader/dispatcher线程启动过程源码分析

    1. 输入系统框架 android输入系统官方文档 // 需FQhttp://source.android.com/devices/input/index.html <深入理解Android 卷 ...

随机推荐

  1. GO 语言中 slice 的理解

    GO 语言中 slice 理解 为什么说 Go 语言的 slice 是引用类型,其底层实现明明是一个结构体? slice 的底层实现是一个包含三个字段的结构体:指向底层数组的指针.slice 的长度和 ...

  2. React后台管理系统05 引入UI组件库

    使用命令引入UI组件库 npm install antd --save 使用命令安装ant的图标库 npm install --save @ant-design/icons ,注意这里的@后面是一个a ...

  3. SRS之StateThreads学习

    最近在看SRS的源码.SRS是基于协程开发的,底层使用了StateThreads.所以为了充分的理解SRS源码,需要先学习一下StateThreads.这里对StateThreads的学习做了一些总结 ...

  4. 春节无法线下社交聚会,来线上“一起X”共享体验

    引语: 共享体验或许是全真互联网时代最显著的标志. 被疫情深刻改变的一年里,人们的社交关系和社交活动正在发生巨大的改变. 一方面,从线下转线上,我们能通过互联网连接更多的人,参与各种形式的社交活动,将 ...

  5. 如何将Maven项目快速改造成一个java web项目(方式二)

    原始的maven项目,使用IDEA打开后,目录结构如下所示 删除pom.xml文件,删除resource目录,将java目录下的代码放到项目根目录下, 将webapp目录放到项目根目录下.如下图所示 ...

  6. BUUCTF-MISC-LSB(stegsolve的一种妙用)

    题目已知是LSB隐写 丢入stegsolve,点 > ,可以看见Red plane 0,Green plane 0,Blue plane 0上边好像有东西 点analyse->data e ...

  7. 分别在Windows系统和Linux系统环境安装anaconda3和tensorflow

    Windows系统环境 一.安装anaconda 这里安装的是Anaconda3-2019.03-Windows-x86_64.exe(python3.7版本) 二.检查安装环境 1.若anacond ...

  8. docker部署zabbix 6.0高可用集群实验

    0 实验环境 虚拟机,postgresql本地部署,zabbix server及nginx容器部署 1 postgresql 参看前作 <postgresql + timescaledb离线安装 ...

  9. while(1) ; CPU 会一直等下去吗

    标题答疑 不会,计算机从外部设备读取数据到存储器,每读取1word的数据,CPU对外设状态进行一次检查. 可能有小伙伴看到这就要退出了,别急,看下去或许会有收获. 首先解决第一个问题:一个字是多少字节 ...

  10. 【游戏开发笔记】编程篇_C#面向对象{上}

    @ 目录 1.变量和表达式 1.1注释 1.2C#控制台程序基本结构 1.3变量(从存储长度来看) 1.4变量的命名 1.5字面值 1.6运算符 2流程控制 2.1分支 2.2循环 3变量知识拓展 3 ...