用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. React后台管理系统09 菜单组件的抽取

    修改Home.tsx的内容:将主菜单的内容进行抽离,然后单独引入=>MainMenu import { Breadcrumb, Layout, Menu } from 'antd'; impor ...

  2. SSM项目整合思路以及代码

    当前真实的企业开发中,SpringBoot已经统一Java企业应用开发,很少有在使用SSM项目来进行项目开发的. 可是在教学阶段,还是很有必要给学生讲解Java开发中一套很经典的框架,SSM框架.作为 ...

  3. BitLocker加密过程中断断电,能否恢复数据?

    BitLocker是Windows系统提供的磁盘加密功能,用户自己可以手动开启.在访问受BitLocker保护的磁盘分区时,需要先提供正确的密码.秘钥或是BEK文件.如果使用BitLocker将系统盘 ...

  4. FAQ: ansible playbook 中 tasks 与 handlers 的区别

    ansible自动化运维有两种执行方式,一种是对远程主机批量执行命令,使用ansible命令,直接调用模块加参数执行:另一种是对远程主机批量执行脚本,也是调用模块,但是要把参数按照yanl语法写到一个 ...

  5. 层叠样式表(CSS)1

    一.css的简介 1.层叠样式表的含义 层叠样式表:css是不仅是表现HTML的语言.还是进行样式修饰的语言 层叠:是对一个元素多次设置同一个样式,层层叠加覆盖,如不同的样式对一html标签进行修饰, ...

  6. html元数据

    元数据就是用来描述数据的数据.HTML中也有很多元数据. <meta>标签提供关于HTML文档的元数据:描述(description)\关键词(keywords).文档的作者(author ...

  7. 宝塔面板配合tomcat部署静态资源网站

    在宝塔中安装下图所示软件 第一步:上传代码到服务器 第二步,点击JAVA项目一键部署 点击添加项目 第一次使用时项目版本是为空的,点击下方的版本管理进行安装 需要填写的选项 至此配置完成 访问方式 i ...

  8. SpringCloud-Hystrix服务熔断与降级工作原理&源码

    先附上Hystrix源码图 在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC),在Spring Cloud可以用RestTemplate+Ribbon和Feign来调用 ...

  9. [loki]轻量级日志聚合系统loki快速入门

    前言 简述:loki是由grafana开源的日志聚合系统,相较于ELK.EFK更轻量. loki特性: 不对日志进行全文索引.通过存储压缩非结构化日志和仅索引元数据,Loki 操作起来会更简单,更省成 ...

  10. ChatGPT大师班 从入门到精通 视频教程 完整版

    本内容收集于:AIGC从入门到精通教程汇总 课程截图 课程目录 01.先导课:工具篇----ChatGPT平替解决方案及汉化教程.mp4 02.第1课:AIGC时代的到来.mp4 03.第2课:认识C ...