平常我们会使用 Fiddler Charles 等抓包工具来抓包分析,但缺点是拓展性比较低,也没法作为一个代理服务器一直在后台代理。 而使用 elazarl / goproxy 这个代理库则可以轻松修改和处理 http(s) 请求,实现自己想要的功能。

本次目的

goproxy 支持两种方式实现 https「中间人攻击」:

1. 类似 fiddler 的代理方式代理流量,简单可用性高,需要在客户端安装信任 CA 证书

2. 透明代理,也称为强制代理(FORCED PROXIES),看名字就知道,是对客户端无感知的强制的,也是经常攻击劫持流量的方式,需要配置在路由器等地方 由于是在传输层劫持,处理数据实现功能不太方便

我们这次用例内容是使用客户端代理方式收到解析的 https 明文响应后 do something,下次有空再继续讲讲透明代理。

开始

要用到的第三方库

import (
    "github.com/elazarl/goproxy"
    "github.com/elazarl/goproxy/transport"
)

生成自签名证书

首先我们先生成用作认证的自签名 CA 证书,使用 openssl 或者其他工具都可以,生成 x.509 PEM格式的密钥对。 go 的标准库中提供一个文件来生成自签名 CA 证书密钥对,执行下面语句:

go run $GOROOT/src/crypto/tls/generate_cert.go --host localhost

$GOROOT/src/crypto/tls/

目录下会出现 cert.pem (公钥/证书)和 key.pem (私钥)文件, macos 直接双击安装 cert.pem 到钥匙串并完全信任,windows 后缀名改为 .crt 双击安装并分类到系统根证书。


设置代理证书

func SetCA(caCert, caKey []byte) error {
    goproxyCa, err := tls.X509KeyPair(caCert, caKey)
    if err != nil {
        return err
    }
    if goproxyCa.Leaf, err = x509.ParseCertificate(goproxyCa.Certificate[0]); err != nil {
        return err
    }
    goproxy.GoproxyCa = goproxyCa
    goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectAccept, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)}
    goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)}
    goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)}
    goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)}
    return nil
}

这段代码中

caCert
caKey

两个参数则是刚才生成的证书内容


配置代理服务器

verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") // 设置是否输出连接信息
addr := flag.String("addr", ":8080", "proxy listen address") // 监听端口和地址
flag.Parse()
proxy := goproxy.NewProxyHttpServer()
pwd, _ := os.Getwd()
caCert, err := ioutil.ReadFile(CaCertPath) // 设置为你刚才生成的证书路径
if err != nil {
    log.Fatal(err)
}
caKey, err := ioutil.ReadFile(CaKeyPath)  // 设置为你刚才生成的证书路径
proxy.SetCA(caCert, caKey)
proxy.Verbose = *verbose
proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)

基本配置完毕,接下来就可以监听请求/响应来做一些自己想做的功能

// 监听来自 qq.com 的响应 并在里面做出自己的处理动作
proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
    if resp.Request.Host == "qq.com" {
        doSomething(resp)
    }
    return resp
})
l, err := net.Listen("tcp", *addr)
if err != nil {
    log.Fatal("listen:", err)
}

log.Println("Starting Proxy")
http.Serve(sl, proxy)


DoFunc() 会处理所有传进去的响应,在里面可以自由处理响应再返回给客户端, 由此非常简单地就可以开启一个 https 的代理服务器了,接下来就可以在 doSomething() 中自由使用 go 来搭建你想要的功能。


Notice: 进入到处理响应这块之后需要对 golang 处理 http 请求有一定了解。 例如假如你要对

resp.Body 响应体全部读取并做处理,那么你读取修改后直接返回你会发现客户端页面拿不到任何请求。 这是因为 golang 中的 http.Response http.Request Body 都是 io.ReadCloser 类型,当你在读取 resp.Body 数据后会被自动关闭回收掉。 这在一般情况下都是没问题的,http 请求和响应在读取后就将连接关闭内存清空,但在我们的代理中连接还会被转发使用,所以需要重新再造一个 Body :

buf := new(bytes.Buffer)
buf.ReadFrom(resp.Body)
resp.Body = ioutil.NopCloser(bytes.NewBuffer(buf.Bytes()))

其他用例

修改标头 Header

proxy.OnRequest().DoFunc(
    func(r *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) {
        r.Header.Set("X-GoProxy","yxorPoG-X")
        return r,nil
    })

返回 nil 作为响应就可以只修改请求继续请求服务器获得响应,如果返回响应的话就会立马将你造的响应返回到客户端而不会请求真实服务器。

工作时间禁止上 reddit

proxy.OnRequest(goproxy.DstHostIs("www.reddit.com")).DoFunc(
    func(r *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) {
        if h,_,_ := time.Now().Clock(); h >= 8 && h <= 17 {
            return r,goproxy.NewResponse(r,
                    goproxy.ContentTypeText,http.StatusForbidden,
                    "Don't waste your time!")
        }
        return r,nil
})

更多信息请直接查看源码

GITHUB地址:elazarl/goproxy