用golang写一个http代理,可以抓包和科学上网
缘起
因为在工作中需要对上网进行限制,只让我们的app上网,意思就是放行app请求的所有域名或ip,而其他域名都禁止,所以我需要对app的http请求进行抓包。上网搜了一下,win下的fiddler不错,可惜我用的是Linux系统,fiddler不跨平台,找了下linux下的抓包软件,当然tcpdump和wireshare是足够强大的,完全可以实现我要的小小要求,但用起来有一定的复杂性,门槛稍高。在网上找到其他类似的软件还挺多,charles、NProxy等,最后发现mitmproxy最符合我的胃口,如图:
但是mitmproxy的安装依赖太多,python就是这样,一不小心就报错了,so,想着用golang实现类似的功能,自己也很喜欢go语言,如果有空能安静写写自己喜欢的代码,是多么的幸福。想好了就实践,Let’s do it,当然凡事都有个从简到繁的过程,下面的特性慢慢增加。
特性
特性,也可以说功能,以下是实现或者将要实现的特性
- [X] http代理
- [X] http请求响应的抓取
- [ ] 修改http请求
- [ ] 重复请求
- [ ] 同时监听多端口
- [ ] 支持socks5、websocket、https协议
- [ ] 界面支持终端和网页两种形式
安装
git clone https://github.com/sheepbao/gomitmproxy.git cd gomitmproxy go build 使用
- http代理
gomimtproxy 不带任何参数,表示http代理,默认端口8080,更改端口用 -port
- http抓包
gomimtproxy -m 加 -m 参数,表示抓取http请求和响应

http代理科学上网
首先你得有个墙外的服务器,如阿里香港的服务器,为图中的Server,假设其ip地址为:22.222.222.222
在Server执行: gomitmproxy -port 8888 在你自己电脑执行: gomitmproxy -port 8080 -raddr 22.222.222.222:8888 然后浏览器设置代理,ip为localhost,端口为8080,即可实现科学上网

源码简析
对于网络编程,Anything is a socket! 实现http代理并不难,简单地说就是用代理服务器代替客户端去请求web服务,然后在把请求的结果返回给客户端。 先来个示意图: 
1. 客户端发起一个到gomitmproxy的连接,并且提交了HTTP CONNECT请求。 2. gomitmproxy返回200表示连接已经建立。 if req.Method == "CONNECT" { b := []byte("HTTP/1.1 200 Connection Established\r\n" + "Proxy-Agent: golang_proxy/" + Version + "\r\n\r\n") _, err := conn.Write(b) if err != nil { logger.Println("Write Connect err:", err) return } } else { req.Header.Del("Proxy-Connection") req.Header.Set("Connection", "Keep-Alive") if err = req.Write(conn_proxy); err != nil { logger.Println("send to server err", err) return } } 3. gomitmproxy连接服务器的host,并将客户端的请求发送给服务器,如果没加 -m 参数,那直接将客户端和服务器的io口通过gomitmproxy连接,gomitmproxy很像水管的连接器,把断开的水管连接起来。 //两个io口的连接 func Transport(conn1, conn2 net.Conn) (err error) { rChan := make(chan error, 1) wChan := make(chan error, 1) go MyCopy(conn1, conn2, wChan) go MyCopy(conn2, conn1, rChan) select { case err = <-wChan: case err = <-rChan: } return } func MyCopy(src io.Reader, dst io.Writer, ch chan<- error) { _, err := io.Copy(dst, src) ch <- err } 如果加了 -m 参数,表示要截取http请求和响应。 //打印http请求和响应 func httpDump(req *http.Request, resp *http.Response) { defer resp.Body.Close() var respStatusStr string respStatus := resp.StatusCode respStatusHeader := int(math.Floor(float64(respStatus / 100))) switch respStatusHeader { case 2: respStatusStr = Green("<--" + strconv.Itoa(respStatus)) case 3: respStatusStr = Yellow("<--" + strconv.Itoa(respStatus)) case 4: respStatusStr = Magenta("<--" + strconv.Itoa(respStatus)) case 5: respStatusStr = Red("<--" + strconv.Itoa(respStatus)) } fmt.Println(Green("Request:")) fmt.Printf("%s %s %s\n", Blue(req.Method), req.RequestURI, respStatusStr) for headerName, headerContext := range req.Header { fmt.Printf("%s: %s\n", Blue(headerName), headerContext) } fmt.Println(Green("Response:")) for headerName, headerContext := range resp.Header { fmt.Printf("%s: %s\n", Blue(headerName), headerContext) } respBody, err := ioutil.ReadAll(resp.Body) if err != nil { logger.Println("read resp body err:", err) } else { acceptEncode := resp.Header["Content-Encoding"] var respBodyBin bytes.Buffer w := bufio.NewWriter(&respBodyBin) w.Write(respBody) w.Flush() for _, compress := range acceptEncode { switch compress { case "gzip": r, err := gzip.NewReader(&respBodyBin) if err != nil { logger.Println("gzip reader err:", err) } else { defer r.Close() respBody, _ = ioutil.ReadAll(r) } break case "deflate": r := flate.NewReader(&respBodyBin) defer r.Close() respBody, _ = ioutil.ReadAll(r) break } } fmt.Printf("%s\n", string(respBody)) } fmt.Printf("%s%s%s\n", Black("####################"), Cyan("END"), Black("####################")) } 4. gomitmproxy将请求服务器的响应发送给客户端。 resp, err := http.ReadResponse(bufio.NewReader(conn_proxy), req) if err != nil { logger.Println("read response err:", err) return } respDump, err := httputil.DumpResponse(resp, true) if err != nil { logger.Println("respDump err:", err) } _, err = conn.Write(respDump) if err != nil { logger.Println("conn write err:", err) } 总结
源码放在github上了,欢迎各位指导和贡献!
The more you know, the more you know you don't know! 有疑问加站长微信联系(非本文作者)
