[インデックス 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) の変更リスト (上記に記載)