[インデックス 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.Request
のURL.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.Reader
やio.Writer
インターフェースを実装しており、メモリ内でのI/O操作のシミュレーションや、データの蓄積によく使用されます。net.Conn
インターフェース:net
パッケージで定義される、ネットワーク接続を表すインターフェースです。Read
、Write
、Close
などのメソッドを持ち、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
ではこのハンドシェイクが完了できないため、関数がブロックしたり、リソースが枯渇したりする問題が発生していました。
このコミットによる修正は、以下のロジックを導入することでこの問題を解決します。
DumpRequestOut
関数内で、ダンプ対象のhttp.Request
のURL.Scheme
が"https"
であるかをチェックします。- もし
"https"
であれば、一時的にreq.URL.Scheme
を"http"
に書き換えます。 - この変更は、
defer
ステートメントによって囲まれた無名関数内で、DumpRequestOut
関数が終了する際に元の"https"
に戻されるように設定されます。これにより、req
オブジェクトの外部からの観測可能な状態は変更されず、関数の副作用が局所化されます。 req.URL.Scheme
が"http"
に設定されることで、http.Transport
はTLSハンドシェイクを試みることなく、純粋なHTTPワイヤーフォーマットとしてリクエストを処理します。- HTTPとHTTPSのワイヤーフォーマット(HTTPメッセージ自体)は同じであるため、このスキームの一時的な変更は、ダンプされるHTTPメッセージの正確性に影響を与えません。
このアプローチにより、DumpRequestOut
はHTTPSリクエストに対しても、実際のネットワーク通信を伴わずに、期待されるワイヤーフォーマットを正確にダンプできるようになります。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の2つのファイルにあります。
src/pkg/net/http/httputil/dump.go
:DumpRequestOut
関数に、HTTPSスキームを持つリクエストを処理するためのロジックが追加されました。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が
dumpConn
(bytes.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 Issue #3135: https://github.com/golang/go/issues/3135
- Go CL 5709050: https://golang.org/cl/5709050
参考にした情報源リンク
- 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言語のドキュメント