目标是现实一个 HTTP HTTPS 的代理服务器, 目前代理的实现方法有两种

普通代理: 这种代理扮演的是中间人角色, 对于客户端来说, 它就服务器, 对于服务端来说, 它是客户端, 它负责在中间来回传递 HTTP 报文

隧道代理: 它是通过 HTTP 正文部分(body) 完成代理, 以 HTTP 的方式实现基于 TCP 的应用层协议代理, 这种代理使用 HTTP 的 CONNECT 方法建立连接

这是一次 HTTP 的请求, 用 \r\n 进行换行, 碰到连续两个 \r\n 后内容为请求数据, 分为以下几个部分

  1. 请求行 (request line)
  2. 请求头 (header)
  3. 空行
  4. 请求数据 (body)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
curl -Lv http://baidu.com

> GET / HTTP/1.1
> Host: baidu.com
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 25 Jun 2020 12:12:32 GMT
< Server: Apache
< Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
< ETag: "51-47cf7e6ee8400"
< Accept-Ranges: bytes
< Content-Length: 81
< Cache-Control: max-age=86400
< Expires: Fri, 26 Jun 2020 12:12:32 GMT
< Connection: Keep-Alive
< Content-Type: text/html
<
<html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</html>

实现

 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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

import (
    "io"
    "log"
    "net"
    "net/http"
    "time"
)

func handleTunneling(w http.ResponseWriter, r *http.Request) {
    destConn, err := net.DialTimeout("tcp", r.Host, 10*time.Second)
    if err != nil {
        http.Error(w, err.Error(), http.StatusServiceUnavailable)
        return
    }
    w.WriteHeader(http.StatusOK)
    hijacker, ok := w.(http.Hijacker)
    if !ok {
        http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
        return
    }
    clientConn, _, err := hijacker.Hijack()
    if err != nil {
        http.Error(w, err.Error(), http.StatusServiceUnavailable)
    }
    go transfer(destConn, clientConn)
    go transfer(clientConn, destConn)
}

func transfer(destination io.WriteCloser, source io.ReadCloser) {
    defer destination.Close()
    defer source.Close()
    io.Copy(destination, source)
}

func handleHTTP(w http.ResponseWriter, req *http.Request) {
    resp, err := http.DefaultTransport.RoundTrip(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusServiceUnavailable)
        return
    }
    defer resp.Body.Close()
    copyHeader(w.Header(), resp.Header)
    w.WriteHeader(resp.StatusCode)
    io.Copy(w, resp.Body)
}

func copyHeader(dst, src http.Header) {
    for k, vv := range src {
        for _, v := range vv {
            dst.Add(k, v)
        }
    }
}

func main() {
    server := &http.Server{
        Addr: ":8888",
        Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if r.Method == http.MethodConnect {
                handleTunneling(w, r)
            } else {
                handleHTTP(w, r)
            }
        }),
        // Disable HTTP/2.
        TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
    }

    log.Fatal(server.ListenAndServe())
}

以上代码不适用于生产环境

上述代码实现了普通代理和隧道代理两个方法, 通过判断请求方是否使用 CONNECT 方法请求代理服务器 来区分不同的处理逻辑

1
2
3
4
5
6
7
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodConnect {
        handleTunneling(w, r)
    } else {
        handleHTTP(w, r)
    }
})

基于普通代理的逻辑是容易理解的, 这里主要看一下是如何实现基于隧道代理的, handleTunneling 第一步是建立和目标服务器的连接

1
2
3
4
5
6
destConn, err := net.DialTimeout("tcp", r.Host, 10*time.Second)
if err != nil {
    http.Error(w, err.Error(), http.StatusServiceUnavailable)
    return
 }
 w.WriteHeader(http.StatusOK)

下一步是劫持 HTTP 服务维持的连接, net.Conn 类型

1
2
3
4
5
6
7
8
9
hijacker, ok := w.(http.Hijacker)
    if !ok {
        http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
        return
    }
    clientConn, _, err := hijacker.Hijack()
    if err != nil {
        http.Error(w, err.Error(), http.StatusServiceUnavailable)
    }

Hijacker interface 获取连接后, 调用者来 维护此连接, HTTP 标准库就不会在负责管理了

这时已经建立了两个 TCP 连接(客户端 -> 代理端, 代理端 -> 目标服务器), 最后一步就是互相转发 他们的消息

1
2
go transfer(destConn, clientConn)
go transfer(clientConn, destConn)

测试

使用 Chrome:

1
Chrome --proxy-server=https://localhost:8888

使用 Curl:

1
curl -Lv --proxy https://localhost:8888 --proxy-cacert server.pem https://baidu.com

参考

  1. https://medium.com/@mlowicki/http-s-proxy-in-golang-in-less-than-100-lines-of-code-6a51c2f2c38c
  2. https://imququ.com/post/web-proxy.html