Web开发中可采用HTTP中的GET、POST、HEAD、CONNECT等方式发送请求等待响应来操作指定的资源。
HTTP请求方法 | 描述 |
---|---|
GET | 向指定资源请求数据 |
POST | 向指定资源提交要被处理的数据 |
PUT | 上传指定的URI表示 |
DELETE | 移除指定的资源,请求服务器删除Request-URI所标识的资源。 |
HEAD | 与GET相同但仅返回HTTP报头不返回主体 |
CONNECT | 将连接改为管道方式的代理服务器,用于SSL加密服务器的连接。 |
OPTIONS | 使服务器传回资源所支持的所有HTTP请求方法,用于测试服务器功能是否正常运作。 |
TRACE | 回显服务器接收的请求,用于测试或诊断。 |
HTTP AGENT
HTTP代理存在两种形式
第一种:普通代理:RFC 7230 - HTTP/1.1: Message Syntax and Routing
修订后的RFC 2616, HTTP/1.1协议的第一部分描述的普通代理
普通代理扮演着中间人的角色,对于连接到它的客户端来说它是服务端,对于要连接的服务端来说它是客户端,它负责在两端之间来回传送HTTP报文。
普通代理原理:HTTP客户端向代理发送请求报文,代理服务器需要正确地处理请求和连接,同时向服务器发送请求,并将接收到的响应转发给客户端。
对于目标服务器来说,会将代理作为客户端,也就完全觉察不到真正客户端的存在,这实现了隐藏客户端IP的目的。代理也可以修改HTTP请求头,通过X-Forwarded-IP
自定义头部字段告知目标服务器真正的客户端。但目标服务器是无法验证自定义头部是由代理添加的还是由客户端添加的,所以从HTTP头部字段获取IP时需格外小心。
正向代理
给浏览器显式地指定代理时需手动修改浏览器或操作系统相关配置,或指明PAC(Proxy Auto-Configuration,自动配置代理)文件自动设置。某些浏览器支持WPAD(Web Proxy Autodiscovery Protocol,Web代理自动发现协议)。显式地指定浏览器代理这种方式一般称为正向代理,浏览器启用正向代理后会对HTTP请求报文做一些修改,来规避老旧代理服务器的一些问题。
反向代理
另外一种情况是访问目标服务器时实际上访问的是代理,代理接收到请求报文后,再向真正提供服务的服务器发起请求,并将响应转发给浏览器,这种情况称为反向代理。反向代理可隐藏服务器IP与端口。使用反向代理需通过修改DNS让域名解析到代理服务器IP,此时浏览器是无法察觉到真正服务器的存在。
第二种:隧道代理:Tunneling TCP based protocols through Web proxy servers
通过Web代理服务器使用隧道方式传输基于TCP的协议
隧道代理通过HTTP协议正文部分(Body)完成通讯,以HTTP的方式实现任意基于TCP的应用层协议的代理。
隧道代理使用HTTP的CONNECT方法建立连接,但CONNECT最开始并不是RFC 2616 - HTTP/1.1的一部分,直到2014年发布的HTTP/1.1修订版中,才增加了对CONNECT以及隧道技术的描述。
隧道代理原理:HTTP客户端通过CONNECT方法请求隧道代理创建一条到达任意目标服务器和端口的TCP连接,并对客户端和服务端之间的后续数据进行盲转发。
CONNECT
为确保数据通信安全,浏览器与服务器之间的HTTPS通信是加密的。当浏览器需要通过代理服务器发送HTTPS请求时,由于请求的站点地址和段都都是加密保存于HTTPS请求头中的,代理服务器是如何既确保通信是加密的(代理服务器自身无法读取通信内容)又知道向哪里发送请求呢?
为了解决这个问题,浏览器需先通过明文HTTP形式向代理服务器发送一个CONNECT请求告诉它目标站点的地址和端口。
对于CONNECT连接来说,只是用来让代理创建TCP连接,所以只需要提供服务器域名和端口即可,并不需要具体的资源路径。
浏览器建立到服务器TCP连接产生的HTTP往返完全是明文的,这也是为什么CONNECT请求只需要提供IP和端口。若发送完整的URL、Cookie等信息会被中间人一览无余,降低了HTTPS的安全性。
CONNECT www.microsoft.com:443 HTTP/1.0
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Host: www.microsoft.com
Content-Length: 0
DNT: 1
Connection: Keep-Alive
Pragma: no-cache
当代理服务器接收到这个请求后,会在对应的端口上与目标站点建立一个TCP连接,连接建立成功后返回一个HTTP 200状态码告诉浏览器与该站点的加密通道已经建立完成。
浏览器接收到响应报文后,即可认为到服务端的TCP连接已经打通,后续直接往这个TCP连接写协议数据即可。
HTTP/1.0 200 Connection Established
FiddlerGateway: Direct
StartTime: 11:56:22.008
Connection: close
EndTime: 11:56:22.538
ClientToServerBytes: 1416
ServerToClientBytes: 1358
接下来代理服务器仅仅是来回传送浏览器与目标服务器之间的加密数据包,代理服务器并不需要解析加密内容以保证HTTPS的安全性。
使用注意
- HTTP代理协议只有当浏览器配置为使用代理服务器时才会使用到CONNECT方法
- HTTP CONNECT METHOD的作用是将服务器作为代理,让服务器代理用户去访问网页,之后将数据返回给用户。
- HTTP CONNECT METHOD是通过TCP连接代理服务器
CONNECT的作用是把服务器作为跳板,让服务器代替用户取访问其它URL,之后把数据原原本本的返回给用户,这样用户就可以访问一些只有服务器上才能访问的站点,这也就是HTTP代理。
GET vs CONNECT
CONNECT 与 GET不同之处在于:代理服务器对CONNECT连接处理上,它会为其建立一个到目标服务器的连接,而不把CONNECT请求发送出去,建立连接以后代理服务器不会对连接数据做任何修改,只是转发(通常使用的是SSL的443端口),代理服务器可在80端口通知支持GET和CONNECT。
代理服务器如何处理GET呢?
代理服务器会分析出目标服务器地址后建立连接,然后修改GET请求为直接发往目标服务器的格式,比如会删掉只是用来提供给代理的部分,以降低HTTP版本为代理服务器所能支持的版本,比如会降低HTTP/1.1为HTTP/1.0。
实现
使用Golang实现支持HTTP CONNECT方法的隧道代理服务器
$ vim tunnel/main.go
package main
import (
"io"
"log"
"net"
"net/http"
"sync"
)
var (
addr = "127.0.0.1:7100"
username = "administrator"
password = "1234567"
)
//tunnel 通道处理
func tunnel(w http.ResponseWriter, r *http.Request){
//判断请求方法
if r.Method != http.MethodConnect{
log.Println(r.Method, r.RequestURI)
http.NotFound(w, r)//404
return
}
//获取用户名与密码
auth := r.Header.Get("Proxy-Authorization")//获取客户端授权信息
//设置用户名与密码
r.Header.Set("Authorization", auth)
//验证账户密码
u,p,ok := r.BasicAuth()//BasicAuth依赖Authorization
if !ok || !(username==u || password==p){
log.Printf("bad credential: username %s or password %s\n", u, p)
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
//获取目标服务器地址
dstAddr := r.RequestURI
//连接远程服务器
dstConn,err := net.Dial("tcp", dstAddr)
if err!=nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer dstConn.Close()
//为客户端返回成功消息
w.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
//劫持writer获取潜在conn
//HTTP是应用层协议,下层TCP是网络层协议,hijack可从HTTP Response获取TCP连接,若是HTTPS服务器则是TLS连接。
//bio是带缓冲的读写者
srcConn,bio,err := w.(http.Hijacker).Hijack()
if err!= nil{
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer srcConn.Close()
//创建两个线程
wg := &sync.WaitGroup{}
wg.Add(2)
//并发执行单元1: 将TCP连接拷贝到HTTP连接中
go func(){
defer wg.Done()
//缓存处理
n := bio.Reader.Buffered()
if n>0 {
n64,err := io.CopyN(dstConn, bio, int64(n))
if n64!=int64(n) || err!=nil{
log.Printf("io.CopyN: %d %v\n", n64, err)
return
}
}
//进行全双工的双向数据拷贝(中继)
io.Copy(dstConn, srcConn)//relay: src->dst
}()
//并发执行单元2:将HTTP连接拷贝到TCP连接中
go func(){
defer wg.Done()
//进行全双工的双向数据拷贝(中继)
io.Copy(srcConn, dstConn)//relay:dst->src
}()
wg.Wait()
}
//服务器 go run main.go
//客户端 curl -p --proxy username:password@hostname:port http://target.com
//curl -p --proxy administartor:[email protected]:7100 http://www.baidu.com
func main(){
//HTTP处理器
handler := http.HandlerFunc(tunnel)
//建立HTTP服务器
err := http.ListenAndServe(addr, handler)
if err!=nil{
panic(err)
}
}
运行服务器
$ go run main.go
测试
$ curl -p --proxy administartor:[email protected]:7100 http://www.baidu.com
cURL在目标HTTP而非HTTPS时会采用GET请求,为保证数据安全、防监听、插入广告,在服务器上应使用HTTPS,使用ListenAndServeTLS
替换ListenAndServe
即可。