[インデックス 12274] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http/httputil パッケージ内の DumpRequestOut 関数における潜在的な競合状態(race condition)を修正するものです。具体的には、HTTPSリクエストをダンプする際に、元の http.Request オブジェクトの URL.Scheme フィールドが一時的に変更されることによって発生する問題に対処しています。
コミット
commit 02b124e59a444864b9a2b98f556ba606068305b6
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 29 09:52:28 2012 -0800
net/http/httputil: make https DumpRequestOut less racy
It's still racy in that it mutates req.Body, though. *shrug*
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5709054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/02b124e59a444864b9a2b98f556ba606068305b6
元コミット内容
このコミットは、src/pkg/net/http/httputil/dump.go ファイルに対して行われました。主な変更点は、DumpRequestOut 関数内でHTTPSリクエストを処理する際に、元の http.Request オブジェクトを直接変更するのではなく、そのコピーを作成して操作するようにしたことです。
変更前は、HTTPSリクエストの場合、req.URL.Scheme を一時的に "https" から "http" に変更し、t.RoundTrip(req) を呼び出した後に defer を使って元の "https" に戻していました。
変更後は、HTTPSリクエストの場合に reqSend という http.Request の新しいインスタンスを作成し、元の req の内容をコピーします。そして、この reqSend の URL.Scheme のみを "http" に変更し、t.RoundTrip(reqSend) を呼び出すように修正されました。
変更の背景
net/http/httputil.DumpRequestOut 関数は、HTTPリクエストの内容をバイト列としてダンプ(出力)するために使用されます。これは主にデバッグやロギングの目的で利用されます。
この関数がHTTPSリクエストをダンプする際、内部的には http.Transport を利用してリクエストを「送信」するシミュレーションを行います。しかし、実際のTLSハンドシェイクを行う必要はなく、単にHTTPリクエストのワイヤーフォーマット(ネットワーク上を流れる形式)を再現したいだけです。HTTPとHTTPSのワイヤーフォーマットは、プロトコルスキーム(http または https)を除けば同じです。
以前の実装では、HTTPSリクエストの場合に、ダンプ処理のために一時的に req.URL.Scheme を "https" から "http" に変更していました。そして、defer ステートメントを使って、関数が終了する際に元の "https" に戻すようにしていました。
このアプローチには、以下の問題がありました。
- 競合状態(Race Condition)の可能性:
reqオブジェクトが複数のゴルーチン(goroutine)間で共有されている場合、またはDumpRequestOutが呼び出されている間に他のコードがreqオブジェクトにアクセスする可能性がある場合、req.URL.Schemeが一時的に "http" になっている状態を他のゴルーチンが観測してしまう可能性があります。これにより、予期しない動作やバグが発生する競合状態が生じます。特に、RoundTripの処理中に他のゴルーチンがreq.URL.Schemeを参照した場合、誤ったスキームが読み取られるリスクがありました。 - 副作用:
DumpRequestOutはリクエストをダンプするユーティリティ関数であり、本来は元のリクエストオブジェクトに副作用を与えるべきではありません。しかし、req.URL.Schemeを直接変更することは、この原則に反していました。
このコミットは、これらの問題を解決し、DumpRequestOut 関数がより安全で予測可能な動作をするようにするために行われました。コミットメッセージにある「It's still racy in that it mutates req.Body, though. shrug」という記述は、req.Body の変更(ダンプのために読み取られ、その後再構築される)という別の副作用は残るものの、URL.Scheme に関する競合状態は解消されたことを示しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の net/http パッケージに関する知識が必要です。
http.Request構造体:- HTTPリクエストを表すGoの構造体です。
URLフィールドを持ち、これは*url.URL型です。Bodyフィールドを持ち、これはio.ReadCloser型で、リクエストボディの読み取りに使用されます。
url.URL構造体:- URLを解析して表現する構造体です。
Schemeフィールドを持ち、URLのスキーム(例: "http", "https")を文字列で保持します。
http.TransportとRoundTripperインターフェース:http.Transportは、HTTPリクエストを送信し、HTTPレスポンスを受信するメカニズムを実装する型です。RoundTripperインターフェースは、RoundTrip(*Request) (*Response, error)メソッドを定義しており、単一のHTTPトランザクションを実行します。http.Transportはこのインターフェースの実装の一つです。DumpRequestOut関数は、リクエストを実際にネットワークに送信する代わりに、http.TransportのRoundTripメソッドを模倣してリクエストのワイヤーフォーマットを生成します。
- 競合状態(Race Condition):
- 複数のゴルーチンが共有リソース(この場合は
http.RequestオブジェクトのURL.Schemeフィールド)に同時にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生するプログラミング上のバグです。 - 操作の順序が非決定論的であるため、プログラムの実行ごとに結果が変わる可能性があります。
- 複数のゴルーチンが共有リソース(この場合は
deferステートメント:- Go言語のキーワードで、
deferに続く関数呼び出しを、その関数がリターンする直前に実行するようにスケジュールします。 - リソースの解放やクリーンアップによく使用されますが、このコミットのケースのように、共有リソースの一時的な変更とその復元に使用すると、競合状態を引き起こす可能性があります。
- Go言語のキーワードで、
技術的詳細
DumpRequestOut 関数は、与えられた http.Request オブジェクトをバイト列に変換します。このプロセスでは、http.Transport の RoundTrip メソッドを内部的に呼び出すことで、リクエストがネットワーク上でどのように見えるかをシミュレートします。
問題は、HTTPSリクエスト(req.URL.Scheme == "https")の場合に発生していました。http.Transport は通常、HTTPSリクエストに対してTLSハンドシェイクを試みますが、DumpRequestOut の目的は単にHTTPリクエストの生データを取得することであり、実際のTLS通信は不要です。そのため、以前の実装では、RoundTrip がTLSハンドシェイクを試みないように、一時的に req.URL.Scheme を "http" に変更していました。
変更前:
if req.URL.Scheme == "https" {
defer func() { req.URL.Scheme = "https" }() // (A) 関数終了時に元のスキームに戻す
req.URL.Scheme = "http" // (B) スキームを一時的に変更
}
// ...
_, err := t.RoundTrip(req) // (C) 変更されたreqでRoundTripを呼び出す
このコードでは、(B)で req.URL.Scheme が直接変更されます。もし、(B)と(A)の間で、別のゴルーチンが req オブジェクトにアクセスし、その URL.Scheme を読み取ろうとした場合、"http" という一時的な値を見てしまう可能性がありました。これは、req オブジェクトが共有されている場合に競合状態を引き起こします。
変更後:
reqSend := req // (D) まずはreqをreqSendに代入
if req.URL.Scheme == "https" {
reqSend = new(http.Request) // (E) 新しいhttp.Requestインスタンスを作成
*reqSend = *req // (F) reqの内容をreqSendにシャローコピー
reqSend.URL = new(url.URL) // (G) 新しいurl.URLインスタンスを作成
*reqSend.URL = *req.URL // (H) req.URLの内容をreqSend.URLにシャローコピー
reqSend.URL.Scheme = "http" // (I) コピーしたreqSendのスキームのみを変更
}
// ...
_, err := t.RoundTrip(reqSend) // (J) コピーしたreqSendでRoundTripを呼び出す
この修正では、元の req オブジェクトを直接変更する代わりに、reqSend という新しい http.Request オブジェクトを作成し、そこに元の req の内容をコピーします(シャローコピー)。特に重要なのは、req.URL フィールドもポインタであるため、reqSend.URL も新しい url.URL インスタンスを作成し、元の req.URL の内容をコピーしている点です。これにより、reqSend.URL.Scheme を "http" に変更しても、元の req.URL.Scheme には一切影響が及びません。
結果として、DumpRequestOut 関数は、元の http.Request オブジェクトに副作用を与えることなく、安全にHTTPSリクエストのダンプ処理を実行できるようになりました。これにより、マルチスレッド環境での競合状態のリスクが排除されます。
コアとなるコードの変更箇所
src/pkg/net/http/httputil/dump.go ファイルの DumpRequestOut 関数内。
--- a/src/pkg/net/http/httputil/dump.go
+++ b/src/pkg/net/http/httputil/dump.go
@@ -12,6 +12,7 @@ import (
"io/ioutil"
"net"
"net/http"
+ "net/url" // 追加されたインポート
"strings"
"time"
)
@@ -63,9 +64,13 @@ func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
// 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"
+ reqSend := req // 新しい変数reqSendを導入
+ if req.URL.Scheme == "https" {
+ reqSend = new(http.Request) // 新しいhttp.Requestインスタンスを作成
+ *reqSend = *req // reqの内容をreqSendにコピー
+ reqSend.URL = new(url.URL) // 新しいurl.URLインスタンスを作成
+ *reqSend.URL = *req.URL // req.URLの内容をreqSend.URLにコピー
+ reqSend.URL.Scheme = "http" // コピーしたreqSendのスキームのみを変更
}
// Use the actual Transport code to record what we would send
@@ -88,7 +93,7 @@ func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
},
}
- _, err := t.RoundTrip(req) // 変更前: 元のreqを使用
+ _, err := t.RoundTrip(reqSend) // 変更後: コピーしたreqSendを使用
req.Body = save
if err != nil {
コアとなるコードの解説
import "net/url"の追加:url.URL構造体を操作するために、net/urlパッケージが新しくインポートされました。reqSend := reqの導入: まず、reqSendという新しい*http.Request型の変数を宣言し、元のreqを代入します。これにより、デフォルトでは元のリクエストが使われます。- HTTPSスキームの条件分岐内でのコピー処理:
if req.URL.Scheme == "https"のブロック内で、HTTPSリクエストの場合の特別な処理が記述されます。reqSend = new(http.Request): 新しいhttp.Requestオブジェクトをヒープ上に割り当て、そのポインタをreqSendに代入します。*reqSend = *req: これは構造体のシャローコピー(shallow copy)です。reqのすべてのフィールドの値がreqSendにコピーされます。これにより、req.Bodyや他のフィールドは元のreqと同じポインタを指すことになりますが、http.Request構造体自体の内容は複製されます。reqSend.URL = new(url.URL):http.RequestのURLフィールドは*url.URL型(ポインタ)であるため、*reqSend = *reqだけではreqSend.URLは元のreq.URLと同じurl.URLオブジェクトを指してしまいます。これを防ぐため、reqSend.URL用に新しいurl.URLオブジェクトを割り当てます。*reqSend.URL = *req.URL: 新しく割り当てたreqSend.URLに、元のreq.URLの内容をシャローコピーします。これにより、reqSend.URLは元のreq.URLとは異なるurl.URLオブジェクトを指し、その内容が複製されます。reqSend.URL.Scheme = "http": ここで、コピーされたreqSendのURL.Schemeのみを "http" に変更します。元のreqオブジェクトは一切変更されません。
t.RoundTrip(reqSend)の呼び出し: 最後に、http.TransportのRoundTripメソッドを呼び出す際に、元のreqではなく、安全にスキームが変更されたreqSendオブジェクトを渡します。
この一連のコピー処理により、DumpRequestOut 関数は、元の http.Request オブジェクトの整合性を保ちつつ、ダンプ処理に必要なスキーム変更を安全に行うことができるようになりました。
関連リンク
- Go言語
net/httpパッケージのドキュメント: https://pkg.go.dev/net/http - Go言語
net/http/httputilパッケージのドキュメント: https://pkg.go.dev/net/http/httputil - Go言語
net/urlパッケージのドキュメント: https://pkg.go.dev/net/url - Go言語のコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/5709054
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- Go言語のコードレビューシステム (Gerrit) の変更リスト (上記に記載)