Web開發(fā)中可采用HTTP中的GET、POST、HEAD、CONNECT等方式發(fā)送請求等待響應來操作指定的資源。
| HTTP請求方法 | 描述 |
|---|---|
| GET | 向指定資源請求數(shù)據(jù) |
| POST | 向指定資源提交要被處理的數(shù)據(jù) |
| 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協(xié)議的第一部分描述的普通代理
普通代理扮演著中間人的角色,對于連接到它的客戶端來說它是服務端,對于要連接的服務端來說它是客戶端,它負責在兩端之間來回傳送HTTP報文。
普通代理原理:HTTP客戶端向代理發(fā)送請求報文,代理服務器需要正確地處理請求和連接,同時向服務器發(fā)送請求,并將接收到的響應轉(zhuǎn)發(fā)給客戶端。

對于目標服務器來說,會將代理作為客戶端,也就完全覺察不到真正客戶端的存在,這實現(xiàn)了隱藏客戶端IP的目的。代理也可以修改HTTP請求頭,通過X-Forwarded-IP自定義頭部字段告知目標服務器真正的客戶端。但目標服務器是無法驗證自定義頭部是由代理添加的還是由客戶端添加的,所以從HTTP頭部字段獲取IP時需格外小心。
正向代理
給瀏覽器顯式地指定代理時需手動修改瀏覽器或操作系統(tǒng)相關(guān)配置,或指明PAC(Proxy Auto-Configuration,自動配置代理)文件自動設置。某些瀏覽器支持WPAD(Web Proxy Autodiscovery Protocol,Web代理自動發(fā)現(xiàn)協(xié)議)。顯式地指定瀏覽器代理這種方式一般稱為正向代理,瀏覽器啟用正向代理后會對HTTP請求報文做一些修改,來規(guī)避老舊代理服務器的一些問題。
反向代理
另外一種情況是訪問目標服務器時實際上訪問的是代理,代理接收到請求報文后,再向真正提供服務的服務器發(fā)起請求,并將響應轉(zhuǎn)發(fā)給瀏覽器,這種情況稱為反向代理。反向代理可隱藏服務器IP與端口。使用反向代理需通過修改DNS讓域名解析到代理服務器IP,此時瀏覽器是無法察覺到真正服務器的存在。
第二種:隧道代理:Tunneling TCP based protocols through Web proxy servers
通過Web代理服務器使用隧道方式傳輸基于TCP的協(xié)議
隧道代理通過HTTP協(xié)議正文部分(Body)完成通訊,以HTTP的方式實現(xiàn)任意基于TCP的應用層協(xié)議的代理。
隧道代理使用HTTP的CONNECT方法建立連接,但CONNECT最開始并不是RFC 2616 - HTTP/1.1的一部分,直到2014年發(fā)布的HTTP/1.1修訂版中,才增加了對CONNECT以及隧道技術(shù)的描述。
隧道代理原理:HTTP客戶端通過CONNECT方法請求隧道代理創(chuàng)建一條到達任意目標服務器和端口的TCP連接,并對客戶端和服務端之間的后續(xù)數(shù)據(jù)進行盲轉(zhuǎn)發(fā)。

CONNECT
為確保數(shù)據(jù)通信安全,瀏覽器與服務器之間的HTTPS通信是加密的。當瀏覽器需要通過代理服務器發(fā)送HTTPS請求時,由于請求的站點地址和段都都是加密保存于HTTPS請求頭中的,代理服務器是如何既確保通信是加密的(代理服務器自身無法讀取通信內(nèi)容)又知道向哪里發(fā)送請求呢?

為了解決這個問題,瀏覽器需先通過明文HTTP形式向代理服務器發(fā)送一個CONNECT請求告訴它目標站點的地址和端口。
對于CONNECT連接來說,只是用來讓代理創(chuàng)建TCP連接,所以只需要提供服務器域名和端口即可,并不需要具體的資源路徑。
瀏覽器建立到服務器TCP連接產(chǎn)生的HTTP往返完全是明文的,這也是為什么CONNECT請求只需要提供IP和端口。若發(fā)送完整的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狀態(tài)碼告訴瀏覽器與該站點的加密通道已經(jīng)建立完成。
瀏覽器接收到響應報文后,即可認為到服務端的TCP連接已經(jīng)打通,后續(xù)直接往這個TCP連接寫協(xié)議數(shù)據(jù)即可。
HTTP/1.0 200 Connection Established
FiddlerGateway: Direct
StartTime: 11:56:22.008
Connection: close
EndTime: 11:56:22.538
ClientToServerBytes: 1416
ServerToClientBytes: 1358
接下來代理服務器僅僅是來回傳送瀏覽器與目標服務器之間的加密數(shù)據(jù)包,代理服務器并不需要解析加密內(nèi)容以保證HTTPS的安全性。
使用注意
- HTTP代理協(xié)議只有當瀏覽器配置為使用代理服務器時才會使用到CONNECT方法
- HTTP CONNECT METHOD的作用是將服務器作為代理,讓服務器代理用戶去訪問網(wǎng)頁,之后將數(shù)據(jù)返回給用戶。
- HTTP CONNECT METHOD是通過TCP連接代理服務器
CONNECT的作用是把服務器作為跳板,讓服務器代替用戶取訪問其它URL,之后把數(shù)據(jù)原原本本的返回給用戶,這樣用戶就可以訪問一些只有服務器上才能訪問的站點,這也就是HTTP代理。
GET vs CONNECT
CONNECT 與 GET不同之處在于:代理服務器對CONNECT連接處理上,它會為其建立一個到目標服務器的連接,而不把CONNECT請求發(fā)送出去,建立連接以后代理服務器不會對連接數(shù)據(jù)做任何修改,只是轉(zhuǎn)發(fā)(通常使用的是SSL的443端口),代理服務器可在80端口通知支持GET和CONNECT。
代理服務器如何處理GET呢?
代理服務器會分析出目標服務器地址后建立連接,然后修改GET請求為直接發(fā)往目標服務器的格式,比如會刪掉只是用來提供給代理的部分,以降低HTTP版本為代理服務器所能支持的版本,比如會降低HTTP/1.1為HTTP/1.0。
實現(xiàn)
使用Golang實現(xiàn)支持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")//獲取客戶端授權(quán)信息
//設置用戶名與密碼
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是應用層協(xié)議,下層TCP是網(wǎng)絡層協(xié)議,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()
//創(chuàng)建兩個線程
wg := &sync.WaitGroup{}
wg.Add(2)
//并發(fā)執(zhí)行單元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
}
}
//進行全雙工的雙向數(shù)據(jù)拷貝(中繼)
io.Copy(dstConn, srcConn)//relay: src->dst
}()
//并發(fā)執(zhí)行單元2:將HTTP連接拷貝到TCP連接中
go func(){
defer wg.Done()
//進行全雙工的雙向數(shù)據(jù)拷貝(中繼)
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:1234567@127.0.0.1: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:1234567@127.0.0.1:7100 http://www.baidu.com
cURL在目標HTTP而非HTTPS時會采用GET請求,為保證數(shù)據(jù)安全、防監(jiān)聽、插入廣告,在服務器上應使用HTTPS,使用ListenAndServeTLS替換ListenAndServe即可。