10. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP内网穿透支持修改头信息
用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,由于以上需求,我们把之前的简单的转发逻辑改成以服务端接收客户端请求的模式对数据进行重加工。
新流程如下
以下是数据从外网进入到内网服务器的加工流程
A[外网客户端]
B[代理服务端-外网]
C[请求端]
D[新的请求端]
E[代理客户端--内网]
F[内网服务器]
A-->|请求http端口| B
B -->|解析成Request| C
C -->|修改Request中的Header|D
D -->|发送HTTP请求数据给CenterClient|E
E -->|请求内网服务器转发数据|F
以下是内网服务器返回数据给外网客户端的流程
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修改。 - 关于修改的动作有
- 添加,如
x-forward-for需要末尾添加,我们用操作符+,比如[proxy, +, x-forward-for, $client_ip] - 删除,我们用操作符
+,如[-, hidden] - 设置,设置我们默认不做任何参数,直接以header_name开头,如
[custom-key, custom-value] - 默认值,有些值有了参数我们就不将其重写,如果没有我们则设为默认值,我们用操作符
?,如[?, 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内网穿透支持修改头信息的更多相关文章
- Java实战_手把手编写记事本
Java运用SWT插件编写桌面记事本应用程序 可实现windows系统桌面记事本基本功能.傻瓜式教学,一步一步手把手操作.小白也可自己编写出完整的应用程序. 须要工具:Eclipse(带SWT插件) ...
- nginx是一个反向代理的软件
nginx只是一个反向代理的软件,和语言无关,理论上支持任何Web平台,当然http://Asp.net也不例外,http://51aspx.com就是http://Asp.net开发的,前端暴漏的是 ...
- 3 微信开发本地代理环境的搭建--实现将内网ip映射到外网
微信公众号的开发,要搭建网站,并且随时都有可能修改网站内容进行调试,这就需要临时外网能返回本地开发环境搭建的项目进行测试,即内网映射到公网,但是好多开发者没有自己的域名和服务器,这里我们先来搭建一个本 ...
- 手把手教你编写一个具有基本功能的shell(已开源)
刚接触Linux时,对shell总有种神秘感:在对shell的工作原理有所了解之后,便尝试着动手写一个shell.下面是一个从最简单的情况开始,一步步完成一个模拟的shell(我命名之为wshell) ...
- 编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。如:ABCABCABC…… 依次递归
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.uti ...
- 编写一个算法,将非负的十进制整数转换为其他进制的数输出,10及其以上的数字从‘A’开始的字母表示
编写一个算法,将非负的十进制整数转换为其他进制的数输出,10及其以上的数字从‘A’开始的字母表示. 要求: 1) 采用顺序栈实现算法: 2)从键盘输入一个十进制的数,输出相应的八进制数和十六进制数. ...
- 手把手教你编写一个简单的PHP模块形态的后门
看到Freebuf 小编发表的用这个隐藏于PHP模块中的rootkit,就能持久接管服务器文章,很感兴趣,苦无作者没留下PoC,自己研究一番,有了此文 0×00. 引言 PHP是一个非常流行的web ...
- python练习:编写一个程序,要求用户输入10个整数,然后输出其中最大的奇数,如果用户没有输入奇数,则输出一个消息进行说明。
python练习:编写一个程序,要求用户输入10个整数,然后输出其中最大的奇数,如果用户没有输入奇数,则输出一个消息进行说明. 重难点:通过input函数输入的行消息为字符串格式,必须转换为整型,否则 ...
- Go语言:编写一个 WebsiteRacer 的函数,用来对比请求两个 URL 来「比赛」,并返回先响应的 URL。如果两个 URL 在 10 秒内都未返回结果,返回一个 error。
问题: 你被要求编写一个叫做 WebsiteRacer 的函数,用来对比请求两个 URL 来「比赛」,并返回先响应的 URL.如果两个 URL 在 10 秒内都未返回结果,那么应该返回一个 error ...
- 10.4 android输入系统_框架、编写一个万能模拟输入驱动程序、reader/dispatcher线程启动过程源码分析
1. 输入系统框架 android输入系统官方文档 // 需FQhttp://source.android.com/devices/input/index.html <深入理解Android 卷 ...
随机推荐
- 11. Mybatis的逆向工程
正向工程:先创建 Java 实体类,由框架负责根据实体类生成数据库表. Hibernate 是支持正向工 程的. 逆向工程:先创建数据库表,由框架负责根据数据库表,反向生成如下资源: Java 实体类 ...
- Codeforces Round #877 (Div. 2) A-E
A 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; bool solve() { int n; ...
- 使@schedule支持多线程的配置类
package com.longshine.goverquartz.core.config;import org.springframework.boot.autoconfigure.batch.Ba ...
- javaSE 温故而知新
重温 javaSE 前言:有地基才能有高楼大厦 目录 重温 javaSE 认识java Java基础 1.数据类型 1.1 基本数据类型: 1.2 引用数据类型 1.3 基本数据类型的包装类 1.4 ...
- 用 Golang 从0到1实现一个高性能的 Worker Pool(一) - 每天5分钟玩转 GPT 编程系列(3)
目录 1. 概述 2. 设计 2.1 让 GPT-4 给出功能点 2.2 自己总结需求,再给 GPT 派活 3. 实现 3.1 你先随意发挥 3.2 你得让 Worker 跑起来呀 3.3 你说说 P ...
- 小白也能看懂的 ROC 曲线详解
作者:PrimiHub-Kevin ROC 曲线是一种坐标图式的分析工具,是由二战中的电子和雷达工程师发明的,发明之初是用来侦测敌军飞机.船舰,后来被应用于医学.生物学.犯罪心理学. 如今,ROC 曲 ...
- happens-before 原则
happens-before 简述 从 JDK 5 开始,Java 使用新的 JSR-133 内存模型.JSR-133 使用 happens-before 的概念来阐述操作之间的内存可见性.在 JMM ...
- 【持续更新】C++ 并不完全是 C 的超集!
一些容易被忽略的 C 与 C++ 的不兼容特性 头文件和命名空间 C 标准库头文件名在 C++ 中通常去除扩展名,并加上 c 前缀,如: stdio.h -> cstdio stdlib.h - ...
- 使用 virt-install 命令创建虚拟机
实践 参考文档:官方手册 这个命令适用于创建第一个虚拟机,后面如果再增加,修改xml文件或者使用clone命令就可以了. centos.sh #!/bin/bash name='centos7' is ...
- 2021-11-17 WPF初识
StackPanel容器:默认竖直排列,Orientation="Horizontal"横向排列,超过就不会显示 wrapPanel:超过会自动换行 设置样式: <Windo ...