最近在用Golang做httpProxy,看到一篇不错的老外博客.所以翻译一遍以备后用

原文地址:https://medium.com/@mlowicki/http-s-proxy-in-golang-in-less-than-100-lines-of-code-6a51c2f2c38c

HTTP(S) Proxy在Golang中实现不超过一百行代码

本文的目标是为HTTP何HTTPS实施代理服务器.处理HTTP请求只是一个简单的解析请求:将http的请求传递给最终的目标服务器,读取响应并将其传递回客户端. 我们所需要的就是内置http server和client(net/http);HTTPS不同,因为它将使用HTTP CONNECT隧道技术.第一个客户端使用HTTP CONNECT方法发送请求以建立客户端和目标服务器之间的隧道. 当这种由两个TCP链接组成的隧道准备就绪后,客户端就会与目标服务器开始定义的TLS握手.以建立安全的连接并在随后发送请求并接收响应.

证书

我们的代理将是一个HTTPS服务器(当使用–proto https时),所以我们需要证书和私钥.为了本文的目的,我们使用自签名证书.可以使用以下脚本生成证书:

#!/usr/bin/env bash
case `uname -s` in
    Linux*)     sslConfig=/etc/ssl/openssl.cnf;;
    Darwin*)    sslConfig=/System/Library/OpenSSL/openssl.cnf;;
esac
openssl req \
    -newkey rsa:2048 \
    -x509 \
    -nodes \
    -keyout server.key \
    -new \
    -out server.pem \
    -subj /CN=localhost \
    -reqexts SAN \
    -extensions SAN \
    -config <(cat $sslConfig \
        <(printf '[SAN]\nsubjectAltName=DNS:localhost')) \
    -sha256 \
    -days 3650

它需要说服你的操作系统来信任这样的证书。 在OS X中,可以使用钥匙串访问完成 - https://tosbourn.com/getting-os-x-to-trust-self-signed-ssl-certificates/。

HTTP

为了支持HTTP,我们将使用内置的HTTP Server和Client。 代理的角色是处理HTTP请求,将此请求传递给目标服务器并将响应发送回客户端。

HTTP CONNECT 隧道

假设客户端想要使用HTTPS或WebSockets来与服务器通信.客户端会察觉到使用了代理。简单的http请求/响应流程不再支持,因为客户端需要建立与服务器的安全连接(HTTPS)或系统通过TCP连接(WebSockets)使用其他协议.实现的方法是使用HTTP CONNECT.它会告诉代理服务器建议与目标服务器的TCP连接,并完成代理到客户端和来自客户端的TCP流。这样代理服务器不会终止SSL,但是在客户端和目标服务之间传递数据,因为代理和客户端简历了安全的连接.

实现

package main
import (
    "crypto/tls"
    "flag"
    "io"
    "log"
    "net"
    "net/http"
    "time"
)
func handleTunneling(w http.ResponseWriter, r *http.Request) {
    dest_conn, 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
    }
    client_conn, _, err := hijacker.Hijack()
    if err != nil {
        http.Error(w, err.Error(), http.StatusServiceUnavailable)
    }
    go transfer(dest_conn, client_conn)
    go transfer(client_conn, dest_conn)
}
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() {
    var pemPath string
    flag.StringVar(&pemPath, "pem", "server.pem", "path to pem file")
    var keyPath string
    flag.StringVar(&keyPath, "key", "server.key", "path to key file")
    var proto string
    flag.StringVar(&proto, "proto", "https", "Proxy protocol (http or https)")
    flag.Parse()
    if proto != "http" && proto != "https" {
        log.Fatal("Protocol must be either http or https")
    }
    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)),
    }
    if proto == "http" {
        log.Fatal(server.ListenAndServe())
    } else {
        log.Fatal(server.ListenAndServeTLS(pemPath, keyPath))
    }
}

提供的代码不是生产级解决方案。 它缺乏 处理逐跳报头,在两个连接之间复制数据或者通过net/http暴露数据时设置超时 - 在“The complete guide to Go net/http timeouts”中可以查看到更多内容。

我们的服务器在获取请求时将采取两种处理方式:处理HTTP或处理HTTP CONNECT隧道。 这是通过:

http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodConnect {
        handleTunneling(w, r)
    } else {
        handleHTTP(w, r)
    }
})

handleHTTP不言自明是处理http的函数,所以让我们专注于处理隧道。 handleTunneling的第一部分是关于设置连接到目标服务器:

dest_conn, 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 Server维持的连接:

hijacker, ok := w.(http.Hijacker)
    if !ok {
        http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
        return
    }
    client_conn, _, err := hijacker.Hijack()
    if err != nil {
        http.Error(w, err.Error(), http.StatusServiceUnavailable)
    }

Hijacker接口允许接管连接。 之后,调用者负责管理这种连接(HTTP库不会负责)。

一旦我们拿到两个TCP连接(客户端→代理,代理→目标服务器)之后,我们需要设置隧道:

go transfer(dest_conn, client_conn)
go transfer(client_conn, dest_conn)

在两个例程中,数据被复制到两个方向:从客户端到目标服务器并向后(backward)。

测试

要测试我们的代理,您可以使用例如 Chrome:

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

或者Curl

> curl -Lv --proxy https://localhost:8888 --proxy-cacert server.pem https://google.com

curl需要使用HTTPS代理支持(在7.52.0中引入)来构建。

HTTP/2

在我们的服务器HTTP / 2支持已被故意删除,因为劫持不支持的。 更多信息可以查看#14797

更新

请阅读https://medium.com/@mlowicki/https-proxies-support-in-go-1-10-b956fb501d6b关于对Go 1.10中的HTTPS代理的支持。