4合一之HTTP代理实现
简单HTTP代理golang
实现
- 基础框架搭建,直接上代码
1 |
|
完善 requestHandler 实现HTTP代理请求处理
由于HTTP Request Header中的Host有可能是不带端口号的,先定义一个方法将HTTP Request中的Host 转为 host:port 格式
1
2
3
4
5
6
7
8
9
10
11func hostToTcpAddr(host string, defPort int) (addr string, err error) {
target, err := url.Parse("shs://" + host)
if err != nil {
return
}
port, err := strconv.Atoi(target.Port())
if err != nil {
port = defPort
}
return fmt.Sprintf("%s:%d", target.Hostname(), port), nil
}继续完善requestHandler方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25func requestHandler(ctx *fasthttp.RequestCtx) {
// 暂时不支持CONNECT方法
if "CONNECT" == string(ctx.Method()) {
ctx.Error(http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
target, err := hostToTcpAddr(string(ctx.Host()), DefaultPort)
if err != nil {
ctx.Error(err.Error(), http.StatusServiceUnavailable)
return
}
//客户端连接代理服务器的时候可能会传递一些给代理服务专用的`Header`,我们需要在转发到目标服务的时候剔除
for _, h := range []string{"Proxy-Connection", "Proxy-Authenticate"} {
ctx.Request.Header.Del(h)
}
ctx.Request.SetConnectionClose()
rConn, err := net.Dial("tcp", target)
if err != nil {
ctx.Error(err.Error(), fasthttp.StatusServiceUnavailable)
log.Println("与目标服务器建立连接失败", err)
return
}
copyHttpPayload(ctx, rConn)
}在客户端与目标服务器之间转发HTTP请求及响应,即copyHttpPayload方法
在实现该方法前我们先回忆一下HTTP Response中Body长度的获取,通常Body的长度通过Header中的content-length来标识,但是有2种例外。- 使用HTTP/1.0时,可不返回content-length (HTTP/1.0 不支持连接复用,一个请求完成之后即关闭连接,客户端可通过EOF来判断body是否传输结束)
- Response的transfer-encoding 为 truck时,无需返回 content-length
了解了Body的长度的获取方式,继续完善copyHttpPayload方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57func copyHttpPayload(ctx *fasthttp.RequestCtx, rConn io.ReadWriteCloser) {
n, err := ctx.Request.WriteTo(rConn)
if err != nil {
_ = rConn.Close()
return
}
reader := bufio.NewReader(rConn)
err = ctx.Response.Header.Read(reader)
if err != nil {
_ = rConn.Close()
if err != io.EOF {
ctx.Error(err.Error(), fasthttp.StatusServiceUnavailable)
}
return
}
cl := ctx.Response.Header.ContentLength()
switch cl {
case -1: //chunk
ctx.SetConnectionClose()
ctx.SetBodyStreamWriter(func(w *bufio.Writer) {
defer func() {
_ = rConn.Close()
}()
for {
data, _, err := reader.ReadLine()
if err != nil {
log.Println(err)
return
}
if strings.TrimSpace(string(data)) == "0" {
break
}
cs, err := strconv.ParseInt(string(data), 16, 32)
if err != nil {
log.Println(err)
return
}
_, copyErr := io.CopyN(w, reader, cs)
if copyErr != nil {
return
}
_, err = reader.Discard(2)
if err != nil {
log.Println(err)
return
}
}
})
case -2: //EOF
ctx.SetBodyStream(reader, -1)
case 0:
_ = rConn.Close()
return
default: //Fix len
ctx.SetBodyStream(reader, cl)
}
}再来理解一下
truck
模式,即分块模式,适用于大文件
不确定长度数据
动态压缩或编码
等场景,格式为:1
2
3
4
5
6
70A
xx xx xx xx xx xx xx xx xx xx
01
xx
03
xx xx xx
01,3,5,7行为
truck
数据长度,为16进制数字没有0x前缀的文本标识方式,0A
标识块长度为10个byte
,最后一个0表示为传输结束,
2,4,6 行为数据块.
至此我们就完成了一个简单的HTTP
代理服务器
三方依赖
github.com/valyala/fasthttp