HTTP Proxy理论与Go实践

2022/02/11 12:52

正在学习计算机网络的HTTP部分,准备自己写一个HTTP代理服务器来练练手。因为Go语言对网络编程支持比较好,参考资料也比较多所以本文的代码实践部分就用Go来写了。

1. HTTP代理

首先从HTTP正向代理1开始,在HTTP正向代理中HTTP代理服务器就相当于一个中间人,从浏览器中接收到请求,再将HTTP请求转发到Web服务器,并且把Web服务器的响应再转发到浏览器。

A proxy must be both a server and a client

下面是Go语言的实现代码,因为完整代理直接粘贴上不太好看,所以只展示了核心的HTTP Handler部分。

// HTTP 处理HTTP请求
func (p *Proxy) HTTP(w http.ResponseWriter, r *http.Request) {
    // 创建客户端对象
    transport := http.DefaultTransport
    // 启动事务
    res, err := transport.RoundTrip(r)
    // 处理返回的错误消息
    if err != nil {
        log.Printf("request forward err %v\n", err)
        w.WriteHeader(http.StatusBadGateway)
        _, _ = w.Write([]byte(err.Error()))
        return
    }
    // 返回http头
    w.WriteHeader(res.StatusCode)
    // 返回Body
    _, _ = io.Copy(w, res.Body)
    _ = res.Body.Close()
}

现在我们可以编译运行试一下,可以通过SwitchyOmega这个浏览器插件,使浏览器的请求通过我们的代理服务器。访问HTTP网站的时候代理工作正常,但是在访问HTTPS的网站的时候则出现了错误,因为我们编写的代理本质就是中间人,而HTTPS诞生的目的就是阻止中间人劫持的,他是通过私钥与公钥进行相互验证,因为我们没有HTTPS证书的私钥所以无法与客户端建立起HTTPS链接,自然无法代理HTTPS的内容了。

2. HTTPS代理

实现HTTPS代理的有好几种,第一种就是自己伪造一个证书,这样浏览器就可以与代理服务器建立TLS连接,因为代理服务器有伪造证书的私钥所以可以解密浏览器发送过来的数据得到数据原文,再用Web服务器的公钥对数据进行加密发送给Web服务器,常用的HTTP抓包工具使用的就是这种方法。还用另外一种方法就是建立隧道,使得HTTPS通过HTTP进行连接,因为这种方式更加方便,所以我们在这里编写的HTTPS代理服务器就是采用的这种方法

Tunnels let non-HTTP traffic flow through HTTP connections

当我要通过HTTPS访问Web网站的时候,首先浏览器会发送一个CONNECT请求与Web服务器建立TCP连接,当TCP连接成功建立Web服务器返回一条HTTP/1.0 200 Connection Established的消息,然后Web服务器与浏览器就通过TCP连接进行通讯。通过隧道代理的原理就是浏览器像代理服务器发送CONNECT请求,然后由代理服务器建立与Web服务器的TCP连接,这时相当于代理服务器与两端都建立了TCP连接,此时代理服务器只需对TCP数据进行双向转发即可,不用管里面的加密数据。

Gateways can proxy-authenticate a client before it’s allowed to use a tunnel

下面是HTTPS代理函数

// HTTPS 处理HTTPS请求
func (p *Proxy) HTTPS(w http.ResponseWriter, r *http.Request) {
    // 获取服务器地址
    addr := r.URL.Host
    // 接管客户端到代理服务器的HTTP连接(将HTTP链接当作TCP用)
    hij, ok := w.(http.Hijacker)
    if !ok {
        log.Println("hijack err")
    }
    client, _, err := hij.Hijack()
    if err != nil {
        log.Printf("hijack err %v\n", err)
        return
    }

    // 建立与Web服务器的TCP连接
    server, err := net.Dial("tcp", addr)
    if err != nil {
        log.Printf("Request forward err %v\n", err)
        return
    }
    // 成功建立连接
    _, _ = client.Write([]byte("HTTP/1.0 200 Connection Established\r\n\r\n"))
    // 建立通道进行双向数据转发
    go func(dst io.Writer, src io.Reader) {
        _, _ = io.Copy(server, client)
    }(server, client)
    // 建立通道进行双向数据转发
    go func(dst io.Writer, src io.Reader) {
        _, _ = io.Copy(client, server)
    }(server, client)
}

3. 验权

到目前为止已经完成了HTTP与HTTPS代理这两个基础功能,如果需要让指定的用户才能使用这个代理服务器就需要在上面的基础上再增加一个验证权限的功能了。代理服务器与权限验证与Web服务器的权限验证差不多,只是有些头名称不同。

Web server Proxy server
Unauthorized status code: 401 Unauthorized status code:407
WWW-Authenticate Proxy-Authenticate
Authorization Proxy-Authorization
Authentication-Info Proxy-Authentication-Info

验证方法非常简单,只是有些细节需要注意一下,Proxy-Authorization头的格式是这样的Basic username:password有一个basic的前缀,并且username:password部分是采用bs64加密了的。下面是相关的代码实现

// 权限验证处理函数
func (p *Proxy) proxyAuthHandler(w http.ResponseWriter, r *http.Request) bool {
    if p.proxyAuthCheck(r) {
        return true
    }
    log.Printf("proxy auth required\n")
    // 提示浏览器需要密码
    w.Header().Add("Proxy-Authenticate", "Basic realm=\"password worry\"")
    w.WriteHeader(http.StatusProxyAuthRequired)
    return false
}

func (p *Proxy) proxyAuthCheck(r *http.Request) bool {
    if p.username == "" && p.password == "" {
        return true //表示账号与密码都没有设置
    }
    auth := r.Header.Get("Proxy-Authorization")
    if auth == "" {
        return false
    }
    // 默认前缀
    prefix := "Basic "
    // 登入信息默认采用bs64加密
    return auth == prefix+base64.
        StdEncoding.
        EncodeToString([]byte(p.username+":"+p.password))
}

  1. Proxy server - Wikipedia↩︎