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

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

このコミットは、Go言語の標準ライブラリであるnet/http/httputilパッケージ内のDumpRequestOut関数におけるバグ修正に関するものです。具体的には、HTTPS URLを持つリクエストをダンプしようとした際に発生する、SSLネゴシエーションの試行によるハングアップ問題を解決しています。

コミット

commit 1b1039a1c1fba650023431696dc02d3f8343ad27
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Feb 28 16:03:32 2012 -0800

    net/http/httputil: fix DumpRequestOut on https URLs
    
    Don't try to do an SSL negotiation with a *bytes.Buffer.
    
    Fixes #3135
    
    R=golang-dev, dsymonds
    CC=golang-dev
    https://golang.org/cl/5709050

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

https://github.com/golang/go/commit/1b1039a1c1fba650023431696dc02d3f8343ad27

元コミット内容

net/http/httputil: fix DumpRequestOut on https URLs

Don't try to do an SSL negotiation with a *bytes.Buffer.

Fixes #3135

変更の背景

net/http/httputilパッケージのDumpRequestOut関数は、http.Requestオブジェクトを、実際にネットワーク上で送信されるであろう生のHTTPメッセージ形式(ワイヤーフォーマット)でダンプすることを目的としています。この関数は、内部的にGoのhttp.Transportのロジックを利用して、リクエストのシミュレーションを行います。

しかし、このシミュレーションでは、実際のTCPネットワーク接続ではなく、bytes.Bufferとパイプを組み合わせた「偽のnet.Conn」が使用されます。問題は、DumpRequestOutに渡されたhttp.RequestURL.Scheme"https"である場合、http.TransportがHTTPS通信のためにSSL/TLSハンドシェイクを試みてしまう点にありました。

偽のnet.Connは実際のTLSハンドシェイクを行う能力がないため、この試みは失敗するか、あるいは無限にハングアップ(ブロック)してしまい、DumpRequestOut関数が正常に完了しないというバグが発生していました。この問題はGoのIssue #3135として報告されており、このコミットはその修正を目的としています。

前提知識の解説

このコミットの理解には、以下のGo言語のネットワークおよびHTTP関連の概念が役立ちます。

  • Go言語のnet/httpパッケージ: Go言語でHTTPクライアントおよびサーバーを構築するための標準ライブラリです。HTTPリクエスト、レスポンス、サーバー、クライアントなどの基本的な型と機能を提供します。
  • net/http/httputilパッケージ: net/httpパッケージを補完するユーティリティ関数を提供するパッケージです。HTTPリクエストやレスポンスのダンプ、リバースプロキシの実装などが含まれます。
  • http.Request構造体: HTTPリクエストのすべての側面(メソッド、URL、ヘッダー、ボディなど)をカプセル化するGoの構造体です。URLフィールドは*url.URL型であり、その中にScheme(例: "http", "https")が含まれます。
  • http.Transport: net/httpクライアントの低レベルな実装を担うコンポーネントです。ネットワーク接続の確立(ダイヤル)、TLSハンドシェイク、リクエストのワイヤーへの書き込み、レスポンスの読み込みなど、実際のネットワーク通信の詳細を処理します。
  • bytes.Buffer: bytesパッケージで提供される、可変長のバイトスライスを扱うためのバッファです。io.Readerio.Writerインターフェースを実装しており、メモリ内でのI/O操作のシミュレーションや、データの蓄積によく使用されます。
  • net.Connインターフェース: netパッケージで定義される、ネットワーク接続を表すインターフェースです。ReadWriteCloseなどのメソッドを持ち、TCP/IPソケットなどの具体的な接続を抽象化します。
  • SSL/TLSハンドシェイク: HTTPS(HTTP Secure)通信を確立する際に、クライアントとサーバー間で行われる一連のプロトコルネゴシエーションです。これにより、暗号化された安全な通信チャネルが確立されます。
  • ワイヤーフォーマット (Wire Format): ネットワーク上を流れるデータの実際のバイト列形式を指します。HTTPとHTTPSのワイヤーフォーマットは、TLS層による暗号化の有無を除けば、HTTPメッセージ自体は同じ構造を持ちます。つまり、TLS層が確立された後、その上で流れるHTTPリクエスト/レスポンスの形式は、HTTPとHTTPSで本質的に変わりません。

技術的詳細

このコミットの技術的な核心は、DumpRequestOut関数がhttp.Transportを利用してリクエストをシミュレートする際に、HTTPSリクエストに対して不要なSSL/TLSハンドシェイクを回避することです。

DumpRequestOut関数は、リクエストをダンプするために、実際のネットワーク接続の代わりに、bytes.Bufferとパイプを組み合わせて作成された「偽のnet.Conn」を使用します。この偽の接続は、リクエストがネットワーク上でどのように見えるかを「記録」するためのものであり、実際のTLSハンドシェイクを実行する能力はありません。

元の実装では、req.URL.Scheme"https"である場合、http.TransportはTLSハンドシェイクを開始しようとします。しかし、偽のnet.Connではこのハンドシェイクが完了できないため、関数がブロックしたり、リソースが枯渇したりする問題が発生していました。

このコミットによる修正は、以下のロジックを導入することでこの問題を解決します。

  1. DumpRequestOut関数内で、ダンプ対象のhttp.RequestURL.Scheme"https"であるかをチェックします。
  2. もし"https"であれば、一時的にreq.URL.Scheme"http"に書き換えます。
  3. この変更は、deferステートメントによって囲まれた無名関数内で、DumpRequestOut関数が終了する際に元の"https"に戻されるように設定されます。これにより、reqオブジェクトの外部からの観測可能な状態は変更されず、関数の副作用が局所化されます。
  4. req.URL.Scheme"http"に設定されることで、http.TransportはTLSハンドシェイクを試みることなく、純粋なHTTPワイヤーフォーマットとしてリクエストを処理します。
  5. HTTPとHTTPSのワイヤーフォーマット(HTTPメッセージ自体)は同じであるため、このスキームの一時的な変更は、ダンプされるHTTPメッセージの正確性に影響を与えません。

このアプローチにより、DumpRequestOutはHTTPSリクエストに対しても、実際のネットワーク通信を伴わずに、期待されるワイヤーフォーマットを正確にダンプできるようになります。

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

このコミットによる主要なコード変更は以下の2つのファイルにあります。

  1. src/pkg/net/http/httputil/dump.go: DumpRequestOut関数に、HTTPSスキームを持つリクエストを処理するためのロジックが追加されました。
  2. src/pkg/net/http/httputil/dump_test.go: DumpRequestOut関数の修正を検証するための新しいテストケースが追加されました。

src/pkg/net/http/httputil/dump.go の変更点

--- a/src/pkg/net/http/httputil/dump.go
+++ b/src/pkg/net/http/httputil/dump.go
@@ -59,6 +59,15 @@ func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
 		}
 	}
 
+	// Since we're using the actual Transport code to write the request,
+	// switch to http so the Transport doesn't try to do an SSL
+	// negotiation with our dumpConn and its bytes.Buffer & pipe.
+	// The wire format for https and http are the same, anyway.
+	if req.URL.Scheme == "https" {
+		defer func() { req.URL.Scheme = "https" }()
+		req.URL.Scheme = "http"
+	}
+
 	// Use the actual Transport code to record what we would send
 	// on the wire, but not using TCP.  Use a Transport with a
 	// customer dialer that returns a fake net.Conn that waits

src/pkg/net/http/httputil/dump_test.go の変更点

--- a/src/pkg/net/http/httputil/dump_test.go
+++ b/src/pkg/net/http/httputil/dump_test.go
@@ -71,6 +71,18 @@ var dumpTests = []dumpTest{
 			"User-Agent: Go http package\r\n" +\
 			"Accept-Encoding: gzip\r\n\r\n",
 	},
+
+	// Test that an https URL doesn't try to do an SSL negotiation
+	// with a bytes.Buffer and hang with all goroutines not
+	// runnable.
+	{
+		Req: *mustNewRequest("GET", "https://example.com/foo", nil),
+
+		WantDumpOut: "GET /foo HTTP/1.1\r\n" +\
+			"Host: example.com\r\n" +\
+			"User-Agent: Go http package\r\n" +\
+			"Accept-Encoding: gzip\r\n\r\n",
+	},
 }
 
 func TestDumpRequest(t *testing.T) {

コアとなるコードの解説

src/pkg/net/http/httputil/dump.go の変更解説

DumpRequestOut関数は、HTTPリクエストをバイト列としてダンプする主要なロジックを含んでいます。追加されたコードブロックは以下の通りです。

	// Since we're using the actual Transport code to write the request,
	// switch to http so the Transport doesn't try to do an SSL
	// negotiation with our dumpConn and its bytes.Buffer & pipe.
	// The wire format for https and http are the same, anyway.
	if req.URL.Scheme == "https" {
		defer func() { req.URL.Scheme = "https" }()
		req.URL.Scheme = "http"
	}
  • コメント: このコードブロックの目的を明確に説明しています。「実際のTransportコードを使ってリクエストを書き込むため、TransportがdumpConnbytes.Bufferとパイプで構成される偽の接続)とのSSLネゴシエーションを試みないように、httpに切り替える。HTTPとHTTPSのワイヤーフォーマットは同じである。」
  • if req.URL.Scheme == "https": ダンプ対象のリクエストのURLスキームが"https"である場合にのみ、以下の処理を実行します。
  • defer func() { req.URL.Scheme = "https" }(): deferキーワードは、囲む関数(この場合はDumpRequestOut)がリターンする直前に、指定された関数を実行することを保証します。ここでは、一時的に"http"に変更されたreq.URL.Schemeを、関数の終了時に元の"https"に戻す役割を果たします。これにより、DumpRequestOut関数がreqオブジェクトに対して行った変更が、関数の外部に影響を与えないようにしています。
  • req.URL.Scheme = "http": これが修正の核心です。http.RequestオブジェクトのURLスキームを一時的に"http"に設定します。これにより、その後のhttp.Transportによる処理が、TLSハンドシェイクを伴わない通常のHTTPリクエストとして行われるようになります。

src/pkg/net/http/httputil/dump_test.go の変更解説

このファイルには、DumpRequestOut関数の動作を検証するためのテストケースが定義されています。追加された新しいテストケースは以下の通りです。

	// Test that an https URL doesn't try to do an SSL negotiation
	// with a bytes.Buffer and hang with all goroutines not
	// runnable.
	{
		Req: *mustNewRequest("GET", "https://example.com/foo", nil),

		WantDumpOut: "GET /foo HTTP/1.1\r\n" +\
			"Host: example.com\r\n" +\
			"User-Agent: Go http package\r\n" +\
			"Accept-Encoding: gzip\r\n\r\n",
	},
  • コメント: このテストの目的を明確に述べています。「HTTPS URLがbytes.BufferとのSSLネゴシエーションを試みず、すべてのゴルーチンが実行不能な状態でハングアップしないことをテストする。」これは、まさにこのコミットが解決しようとしている問題を示しています。
  • Req: *mustNewRequest("GET", "https://example.com/foo", nil): テスト対象のリクエストを定義しています。ここでは、https://example.com/fooというHTTPSスキームを持つURLが使用されています。
  • WantDumpOut: "GET /foo HTTP/1.1\\r\\n" + ...: このフィールドは、DumpRequestOut関数が返すことが期待されるダンプされたHTTPメッセージのバイト列を表します。注目すべきは、リクエストラインがGET /foo HTTP/1.1となっており、スキームがhttpsではなくhttpとして扱われている点です。これは、dump.goでのスキームの一時的な書き換えが正しく機能し、TLSハンドシェイクなしでHTTPワイヤーフォーマットが生成されることを検証しています。

このテストケースの追加により、DumpRequestOut関数がHTTPSリクエストに対しても正しく、かつハングアップせずに動作することが保証されるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: net/httpパッケージ, net/http/httputilパッケージ
  • Go言語のソースコード: src/pkg/net/http/httputil/dump.go, src/pkg/net/http/httputil/dump_test.go
  • HTTP/1.1 RFC 2616 (特にメッセージフォーマットに関するセクション)
  • TLS/SSLプロトコルに関する一般的な知識
  • bytes.BufferのGoドキュメント
  • net.ConnインターフェースのGoドキュメント
  • deferステートメントに関するGo言語のドキュメント