经常会有些需求需要动态拦截、修改请求,包括 http 或者 https 的所有请求, 简单例如以前很火的头脑王者,拦截请求,拿到题目匹配答案并最终在正确答案的前面打钩号。
这种东西怎么做呢,很简单 -> 代理,也就是今天的主角 github.com/elazarl/goproxy
package main
import (
"github.com/elazarl/goproxy"
"log"
"net/http"
)
func main() {
proxy := goproxy.NewProxyHttpServer()
log.Fatal(http.ListenAndServe(":8080", proxy))
}
几行代码就可以启动一个代理了,现在开始加各种逻辑
本地网络可能是通过代理上网的,或者爬虫需要于是只能做代理的代理,假设已有上级代理地址 pProxy="socks://127.0.0.1:10082",那么只需要简单设置即可:
proxy.Tr.Proxy = func(req *http.Request) (*url.URL, error) {
return url.Parse(pProxy)
}
请求可能是 http 的,当然更可能是 https 的,https 的请求就牵扯一个证书的证书的问题,证书可以自己生成,但这个生成的过程是很耗费 cpu 的,所以咱们可以缓存一下:
var memStore = gcache.New(300).LRU().Build()
func (MemCertStore) Fetch(hostname string, gen func() (*tls.Certificate, error)) (*tls.Certificate, error) {
if r, e := memStore.Get(hostname); e == nil {
return r.(*tls.Certificate), nil
} else {
cert, err := gen()
if err != nil {
return nil, err
}
memStore.Set(hostname, cert)
return cert, nil
}
}
这地方用了个支持 LRU 的缓存 github.com/bluele/gcache,如果没多少量或者不需要长时间运行直接扔内存里也可以,如果没有这段代码在请求量稍高的时候很容易跑满 CPU。
拿到请求时,判断请求地址是否符合规则,然后进行各种定制处理就可以了,如果需要在发出正式请求前执行就 OnRequest
,可以用于拦截请求,添加各种其它参数等;如果需要在返回前做修改就 OnResponse
,这就主要用于修改返回了
proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
if needHijack(req.URL) {
resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader([]byte(`HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
`+hijackResp))), req)
ctx.Resp = resp
}
return req, ctx.Resp
})
附带一个获取本机空闲端口的代码:
func GetFreePort(retry int) (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
if err != nil {
if retry >= 3 {
return 0, err
} else {
return GetFreePort(retry + 1)
}
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
if retry >= 3 {
return 0, err
} else {
return GetFreePort(retry + 1)
}
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port, nil
}