Golang 動手寫一個 Http Proxy

【導讀】本文介紹了使用 golang 實現簡單 http proxy 的原理和具體實現細節。

本文主要使用 Golang 實現一個可用但不夠標準,支持 basic authentication 的 http 代理服務。

爲何說不夠標準,在 HTTP/1.1 RFC 中,有些關於代理實現標準的條目在本文中不考慮。

Http Proxy 是如何代理我們的請求

Http 請求的代理如下圖,Http Proxy 只需要將接收到的請求轉發給服務器,然後把服務器的響應,轉發給客戶端即可。

img

Https 請求的代理如下圖,客戶端首先需要發送一個 Http CONNECT 請求到 Http Proxy,Http Proxy 建立一條 TCP 連接到指定的服務器,然後響應 200 告訴客戶端連接建立完成,之後客戶端就可以與服務器進行 SSL 握手和傳輸加密的 Http 數據了。

img

爲何需要 CONNECT 請求?因爲 Http Proxy 不是真正的服務器,沒有 www.foo.com 的證書,不可能以 www.foo.com 的身份與客戶端完成 SSL 握手從而建立 Https 連接。所以需要通過 CONNECT 請求告訴 Http Proxy,讓 Http Proxy 與服務器先建立好 TCP 連接,之後客戶端就可以將 SSL 握手消息發送給 Http Proxy,再由 Http Proxy 轉發給服務器,完成 SSL 握手,並開始傳輸加密的 Http 數據。

Basic Authentication

爲了保護 Http Proxy 不被未授權的客戶端使用,可以要求客戶端帶上認證信息。這裏以 Basic Authentication 爲例。

客戶端在與 Http Proxy 建立連接時,Http 請求頭中需要帶上:

Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l

如果服務端驗證通過,則正常建立連接,否則響應:

HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="*"

所需要開發的功能模塊

  1. 連接處理

  2. 從客戶端請求中獲取服務器連接信息

  3. 基本認證

  4. 請求轉發

連接處理

需要開發一個 TCP 服務器,因爲 HTTP 服務器沒法實現 Https 請求的代理。

Server 的定義:

type Server struct {
 listener   net.Listener
 addr       string
 credential string
}

通過 Start 方法啓動服務,爲每個客戶端連接創建 goroutine 爲其服務:

// Start a proxy server
func (s *Server) Start() {
 var err error
 s.listener, err = net.Listen("tcp", s.addr)
 if err != nil {
  servLogger.Fatal(err)
 }

    if s.credential != "" {
        servLogger.Infof("use %s for auth\n", s.credential)
    }
 servLogger.Infof("proxy listen in %s, waiting for connection...\n", s.addr)

 for {
  conn, err := s.listener.Accept()
  if err != nil {
   servLogger.Error(err)
   continue
  }
  go s.newConn(conn).serve()
 }
}
從客戶端請求中獲取服務器連接信息

對於 http 請求頭的解析,參考了 golang 內置的 http server。

getTunnelInfo 用於獲取:

  1. 請求頭

  2. 服務器地址

  3. 認證信息

  4. 是否 https 請求

// getClientInfo parse client request header to get some information:
func (c *conn) getTunnelInfo() (rawReqHeader bytes.Buffer, host, credential string, isHttps bool, err error) {
 tp := textproto.NewReader(c.brc)

 // First line: GET /index.html HTTP/1.0
 var requestLine string
 if requestLine, err = tp.ReadLine(); err != nil {
  return
 }

 method, requestURI, _, ok := parseRequestLine(requestLine)
 if !ok {
  err = &BadRequestError{"malformed HTTP request"}
  return
 }

 // https request
 if method == "CONNECT" {
  isHttps = true
  requestURI = "http://" + requestURI
 }

 // get remote host
 uriInfo, err := url.ParseRequestURI(requestURI)
 if err != nil {
  return
 }

 // Subsequent lines: Key: value.
 mimeHeader, err := tp.ReadMIMEHeader()
 if err != nil {
  return
 }

 credential = mimeHeader.Get("Proxy-Authorization")

 if uriInfo.Host == "" {
  host = mimeHeader.Get("Host")
 } else {
  if strings.Index(uriInfo.Host, ":") == -1 {
   host = uriInfo.Host + ":80"
  } else {
   host = uriInfo.Host
  }
 }

 // rebuild http request header
 rawReqHeader.WriteString(requestLine + "\r\n")
 for k, vs := range mimeHeader {
  for _, v := range vs {
   rawReqHeader.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
  }
 }
 rawReqHeader.WriteString("\r\n")
 return
}
基本認證
// validateCredentials parse "Basic basic-credentials" and validate it
func (s *Server) validateCredential(basicCredential string) bool {
 c := strings.Split(basicCredential, " ")
 if len(c) == 2 && strings.EqualFold(c[0]"Basic") && c[1] == s.credential {
  return true
 }
 return false
}
請求轉發

serve 方法會進行 Basic Authentication 驗證,對於 http 請求的代理,會把請求頭轉發給服務器,對於 https 請求的代理,則會響應 200 給客戶端。

// serve tunnel the client connection to remote host
func (c *conn) serve() {
    defer c.rwc.Close()
 rawHttpRequestHeader, remote, credential, isHttps, err := c.getTunnelInfo()
 if err != nil {
  connLogger.Error(err)
  return
 }

 if c.auth(credential) == false {
  connLogger.Error("Auth fail: " + credential)
  return
 }

 connLogger.Info("connecting to " + remote)
 remoteConn, err := net.Dial("tcp", remote)
 if err != nil {
  connLogger.Error(err)
  return
 }

 if isHttps {
  // if https, should sent 200 to client
  _, err = c.rwc.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
  if err != nil {
   glog.Errorln(err)
   return
  }
 } else {
  // if not https, should sent the request header to remote
  _, err = rawHttpRequestHeader.WriteTo(remoteConn)
  if err != nil {
   connLogger.Error(err)
   return
  }
 }

 // build bidirectional-streams
 connLogger.Info("begin tunnel", c.rwc.RemoteAddr()"<->", remote)
 c.tunnel(remoteConn)
    connLogger.Info("stop tunnel", c.rwc.RemoteAddr()"<->", remote)
}

完整代碼可查看:https://github.com/yangxikun/gsproxy

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/x5dgC4It__Crt9x9uCX3Tg