Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10090] ファイルの概要

このコミットは、Go言語の標準ライブラリであるnet/httpパッケージ内のReverseProxyにおいて、バックエンドへのリクエストからConnectionヘッダーを削除する変更を導入しています。これにより、リバースプロキシがクライアントからのConnectionヘッダーに影響されずに、バックエンドとの永続的な接続を維持できるようになります。

コミット

commit 7c5d90dfbd1a1647e94c26ba11dba779e54c1b37
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Oct 25 22:11:01 2011 -0700

    http: remove Connection header in ReverseProxy
    
    Fixes #2342
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/5302057

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/7c5d90dfbd1a1647e94c26ba11dba779e54c1b37

元コミット内容

http: remove Connection header in ReverseProxy

Fixes #2342

変更の背景

この変更は、Go言語のIssue #2342「net/http/httputil.ReverseProxy should strip Connection header」を修正するために行われました。

元のReverseProxyの実装では、クライアントから送られてきたHTTPリクエストのConnectionヘッダーがそのままバックエンドサーバーに転送されていました。HTTP/1.1では、Connectionヘッダーはホップバイホップヘッダー(hop-by-hop header)であり、プロキシを介して転送されるべきではありません。このヘッダーは、現在の接続にのみ適用されるべきであり、プロキシとクライアント間、またはプロキシとバックエンド間の個別の接続に対して異なる意味を持つ可能性があります。

具体的には、クライアントがConnection: closeヘッダーを送信した場合、プロキシがそのヘッダーをバックエンドに転送すると、バックエンドは接続を閉じることを期待します。しかし、リバースプロキシの一般的なユースケースでは、プロキシとバックエンドの間で永続的な(Keep-Alive)接続を維持することが望ましいです。これにより、新しいリクエストごとにTCP接続を再確立するオーバーヘッドを削減し、パフォーマンスを向上させることができます。

この問題は、特にクライアントがConnection: closeを送信した場合に顕著で、プロキシがバックエンドとの接続を不必要に切断してしまう原因となっていました。このコミットは、この問題を解決し、ReverseProxyがより堅牢で効率的に動作するようにするためのものです。

前提知識の解説

HTTP Connection ヘッダー

HTTPのConnectionヘッダーは、現在のトランザクションが完了した後に、接続を閉じるか、それとも開いたままにするか(永続的な接続、Keep-Alive)を制御するために使用されます。

  • Connection: close: 現在のトランザクションが完了したら、接続を閉じることを示します。
  • Connection: Keep-Alive: 現在のトランザクションが完了した後も、接続を開いたままにして、同じ接続で後続の要求を送信できるようにすることを示します。

重要なのは、Connectionヘッダーがホップバイホップヘッダーであるという点です。これは、このヘッダーが単一のTCP接続にのみ適用され、プロキシを介して転送されるべきではないことを意味します。プロキシは、受信したConnectionヘッダーを処理した後、それを下流に転送する前に削除する必要があります。

リバースプロキシ (Reverse Proxy)

リバースプロキシは、クライアントからのリクエストを受け取り、それを一つ以上のバックエンドサーバーに転送し、バックエンドからのレスポンスをクライアントに返すサーバーです。クライアントはリバースプロキシの存在を意識せず、直接バックエンドサーバーと通信しているかのように見えます。

リバースプロキシの主な利点には以下のようなものがあります。

  • 負荷分散: 複数のバックエンドサーバーにリクエストを分散し、システムの可用性とスケーラビリティを向上させます。
  • セキュリティ: バックエンドサーバーのIPアドレスや構成を隠蔽し、直接的な攻撃から保護します。
  • SSL終端: SSL/TLS暗号化をプロキシで終端し、バックエンドサーバーの負荷を軽減します。
  • キャッシュ: 静的コンテンツをキャッシュし、バックエンドサーバーへのリクエストを減らします。
  • 永続接続 (Keep-Alive): プロキシとバックエンド間で永続的な接続を維持することで、パフォーマンスを向上させます。

Go言語の net/http/httputil.ReverseProxy

Go言語のnet/http/httputilパッケージは、HTTPユーティリティ機能を提供し、その中にReverseProxy構造体があります。これは、HTTPリクエストを別のサーバーに転送するための基本的なリバースプロキシ機能を提供します。ReverseProxyは、http.Handlerインターフェースを実装しており、http.ListenAndServeなどで直接使用できます。

技術的詳細

このコミットの主要な目的は、ReverseProxyがバックエンドサーバーにリクエストを転送する際に、クライアントから受け取ったConnectionヘッダーを削除することです。これにより、プロキシとバックエンド間の接続が、クライアントのConnectionヘッダーの設定に影響されずに、プロキシ自身の接続管理ポリシー(通常は永続接続)に従うようになります。

変更は主に以下の2つのファイルで行われています。

  1. src/pkg/http/reverseproxy.go: ReverseProxyの主要なロジックが含まれるファイル。
  2. src/pkg/http/reverseproxy_test.go: ReverseProxyのテストファイル。

src/pkg/http/reverseproxy.go の変更点

  • copyHeader 関数の追加: 新しいヘルパー関数copyHeader(dst, src Header)が追加されました。この関数は、srcヘッダーマップからdstヘッダーマップにすべてのヘッダーをコピーします。これは、ヘッダーのコピー処理をカプセル化し、コードの重複を避けるためのものです。

    func copyHeader(dst, src Header) {
        for k, vv := range src {
            for _, v := range vv {
                dst.Add(k, v)
            }
        }
    }
    
  • ServeHTTP メソッド内の Connection ヘッダー処理の追加: ReverseProxy.ServeHTTPメソッド内で、バックエンドに転送するリクエスト(outreq)のヘッダーからConnectionヘッダーを削除するロジックが追加されました。

    // Remove the connection header to the backend.  We want a
    // persistent connection, regardless of what the client sent
    // to us.  This is modifying the same underlying map from req
    // (shallow copied above) so we only copy it if necessary.
    if outreq.Header.Get("Connection") != "" {
        outreq.Header = make(Header)
        copyHeader(outreq.Header, req.Header)
        outreq.Header.Del("Connection")
    }
    

    このコードブロックの意図は以下の通りです。

    1. outreq.Header.Get("Connection") != "":転送するリクエストのヘッダーにConnectionヘッダーが存在するかどうかを確認します。
    2. outreq.Header = make(Header):もしConnectionヘッダーが存在する場合、outreq.Headerを新しい空のHeaderマップで再初期化します。これは、outreq.Headerが元のリクエストreq.Headerのシャローコピーであるため、元のreq.Headerを変更しないようにするためです。新しいマップを作成することで、outreqのヘッダーのみが変更され、reqのヘッダーはそのまま保持されます。
    3. copyHeader(outreq.Header, req.Header):元のリクエストreq.Headerから、新しく作成したoutreq.Headerにすべてのヘッダーをコピーします。
    4. outreq.Header.Del("Connection"):コピーが完了した後、outreq.HeaderからConnectionヘッダーを削除します。
  • レスポンスヘッダーのコピー処理の簡素化: バックエンドからのレスポンスヘッダーをクライアントに転送する際にも、新しく追加されたcopyHeader関数が使用されるように変更されました。これにより、コードがより簡潔になりました。

    変更前:

    hdr := rw.Header()
    for k, vv := range res.Header {
        for _, v := range vv {
            hdr.Add(k, v)
        }
    }
    

    変更後:

    copyHeader(rw.Header(), res.Header)
    

src/pkg/http/reverseproxy_test.go の変更点

  • Connection ヘッダー削除のテストケース追加: TestReverseProxy関数内に、バックエンドサーバーがConnectionヘッダーを受け取らないことを検証するテストロジックが追加されました。

    if c := r.Header.Get("Connection"); c != "" {
        t.Errorf("handler got Connection header value %q", c)
    }
    

    このテストは、バックエンドサーバーのハンドラ内で、受信したリクエストのConnectionヘッダーが空であることを確認します。もし空でなければ、エラーを報告します。

  • テストリクエストに Connection: close を設定: テスト用のHTTPリクエストに明示的にConnection: closeヘッダーとClose: trueを設定することで、このコミットで修正されるシナリオを再現し、ReverseProxyが正しくConnectionヘッダーを削除するかどうかを検証します。

    getReq.Header.Set("Connection", "close")
    getReq.Close = true
    

コアとなるコードの変更箇所

src/pkg/http/reverseproxy.go

--- a/src/pkg/http/reverseproxy.go
+++ b/src/pkg/http/reverseproxy.go
@@ -69,6 +69,14 @@ func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {
 	return &ReverseProxy{Director: director}
 }
 
+func copyHeader(dst, src Header) {
+	for k, vv := range src {
+		for _, v := range vv {
+			dst.Add(k, v)
+		}
+	}
+}
+
 func (p *ReverseProxy) ServeHTTP(rw ResponseWriter, req *Request) {
 	transport := p.Transport
 	if transport == nil {
@@ -84,6 +92,16 @@ func (p *ReverseProxy) ServeHTTP(rw ResponseWriter, req *Request) {
 	outreq.ProtoMinor = 1
 	outreq.Close = false
 
+	// Remove the connection header to the backend.  We want a
+	// persistent connection, regardless of what the client sent
+	// to us.  This is modifying the same underlying map from req
+	// (shallow copied above) so we only copy it if necessary.
+	if outreq.Header.Get("Connection") != "" {
+		outreq.Header = make(Header)
+		copyHeader(outreq.Header, req.Header)
+		outreq.Header.Del("Connection")
+	}
+
 	if clientIp, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
 		outreq.Header.Set("X-Forwarded-For", clientIp)
 	}
@@ -95,12 +113,7 @@ func (p *ReverseProxy) ServeHTTP(rw ResponseWriter, req *Request) {
 		return
 	}
 
-	hdr := rw.Header()
-	for k, vv := range res.Header {
-		for _, v := range vv {
-			hdr.Add(k, v)
-		}
-	}
+	copyHeader(rw.Header(), res.Header)
 
 	rw.WriteHeader(res.StatusCode)
 

src/pkg/http/reverseproxy_test.go

--- a/src/pkg/http/reverseproxy_test.go
+++ b/src/pkg/http/reverseproxy_test.go
@@ -24,6 +24,9 @@ func TestReverseProxy(t *testing.T) {
 		if r.Header.Get("X-Forwarded-For") == "" {
 			t.Errorf("didn't get X-Forwarded-For header")
 		}
+		if c := r.Header.Get("Connection"); c != "" {
+			t.Errorf("handler got Connection header value %q", c)
+		}
 		if g, e := r.Host, "some-name"; g != e {
 			t.Errorf("backend got Host header %q, want %q", g, e)
 		}
@@ -43,6 +46,8 @@ func TestReverseProxy(t *testing.T) {
 
 	getReq, _ := NewRequest("GET", frontend.URL, nil)
 	getReq.Host = "some-name"
+	getReq.Header.Set("Connection", "close")
+	getReq.Close = true
 	res, err := DefaultClient.Do(getReq)
 	if err != nil {
 		t.Fatalf("Get: %v", err)

コアとなるコードの解説

src/pkg/http/reverseproxy.go

  • copyHeader 関数: この関数は、HTTPヘッダー(http.Header型、map[string][]stringのエイリアス)を安全かつ効率的にコピーするためのユーティリティです。for k, vv := range srcでソースヘッダーのキーと値のリストをイテレートし、dst.Add(k, v)を使ってデスティネーションヘッダーに各値を追加します。Addメソッドは、同じキーに対して複数の値が存在する場合(例: Set-Cookieヘッダー)、既存の値を上書きせずに新しい値を追加するため、ヘッダーのセマンティクスを正しく維持します。

  • ServeHTTP メソッド内の Connection ヘッダー処理: この部分がコミットの核心です。 outreqは、クライアントから受け取ったreqを基に、バックエンドに転送するために準備されるリクエストです。 outreq.Headerは、初期段階ではreq.Headerのシャローコピー(同じ基盤マップを指す)である可能性があります。そのため、outreq.Header.Del("Connection")を直接呼び出すと、元のreq.Headerも変更されてしまう可能性があります。 これを避けるために、if outreq.Header.Get("Connection") != ""という条件が真の場合(つまり、Connectionヘッダーが存在する場合)、outreq.Header = make(Header)によって新しい独立したヘッダーマップが作成されます。 次に、copyHeader(outreq.Header, req.Header)を使って、元のreq.Headerのすべての内容がこの新しいoutreq.Headerにコピーされます。 最後に、outreq.Header.Del("Connection")が呼び出され、新しくコピーされたoutreq.HeaderからのみConnectionヘッダーが削除されます。これにより、元のクライアントリクエストのヘッダーは変更されずに保持され、バックエンドに転送されるリクエストからのみConnectionヘッダーが取り除かれることが保証されます。 この処理は、リバースプロキシがクライアントとバックエンドの間で独立した接続管理を行うために不可欠です。

  • レスポンスヘッダーのコピー処理: バックエンドからのレスポンスresのヘッダーを、クライアントへのレスポンスrwのヘッダーにコピーする際にも、新しく定義されたcopyHeader関数が利用されています。これにより、冗長なループ処理が削除され、コードの可読性と保守性が向上しています。

src/pkg/http/reverseproxy_test.go

  • Connection ヘッダーのテスト: テストコードでは、バックエンドサーバーのハンドラ内で、受信したリクエストのConnectionヘッダーが空であることを確認するアサーションが追加されています。これは、ReverseProxyが正しくConnectionヘッダーを削除したことを検証するためのものです。 また、テストクライアントが送信するリクエストにgetReq.Header.Set("Connection", "close")getReq.Close = trueを設定することで、Connection: closeヘッダーが送信されるシナリオを明示的にシミュレートし、この修正が意図通りに機能するかどうかを確認しています。

これらの変更により、GoのReverseProxyはHTTPプロキシの仕様に準拠し、より堅牢で予測可能な動作をするようになりました。

関連リンク

参考にした情報源リンク