阅读目的

弄清楚项目是如何完成代理的功能,因此重点弄懂 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 类型则有两个非常重要的属性,分别是 DirectorModifyResponse ,这两个属性都是函数类型,在接收客户端请求时, ServeHTTP 函数首先调用 Director 函数对接受到的请求体进行修改,例如修改请求的目标地址、请求头等等;然后使用修改后的请求体发起了新的请求,接收到响应之后,调用 ModifyResponse 函数对响应进行修改,最后将修改后的响应体拷贝并响应给客户端,这样就能实现一个简单的反向代理流程

半个总结

该函数整个环节可以归拢为下面几个步骤:

  1. 拷贝上游请求的 Header 到下游请求 - Director
  2. 修改请求(例如协议、参数、URL 等)
  3. 判断是否需要升级协议(Upgrade)
  4. 删除上游请求中的 hop-by-hop Header,即不需要透传到下游的 Header
  5. 设置 X-Forward-For Header,追加当前节点IP
  6. 使用连接池,向下游发起请求 - Transport
  7. 处理协议升级(HttpCode 101)
  8. 删除不需要返回给上游的逐跳 Header
  9. 修改响应体内容(如有需要)
  10. 拷贝下游响应头部到上游响应请求 - ModifyResponse
  11. 返回 HTTP 状态码
  12. 定时刷新内容到 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)
	}
}