一個簡單的 Golang 實現的 HTTP Proxy
最近因為換了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包供我們使用,我們啟動一個代理服務器監聽非常方便。
l, err := net.Listen("tcp", ":8080")
if err != nil {
log.Panic(err)
}
以上代理我們就實現了一個在8080端口上監聽的服務器,我們這里沒有寫ip地址,默認在所有ip地址上進行監聽。如果你只想本機適用,可以使用127.0.0.1:8080,這樣機器就訪問不了你的代理服務器了。
監聽接收代理請求
啟動了代理服務器,就可以開始接受不了代理請求了,有了請求,我們才能做進一步的處理。
for {
client, err := l.Accept()
if err != nil {
log.Panic(err)
}
go handleClientRequest(client)
}
Listener接口的Accept方法,會接受客戶端發來的連接數據,這是一個阻塞型的方法,如果客戶端沒有連接數據發來,他就是阻塞等待。接收來的連接數據,會馬上交給handleClientRequest方法進行處理,這里使用一個go關鍵字開一個goroutine的目的是不阻塞客戶端的接收,代理服務器可以馬上接收下一個連接請求。
解析請求,獲取要訪問的IP和端口
有了客戶端的代理請求了,我們還得從請求里提取客戶端要訪問的遠程主機的IP和端口,這樣我們的代理服務器才可以建立和遠程主機的連接,代理轉發。
HTTP協議的頭信息里就包含有我們需要的主機名(IP)和端口信息,并且是明文的,協議很規范,類似于:
CONNECT www.google.com:443 HTTP/1.1
Host: www.google.com:443
Proxy-Connection: keep-alive
User-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的:
GET http://www.flysnow.org/ HTTP/1.1
Host: www.flysnow.org
Proxy-Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-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信息了。
var b [1024]byte
n, err := client.Read(b[:])
if err != nil {
log.Println(err)
return
}
var method, host, address string
fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &host)
hostPortURL, err := url.Parse(host)
if err != nil {
log.Println(err)
return
}
然后需要進一步對url進行解析,獲取我們需要的遠程服務器信息
if hostPortURL.Opaque == "443" { //https訪問
address = hostPortURL.Scheme + ":443"
} else { //http訪問
if strings.Index(hostPortURL.Host, ":") == -1 { //host不帶端口, 默認80
address = hostPortURL.Host + ":80"
} else {
address = hostPortURL.Host
}
}
這樣就完整了獲取了要請求服務器的信息,他們可能是以下幾種格式
ip:port
hostname:port
domainname:port
就是有可能是ip(v4orv6),有可能是主機名(內網),有可能是域名(dns解析)
代理服務器和遠程服務器建立連接
有了遠程服務器的信息了,就可以進行撥號建立連接了,有了連接,才可以通信。
//獲得了請求的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")
} else {
server.Write(b[:n])
}
//進行轉發
go io.Copy(server, client)
io.Copy(client, server)
其中對CONNECT方法有單獨的回應,客戶端說要建立連接,代理服務器要回應建立好了,然后才可以像HTTP一樣請求訪問。
運行外國外VPS上
到這里,我們的代理服務器全部開發完成了,下面是完整的源代碼:
package main
import (
"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]byte
n, err := client.Read(b[:])
if err != nil {
log.Println(err)
return
}
var method, host, address string
fmt.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不帶端口, 默認80
address = 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")
} else {
server.Write(b[:n])
}
//進行轉發
go io.Copy(server, client)
io.Copy(client, server)
}
把源代碼編譯,然后放到你國外的VPS上,在自己機器上配置好HTTP代理,就可以 到處 訪問,自由自在了。
來自:http://www.jianshu.com/p/53e219fbf3c5