這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
最近因為換了Mac,以前的Linux基本上不再使用了,但是我的SS代理還得用。SS代理大家都瞭解,一個很NB的Socket代理工具,但是就是因為他是Socket的,想用HTTP代理的時候很不方便。
以前在Linux下的時候,會安裝一個Privoxy把Socket代理轉換為HTTP代理,開機啟動,也比較方便。但是Mac下使用Brew安裝的Privoxy就很難用,再加上以前一個有個想法,一個軟體搞定Socket和HTTP代理,這樣就不用安裝一個單獨的軟體做轉換了。
想著就開始做吧,以前基本上沒有搞過太多的網路編程,最近也正好在研究Go,正好練練手。
我們這裡主要講使用HTTP/1.1協議中的CONNECT方法建立起來的隧道串連,實現的HTTP Proxy。這種代理的好處就是不用知道用戶端請求的資料,只需要原封不動的轉寄就可以了,對於處理HTTPS的請求就非常方便了,不用解析他的內容,就可以實現代理。
啟動代理監聽
要想做一個HTTP Proxy,我們需要啟動一個伺服器,監聽一個連接埠,用於接收用戶端的請求。Golang給我們提供了強大的net包供我們使用,我們啟動一個Proxy 伺服器監聽非常方便。
1234 |
l, err := net.Listen("tcp", ":8080")if err != nil {log.Panic(err)} |
以上代理我們就實現了一個在8080連接埠上監聽的伺服器,我們這裡沒有寫ip地址,預設在所有ip地址上進行監聽。如果你只想本機適用,可以使用127.0.0.1:8080,這樣機器就訪問不了你的Proxy 伺服器了。
監聽接收代理請求
啟動了Proxy 伺服器,就可以開始接受不了代理請求了,有了請求,我們才能做進一步的處理。
12345678 |
for {client, err := l.Accept()if err != nil {log.Panic(err)}go handleClientRequest(client)} |
Listener介面的Accept方法,會接受用戶端發來的串連資料,這是一個阻塞型的方法,如果用戶端沒有串連資料發來,他就是阻塞等待。接收來的串連資料,會馬上交給handleClientRequest方法進行處理,這裡使用一個go關鍵字開一個goroutine的目的是不阻塞用戶端的接收,Proxy 伺服器可以馬上接收下一個串連請求。
解析請求,擷取要訪問的IP和連接埠
有了用戶端的代理請求了,我們還得從請求裡提取用戶端要訪問的遠程主機的IP和連接埠,這樣我們的Proxy 伺服器才可以建立和遠程主機的串連,代理轉寄。
HTTP協議的頭資訊裡就包含有我們需要的主機名稱(IP)和連接埠資訊,並且是明文的,協議很規範,類似於:
1234 |
CONNECT www.google.com:443 HTTP/1.1Host: www.google.com:443Proxy-Connection: keep-aliveUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36 |
可以看到我們需要的在第一行,第一個行的資訊以空格分開,第一部分CONNECT是要求方法,這裡是CONNECT,除此之外還有GET,POST等,都是HTTP協議的標準方法。
第二部分是URL,https的請求只有host和port,http的請求是一個完成的url,等下會看個範例,就明白了。
第三部是HTTP的協議和版本,這個我們不用太關注。
以上是一個https的請求,我們看下http的:
12345 |
GET http://www.flysnow.org/ HTTP/1.1Host: www.flysnow.orgProxy-Connection: keep-aliveUpgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36 |
可以看到htt的,沒有連接埠號碼(預設是80);比https多了schame–http://。
有了分析,下面我們就可以從HTTP頭資訊中擷取請求的url和method資訊了。
12345678910111213 |
var b [1024]byten, err := client.Read(b[:])if err != nil {log.Println(err)return}var method, host, address stringfmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &host)hostPortURL, err := url.Parse(host)if err != nil {log.Println(err)return} |
然後需要進一步對url進行解析,擷取我們需要的遠程伺服器資訊
123456789 |
if hostPortURL.Opaque == "443" { //https訪問address = hostPortURL.Scheme + ":443"} else { //http訪問if strings.Index(hostPortURL.Host, ":") == -1 { //host不帶連接埠, 預設80address = hostPortURL.Host + ":80"} else {address = hostPortURL.Host}} |
這樣就完整了擷取了要請求伺服器的資訊,他們可能是以下幾種格式
123 |
ip:porthostname:portdomainname:port |
就是有可能是ip(v4orv6),有可能是主機名稱(內網),有可能是網域名稱(dns解析)
Proxy 伺服器和遠程伺服器建立串連
有了遠程伺服器的資訊了,就可以進行撥號建立串連了,有了串連,才可以通訊。
123456 |
//獲得了請求的host和port,就開始撥號吧server, err := net.Dial("tcp", address)if err != nil {log.Println(err)return} |
資料轉寄
撥號成功後,就可以進行資料代理傳輸了
12345678 |
if method == "CONNECT" {fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n")} else {server.Write(b[:n])}//進行轉寄go io.Copy(server, client)io.Copy(client, server) |
其中對CONNECT方法有單獨的回應,用戶端說要建立串連,Proxy 伺服器要回應建立好了,然後才可以像HTTP一樣請求訪問。
運行外國外VPS上
到這裡,我們的Proxy 伺服器全部開發完成了,下面是完整的原始碼:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374 |
package mainimport ("bytes""fmt""io""log""net""net/url""strings")func main() {log.SetFlags(log.LstdFlags|log.Lshortfile)l, err := net.Listen("tcp", ":8081")if err != nil {log.Panic(err)}for {client, err := l.Accept()if err != nil {log.Panic(err)}go handleClientRequest(client)}}func handleClientRequest(client net.Conn) {if client == nil {return}defer client.Close()var b [1024]byten, err := client.Read(b[:])if err != nil {log.Println(err)return}var method, host, address stringfmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &host)hostPortURL, err := url.Parse(host)if err != nil {log.Println(err)return}if hostPortURL.Opaque == "443" { //https訪問address = hostPortURL.Scheme + ":443"} else { //http訪問if strings.Index(hostPortURL.Host, ":") == -1 { //host不帶連接埠, 預設80address = hostPortURL.Host + ":80"} else {address = hostPortURL.Host}}//獲得了請求的host和port,就開始撥號吧server, err := net.Dial("tcp", address)if err != nil {log.Println(err)return}if method == "CONNECT" {fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n")} else {server.Write(b[:n])}//進行轉寄go io.Copy(server, client)io.Copy(client, server)} |
把原始碼編譯,然後放到你國外的VPS上,在自己機器上配置好HTTP代理,就可以到處訪問,自由自在了。