5分でGo言語(Golang)でHTTPエージェント(Proxy)を実現


私たちはソフトウェア開発の過程で、パッケージをつかんだり、HTTPコンテンツの転送を理解したり、Nginx逆エージェントなど、エージェントが必要な場所に遭遇します.
以前Linuxで降りる時、PrivoxyをインストールしてSocketエージェントをHTTPエージェントに変換して、起動して起動して、比較的に便利です.しかしMacではBrewでインストールしたPrivoxyを使うのは難しいので、SocketとHTTPエージェントを1つのソフトウェアで済ませてみたいと思っています.そうすれば、個別のソフトウェアをインストールして変換する必要はありません.
と思って始めましょう.以前はあまりネットプログラミングをしていませんでしたが、最近はちょうど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プロトコルの標準的な方法です.
第2部はURLで、httpsのリクエストはhostとportだけで、httpのリクエストは完成したurlで、後でサンプルを見て、分かりました.
第3部はHTTPのプロトコルとバージョンで、これはあまり注目しなくてもいいです.
以上はhttpのリクエストです.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).httpより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[:], '
')]), "%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(v 4 orv 6)、ホスト名(イントラネット)、ドメイン名(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
")    } else {        server.Write(b[:n])    }    //    go io.Copy(server, client)    io.Copy(client, server)

このうちCONNECTメソッドには個別の応答があり,クライアントは接続を確立し,プロキシサーバは応答を確立してからHTTPのようにアクセスを要求することができると述べている.
完全なコード
ここで、エージェントサーバの開発はすべて完了しました.次は完全なソースコードです.
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]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[:], '
')]), "%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
")    } else {        server.Write(b[:n])    }    //    go io.Copy(server, client)    io.Copy(client, server) }

ソースコードをコンパイルして、自分のパソコンに置いて、テストしましょう.