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

[インデックス 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 の内容をコピーします。そして、この reqSendURL.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" に戻すようにしていました。

このアプローチには、以下の問題がありました。

  1. 競合状態(Race Condition)の可能性: req オブジェクトが複数のゴルーチン(goroutine)間で共有されている場合、または DumpRequestOut が呼び出されている間に他のコードが req オブジェクトにアクセスする可能性がある場合、req.URL.Scheme が一時的に "http" になっている状態を他のゴルーチンが観測してしまう可能性があります。これにより、予期しない動作やバグが発生する競合状態が生じます。特に、RoundTrip の処理中に他のゴルーチンが req.URL.Scheme を参照した場合、誤ったスキームが読み取られるリスクがありました。
  2. 副作用: DumpRequestOut はリクエストをダンプするユーティリティ関数であり、本来は元のリクエストオブジェクトに副作用を与えるべきではありません。しかし、req.URL.Scheme を直接変更することは、この原則に反していました。

このコミットは、これらの問題を解決し、DumpRequestOut 関数がより安全で予測可能な動作をするようにするために行われました。コミットメッセージにある「It's still racy in that it mutates req.Body, though. shrug」という記述は、req.Body の変更(ダンプのために読み取られ、その後再構築される)という別の副作用は残るものの、URL.Scheme に関する競合状態は解消されたことを示しています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の net/http パッケージに関する知識が必要です。

  1. http.Request 構造体:
    • HTTPリクエストを表すGoの構造体です。
    • URL フィールドを持ち、これは *url.URL 型です。
    • Body フィールドを持ち、これは io.ReadCloser 型で、リクエストボディの読み取りに使用されます。
  2. url.URL 構造体:
    • URLを解析して表現する構造体です。
    • Scheme フィールドを持ち、URLのスキーム(例: "http", "https")を文字列で保持します。
  3. http.TransportRoundTripper インターフェース:
    • http.Transport は、HTTPリクエストを送信し、HTTPレスポンスを受信するメカニズムを実装する型です。
    • RoundTripper インターフェースは、RoundTrip(*Request) (*Response, error) メソッドを定義しており、単一のHTTPトランザクションを実行します。http.Transport はこのインターフェースの実装の一つです。
    • DumpRequestOut 関数は、リクエストを実際にネットワークに送信する代わりに、http.TransportRoundTrip メソッドを模倣してリクエストのワイヤーフォーマットを生成します。
  4. 競合状態(Race Condition):
    • 複数のゴルーチンが共有リソース(この場合は http.Request オブジェクトの URL.Scheme フィールド)に同時にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生するプログラミング上のバグです。
    • 操作の順序が非決定論的であるため、プログラムの実行ごとに結果が変わる可能性があります。
  5. defer ステートメント:
    • Go言語のキーワードで、defer に続く関数呼び出しを、その関数がリターンする直前に実行するようにスケジュールします。
    • リソースの解放やクリーンアップによく使用されますが、このコミットのケースのように、共有リソースの一時的な変更とその復元に使用すると、競合状態を引き起こす可能性があります。

技術的詳細

DumpRequestOut 関数は、与えられた http.Request オブジェクトをバイト列に変換します。このプロセスでは、http.TransportRoundTrip メソッドを内部的に呼び出すことで、リクエストがネットワーク上でどのように見えるかをシミュレートします。

問題は、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 {

コアとなるコードの解説

  1. import "net/url" の追加: url.URL 構造体を操作するために、net/url パッケージが新しくインポートされました。
  2. reqSend := req の導入: まず、reqSend という新しい *http.Request 型の変数を宣言し、元の req を代入します。これにより、デフォルトでは元のリクエストが使われます。
  3. 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.RequestURL フィールドは *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": ここで、コピーされた reqSendURL.Scheme のみを "http" に変更します。元の req オブジェクトは一切変更されません。
  4. t.RoundTrip(reqSend) の呼び出し: 最後に、http.TransportRoundTrip メソッドを呼び出す際に、元の req ではなく、安全にスキームが変更された reqSend オブジェクトを渡します。

この一連のコピー処理により、DumpRequestOut 関数は、元の http.Request オブジェクトの整合性を保ちつつ、ダンプ処理に必要なスキーム変更を安全に行うことができるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • Go言語のコードレビューシステム (Gerrit) の変更リスト (上記に記載)