【导读】本文介绍了使用 golang 实现简单 http proxy 的原理和具体实现细节。
本文主要使用 Golang 实现一个可用但不够标准,支持 basic authentication 的 http 代理服务。
为何说不够标准,在 HTTP/1.1 RFC 中,有些关于代理实现标准的条目在本文中不考虑。
Http 请求的代理如下图,Http Proxy 只需要将接收到的请求转发给服务器,然后把服务器的响应,转发给客户端即可。
Https 请求的代理如下图,客户端首先需要发送一个 Http CONNECT 请求到 Http Proxy,Http Proxy 建立一条 TCP 连接到指定的服务器,然后响应 200 告诉客户端连接建立完成,之后客户端就可以与服务器进行 SSL 握手和传输加密的 Http 数据了。
为何需要 CONNECT 请求?因为 Http Proxy 不是真正的服务器,没有 www.foo.com 的证书,不可能以 www.foo.com 的身份与客户端完成 SSL 握手从而建立 Https 连接。所以需要通过 CONNECT 请求告诉 Http Proxy,让 Http Proxy 与服务器先建立好 TCP 连接,之后客户端就可以将 SSL 握手消息发送给 Http Proxy,再由 Http Proxy 转发给服务器,完成 SSL 握手,并开始传输加密的 Http 数据。
为了保护 Http Proxy 不被未授权的客户端使用,可以要求客户端带上认证信息。这里以 Basic Authentication 为例。
客户端在与 Http Proxy 建立连接时,Http 请求头中需要带上:
Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
如果服务端验证通过,则正常建立连接,否则响应:
HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="*"
需要开发一个 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 用于获取:
// 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
转自:
yangxikun.github.io/http/2017/09/16/http-proxy.html
- EOF -
Go 开发大全
参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。
关注后获取
回复 Go 获取6万star的Go资源库
分享、点赞和在看
支持我们分享更多好文章,谢谢!