客服电话
17728153743之前我们介绍了HTTP普通代理的原理和golang简单实现,也提到过普通代理的局限,比如无法代理HTTPS的报文,此外普通代理也只能代理HTTP协议报文,无法支持其他协议的数据转发。当然有问题就有解决方案,对于这些问题HTTP可以通过隧道(tunnel)代理来解决。
隧道代理的原因也可以用一句话来总结:
代理服务器和真正的服务器之间建立起TCP连接,然后在客户端和真正服务器端进行数据的直接转发。
《HTTP权威指南》对隧道描述的原话是:
The CONNECT method asks a tunnel gateway to create a TCP connection to an arbitrary destination server and port and to blindly relay subsequent data between client and server.
意思和我之前的那句话一样,只不过有了更多的细节说明。
下图是《HTTP权威指南》书中的插图,它讲解了客户端通过隧道代理连接HTTPS服务器的过程。
(a)客户端先发送CONNECT请求到隧道代理服务器,告诉它建立和服务器的TCP连接(因为是TCP连接,只需要ip和端口就行,不需要关注上层的协议类型)
(b,c)代理服务器成功和后端服务器建立TCP连接
(d)代理服务器返回HTTP 200 Connection Established报文,告诉客户端连接已经成功建立
(e)这个时候就建立起了连接,所有发给代理的TCP报文都会直接转发,从而实现服务器和客户端的通信
CONNECT请求
CONNECT请求的内容和其他HTTP方法的语法一样,只不过它在状态栏(status line)指定了真正服务器的地址。请求URI替换成了hostname和port的字符串,比如:
CONNECT realserver.com:443 HTTP/1.0
User-Agent:GoProxy
而其他HTTP请求的状态栏对应位置是路径地址,比如:
GET/about HTTP/1.0
User-Agent:GoProxy
知道了hostname和port,代理服务器就能正确地建立,才能够继续后面的访问。需要注意的是,客户端应该尽量少地暴露其他信息,最好只有状态栏一行的内容,因为CONNECT请求是没有经过加密的。如果想通过这种方式进行HTTPS安全访问,那么不要在CONNECT请求中暴露敏感数据(比如cookie)是必须的。
如果代理服务器正确接受了CONNECT请求,并且成功建立了和后端服务器的TCP连接,它应该返回200状态码的应答,按照大多数的约定为200 Connection Establised。应答也不需要包含其他的头部和body,因为后续的数据传输都是直接转发的,代理不会分析其中的内容。
代码实现
有了上面的理论分析,我们可以写出下面的代码:
package main
import(
"fmt"
"io"
"net"
"net/http"
)
type Pxy struct{}
func NewProxy()*Pxy{
return&Pxy{}
}
//ServeHTTP is the main handler for all requests.
func(p*Pxy)ServeHTTP(rw http.ResponseWriter,req*http.Request){
fmt.Printf("Received request%s%s%sn",
req.Method,
req.Host,
req.RemoteAddr,
)
if req.Method!="CONNECT"{
rw.WriteHeader(http.StatusMethodNotAllowed)
rw.Write([]byte("This is a http tunnel proxy,only CONNECT method is allowed."))
return
}
//Step 1
host:=req.URL.Host
hij,ok:=rw.(http.Hijacker)
if!ok{
panic("HTTP Server does not support hijacking")
}
client,_,err:=hij.Hijack()
if err!=nil{
return
}
//Step 2
server,err:=net.Dial("tcp",host)
if err!=nil{
return
}
client.Write([]byte("HTTP/1.0 200 Connection Establishedrnrn"))
//Step 3
go io.Copy(server,client)
io.Copy(client,server)
}
func main(){
proxy:=NewProxy()
http.ListenAndServe("0.0.0.0:8080",proxy
}
这段代码和前一篇文章的大致框架相同,但是处理的逻辑有些许的区别。
首先接收到客户端的请求之后,代理服务器需要获得底层的TCP连接,这样才能转发数据,所以这里看到了Hijacker类型转换和Hijack()调用,它们最终的目的是拿到客户端的TCP连接(net.TCPConn)
然后代理服务器调用net.Dial函数和真正的服务器端建立TCP连接,并在成功后返回给客户端200应答
最后就是在客户端和服务器端直接转发数据
NOTE:默认的ServeMux不支持CONNECT方法的请求,请直接使用自己编写的Proxy作为Mux。
稍加修改我们就能实现同时支持两种代理的代码,我已经把最终的代码放到了github上面。