阅读目的
弄清楚项目是如何完成代理的功能,因此重点弄懂 Proxy 部分的代码
项目地址
https://github.com/goproxyio/goproxy
从项目启动开始,我们的重点就是下面的这个参数
flag.StringVar(&proxyHost, "proxy", "", "next hop proxy for Go Modules, recommend use https://gopropxy.io")
我们将这个参数设置为国内可以访问的 https://goproxy.cn
,并且尝试一下是否能跑通整个服务,这里使用的是 Powershell,换成 Linux Shell 的时候其实也是一样的流程
# 单独开启一个终端启动服务
$ $CacheDIR = "G:\\GomodDebug"
$ mkdir -p $CacheDIR\pkg\mod\cache\download\github.com\goproxyio\goproxy\@v\
$ go run . -proxy https://goproxy.cn -listen 127.0.0.1:8085 -cacheDir $CacheDIR
goproxy.io: ProxyHost https://goproxy.cn
goproxy.io: ------ --- /github.com/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/@v/list [proxy]
goproxy.io: 0.344s 404 /github.com/@v/list
goproxy.io: 0.548s 200 /github.com/goproxyio/goproxy/@v/list
goproxy.io: 0.619s 404 /github.com/goproxyio/@v/list
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/cfg/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/module/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modfetch/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modfetch/codehost/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modload/@v/list [proxy]
goproxy.io: 0.428s 404 /github.com/goproxyio/goproxy/internal/@v/list
goproxy.io: 0.473s 404 /github.com/goproxyio/goproxy/internal/module/@v/list
goproxy.io: 0.532s 404 /github.com/goproxyio/goproxy/internal/modfetch/@v/list
goproxy.io: 0.555s 404 /github.com/goproxyio/goproxy/internal/modload/@v/list
goproxy.io: 0.592s 404 /github.com/goproxyio/goproxy/internal/cfg/@v/list
goproxy.io: 0.697s 404 /github.com/goproxyio/goproxy/internal/modfetch/codehost/@v/list
# 这是IDE中正常的测试终端
$ go env -w GO111MODULE=on
$ go env -w GOPROXY=http://127.0.0.1:8085
# 我们上面并没有设置direct,意味着拉取源只会通过代理
$ go get -u github.com/goproxyio/goproxy
github.com/goproxyio/goproxy imports
github.com/goproxyio/goproxy/pkg/proxy imports
...
深入研究Proxy的流程
Director
handle = &logger{proxy.NewRouter(proxy.NewServer(new(ops)), &proxy.RouterOptions{
Pattern: excludeHost,
Proxy: proxyHost,
DownloadRoot: downloadRoot,
CacheExpire: cacheExpire,
})}
func NewRouter(srv *Server, opts *RouterOptions) *Router {
rt := &Router{
opts: opts,
srv: srv,
}
if opts != nil {
if opts.Proxy == "" {
log.Printf("not set proxy, all direct.")
return rt
}
remote, err := url.Parse(opts.Proxy)
if err != nil {
log.Printf("parse proxy fail, all direct.")
return rt
}
proxy := httputil.NewSingleHostReverseProxy(remote)
director := proxy.Director
proxy.Director = func(r *http.Request) {
director(r)
r.Host = remote.Host
}
rt.proxy = proxy
rt.proxy.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
rt.proxy.ModifyResponse = rt.customModResponse
...
}
return rt
}
如果没有设置代理或者代理没有通过 url.Parse
的解析 ,服务将会提示将会使用直连模式
proxy := httputil.NewSingleHostReverseProxy(remote)
director := proxy.Director
NewSingleHostReverseProxy
将会返回一个新的 ReverseProxy,把 URLs 请求路由到 targe 的指定的 Scheme/Host/Base Path
而 ReverseProxy
类型则有两个非常重要的属性,分别是 Director
和 ModifyResponse
,这两个属性都是函数类型,在接收客户端请求时, ServeHTTP
函数首先调用 Director 函数对接受到的请求体进行修改,例如修改请求的目标地址、请求头等等;然后使用修改后的请求体发起了新的请求,接收到响应之后,调用 ModifyResponse 函数对响应进行修改,最后将修改后的响应体拷贝并响应给客户端,这样就能实现一个简单的反向代理流程
半个总结
该函数整个环节可以归拢为下面几个步骤:
- 拷贝上游请求的 Header 到下游请求 - Director
- 修改请求(例如协议、参数、URL 等)
- 判断是否需要升级协议(Upgrade)
- 删除上游请求中的 hop-by-hop Header,即不需要透传到下游的 Header
- 设置 X-Forward-For Header,追加当前节点IP
- 使用连接池,向下游发起请求 - Transport
- 处理协议升级(HttpCode 101)
- 删除不需要返回给上游的逐跳 Header
- 修改响应体内容(如有需要)
- 拷贝下游响应头部到上游响应请求 - ModifyResponse
- 返回 HTTP 状态码
- 定时刷新内容到 Response - BufferPool
回到代码部分,这里将我们定义的 Proxy 主机(已经解析出域名)作为 ReverseProxy 的对象,并将传过来的 HTTP 报文目标设置为 Proxy 主机,以防止该报文直接被丢弃,最后在报文从远程目标主机返回的 ModifyResponse 环节使用了 customModResponse
进行修改适配,看这里的 director(r)
,其实是将 Scheme 等以一种简单的方式传递过去,否则会出现错误 http: proxy error: unsupported protocol scheme ""
ModifyResponse
func (router *Router) customModResponse(r *http.Response) error {
var err error
if r.StatusCode == http.StatusOK {
var buf []byte
if strings.Contains(r.Header.Get("Content-Encoding"), "gzip") {
gr, err := gzip.NewReader(r.Body)
if err != nil {
return err
}
defer gr.Close()
buf, err = ioutil.ReadAll(gr)
if err != nil {
return err
}
r.Header.Del("Content-Encoding")
// rewrite content-length header due to the decompressed data will be refilled in the body
r.Header.Set("Content-Length", fmt.Sprint(len(buf)))
} else {
buf, err = ioutil.ReadAll(r.Body)
if err != nil {
return err
}
}
...
// support 302 status code.
if r.StatusCode == http.StatusFound {
loc := r.Header.Get("Location")
if loc == "" {
return fmt.Errorf("%d response missing Location header", r.StatusCode)
}
_, err := url.Parse(loc)
if err != nil {
return fmt.Errorf("failed to parse Location header %q: %v", loc, err)
}
resp, err := http.Get(loc)
if err != nil {
return err
}
defer resp.Body.Close()
var buf []byte
...
resp.Body = ioutil.NopCloser(bytes.NewReader(buf))
...
}
return nil
}
接收到从 Proxy 主机返回的报文后,首先对 StatusCode 进行判断,如果是正常的 StatusOK 当然可以直接进行处理(包括 Gzip 解压数据等),而如果收到了 302 跳转请求,服务这次就不会直接和之前一样转发报文了,而是自己使用 http.Get
取回跳转后的数据,之后就和正常的 200 响应的处理流程一致了
至此,GoProxy 的整个代理环节似乎清晰了起来,那我们根据其思路来写一个属于自己的简易代理服务
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
type Service struct{}
func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var remote *url.URL
if strings.Contains(r.RequestURI, "domain/baidu") {
remote, _ = url.Parse("https://www.baidu.com")
} else if strings.Contains(r.RequestURI, "domain/qq") {
remote, _ = url.Parse("https://www.qq.com")
} else {
fmt.Fprintln(w, fmt.Sprintf(" %s 404 Not Found", r.RequestURI))
return
}
proxy := httputil.NewSingleHostReverseProxy(remote)
proxy.ModifyResponse = s.customModResponse
director := proxy.Director
proxy.Director = func(r *http.Request) {
director(r)
r.URL.Path = remote.Path
r.Host = remote.Host
}
proxy.ServeHTTP(w, r)
}
func (s *Service) customModResponse(r *http.Response) error {
r.Header.Add("Access-Control-Allow-Origin", "*")
var buf []byte
if r.StatusCode == http.StatusOK {
if strings.Contains(
r.Header.Get("Content-Encoding"), "gzip") {
gr, err := gzip.NewReader(r.Body)
if err != nil {
return err
}
defer func(gr *gzip.Reader) {
err := gr.Close()
if err != nil {
log.Println(err)
}
}(gr)
buf, err = ioutil.ReadAll(gr)
if err != nil {
return err
}
r.Header.Del("Content-Encoding")
r.Header.Set("Content-Length", fmt.Sprint(len(buf)))
} else {
var err error
buf, err = ioutil.ReadAll(r.Body)
if err != nil {
return err
}
}
r.Body = ioutil.NopCloser(bytes.NewReader(buf))
} else {
return fmt.Errorf("return result : %d", r.StatusCode)
}
return nil
}
func main() {
s := &Service{}
err := http.ListenAndServe(":9095", s)
if err != nil {
log.Fatalln(err)
}
}