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

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

このコミットは、Go言語の標準ライブラリである net/http/httputil パッケージにおける DumpRequestOut 関数のバグ修正に関するものです。具体的には、HTTPリクエストの Content-Length ヘッダが設定されているにもかかわらず、リクエストボディをダンプしない設定(body パラメータが false)で DumpRequestOut を呼び出した際に発生する問題を解決します。

コミット

commit 1428045469302d81a6bc19ae9f1dd1e2905ea855
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Oct 29 14:06:11 2013 -0700

    net/http/httputil: fix DumpRequestOut with ContentLength & false body param
    
    Fixes #6471
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/14920050

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

https://github.com/golang/go/commit/1428045469302d81a6bc19ae9f1dd1e2905ea855

元コミット内容

net/http/httputil: fix DumpRequestOut with ContentLength & false body param

このコミットは、DumpRequestOut 関数が Content-Length ヘッダを持つリクエストを処理する際に、ボディをダンプしない設定(body パラメータが false)であった場合に発生する問題を修正します。

変更の背景

net/http/httputil パッケージは、HTTPリクエストやレスポンスをデバッグ目的でダンプ(文字列化)するためのユーティリティを提供します。DumpRequestOut 関数は、http.Transport が追加するヘッダ(例: User-Agent)を含む、クライアントから送信される形式のリクエストをダンプするために使用されます。

このコミット以前の DumpRequestOut の実装には、特定のシナリオで問題がありました。HTTPリクエストが Content-Length ヘッダ(ボディの長さを指定する)を持っているにもかかわらず、DumpRequestOutbody 引数が false に設定されている場合、つまりリクエストボディの内容自体はダンプしたくない場合に問題が発生しました。

Goの net/http パッケージ内部では、リクエストをワイヤー形式に変換する際に、Content-Length が設定されている場合はリクエストボディ (req.Body) からそのバイト数だけを読み取ろうとします。しかし、DumpRequestOutbodyfalse に設定されていると、req.Bodynil に設定されていました。nilio.Reader から読み取ろうとすると、予期せぬエラーやパニックが発生する可能性がありました。

このバグは、Issue #6471 で報告されたものと関連していると考えられます。ユーザーが Content-Length を持つリクエストをダンプしようとした際に、期待通りの出力が得られない、またはエラーが発生するといった挙動が観測された可能性があります。

前提知識の解説

  • net/http パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。
  • net/http/httputil パッケージ: net/http パッケージのユーティリティ機能を提供し、特にHTTPリクエスト/レスポンスのダンプやリバースプロキシの実装に利用されます。
  • http.Request 構造体: HTTPリクエストを表すGoの構造体です。
    • Body: リクエストボディを表す io.ReadCloser インターフェース。
    • ContentLength: リクエストボディの長さをバイト単位で示す int64 型のフィールド。
  • io.Reader インターフェース: データを読み取るための基本的なインターフェースで、Read([]byte) (n int, err error) メソッドを持ちます。
  • ioutil.NopCloser: io.Readerio.ReadCloser に変換するユーティリティ関数。Close メソッドは何もしません。
  • io.LimitReader: 指定されたバイト数までしか読み取らない io.Reader を作成する関数。
  • HTTP Content-Length ヘッダ: HTTPメッセージのエンティティボディのサイズをオクテット単位で示すヘッダです。このヘッダが存在する場合、受信側は指定されたバイト数だけボディを読み取ることを期待します。
  • HTTPリクエストのワイヤー形式: HTTPリクエストがネットワーク上を流れる際のバイト列の形式。ヘッダとボディが特定のフォーマットで連結されます。

技術的詳細

このコミットの主要な変更点は、DumpRequestOut 関数が req.ContentLength がゼロでなく、かつ body パラメータが false の場合に、ダミーのボディを一時的に設定するようになったことです。

以前の実装では、bodyfalse の場合、req.Body は単純に nil に設定されていました。しかし、Content-Length が設定されている場合、net/http の内部処理(特にリクエストをワイヤー形式に変換する部分)は、req.Body から Content-Length で指定されたバイト数を読み取ろうとします。req.Bodynil だと、この読み取り操作が失敗します。

新しい実装では、この問題を回避するために以下の手順を踏みます。

  1. bodyfalse で、かつ req.ContentLength がゼロでない場合、ダミーの io.Reader を作成します。
  2. このダミーのリーダーは、neverEnding という新しい型で実装されており、常に指定されたバイト(この場合は 'x')を返すように設計されています。
  3. io.LimitReader を使用して、この neverEnding リーダーを req.ContentLength で指定されたバイト数に制限します。
  4. ioutil.NopCloser を使用して、この制限されたリーダーを io.ReadCloser にラップし、一時的に req.Body に設定します。これにより、内部のダンプ処理が Content-Length に従ってボディを「読み取る」ことが可能になります。
  5. dummyBody というフラグを true に設定し、ダミーボディが使用されたことを記録します。
  6. ダンプ処理が完了した後、dump されたバイト列からダミーボディの内容を削除します。これは、HTTPヘッダとボディの区切りである \r\n\r\n を探し、それ以降のバイトを切り捨てることで行われます。これにより、出力されるダンプにはヘッダのみが含まれ、ダミーボディの内容は含まれません。

このアプローチにより、DumpRequestOutContent-Length ヘッダを正しく含んだリクエストヘッダを生成しつつ、実際にボディの内容をダンプすることなく、内部的なボディ読み取りの要件を満たすことができます。

テストコード (dump_test.go) では、Content-Length6 でボディが "abcdef" であるPOSTリクエストを定義し、DumpRequestOutNoBody: true (つまり body=false) で呼び出す新しいテストケースが追加されています。このテストは、出力されるダンプが Content-Length: 6 ヘッダを含みつつ、ボディの内容を含まないことを検証します。

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

src/pkg/net/http/httputil/dump.go

type neverEnding byte

func (b neverEnding) Read(p []byte) (n int, err error) {
	for i := range p {
		p[i] = byte(b)
	}
	return len(p), nil
}

 func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
 	save := req.Body
+	dummyBody := false
  	if !body || req.Body == nil {
  		req.Body = nil
+		if req.ContentLength != 0 {
+			req.Body = ioutil.NopCloser(io.LimitReader(neverEnding('x'), req.ContentLength))
+			dummyBody = true
+		}
  	} else {
  		var err error
  		save, req.Body, err = drainBody(req.Body)
@@ -99,7 +113,19 @@ func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
  	if err != nil {
  		return nil, err
  	}
-	return buf.Bytes(), nil
+	dump := buf.Bytes()
+
+	// If we used a dummy body above, remove it now.
+	// TODO: if the req.ContentLength is large, we allocate memory
+	// unnecessarily just to slice it off here.  But this is just
+	// a debug function, so this is acceptable for now. We could
+	// discard the body earlier if this matters.
+	if dummyBody {
+		if i := bytes.Index(dump, []byte("\r\n\r\n")); i >= 0 {
+			dump = dump[:i+4]
+		}
+	}
+	return dump, nil
 }

src/pkg/net/http/httputil/dump_test.go

 var dumpTests = []dumpTest{
@@ -83,6 +84,31 @@ var dumpTests = []dumpTest{
 			"User-Agent: Go 1.1 package http\r\n" +\
 			"Accept-Encoding: gzip\r\n\r\n",
 	},\
+
+	// Request with Body, but Dump requested without it.
+	{
+		Req: http.Request{
+			Method: "POST",
+			URL: &url.URL{
+				Scheme: "http",
+				Host:   "post.tld",
+				Path:   "/",
+			},
+			ContentLength: 6,
+			ProtoMajor:    1,
+			ProtoMinor:    1,
+		},
+
+		Body: []byte("abcdef"),
+
+		WantDumpOut: "POST / HTTP/1.1\r\n" +\
+			"Host: post.tld\r\n" +\
+			"User-Agent: Go 1.1 package http\r\n" +\
+			"Content-Length: 6\r\n" +\
+			"Accept-Encoding: gzip\r\n\r\n",
+
+		NoBody: true,
+	},
 }
 
 func TestDumpRequest(t *testing.T) {
@@ -105,7 +131,7 @@ func TestDumpRequest(t *testing.T) {
 
 		if tt.WantDump != "" {
 			setBody()
-			dump, err := DumpRequest(&tt.Req, true)
+			dump, err := DumpRequest(&tt.Req, !tt.NoBody)
 			if err != nil {
 				t.Errorf("DumpRequest #%d: %s", i, err)
 				continue
@@ -118,7 +144,7 @@ func TestDumpRequest(t *testing.T) {
 
 		if tt.WantDumpOut != "" {
 			setBody()
-			dump, err := DumpRequestOut(&tt.Req, true)
+			dump, err := DumpRequestOut(&tt.Req, !tt.NoBody)
 			if err != nil {
 				t.Errorf("DumpRequestOut #%d: %s", i, err)
 				continue

コアとなるコードの解説

neverEnding 型と Read メソッド

type neverEnding byte

func (b neverEnding) Read(p []byte) (n int, err error) {
	for i := range p {
		p[i] = byte(b)
	}
	return len(p), nil
}

この新しい型 neverEnding は、io.Reader インターフェースを満たすように定義されています。その Read メソッドは、渡されたバイトスライス p を、neverEnding 型のインスタンスが持つバイト値(この場合は 'x')で埋め尽くします。これは、io.LimitReader と組み合わせて、指定された長さのダミーボディを生成するために使用されます。これにより、net/http の内部処理がボディを読み取ろうとした際に、実際にデータが提供されるように見せかけることができます。

DumpRequestOut 関数の変更

 func DumpRequestOut(req *http.Request, body bool) ([]byte, error) {
 	save := req.Body
+	dummyBody := false
  	if !body || req.Body == nil {
  		req.Body = nil
+		if req.ContentLength != 0 {
+			req.Body = ioutil.NopCloser(io.LimitReader(neverEnding('x'), req.ContentLength))
+			dummyBody = true
+		}
  	} else {
  		var err error
  		save, req.Body, err = drainBody(req.Body)

この部分が修正の核心です。

  • dummyBody という新しいブール変数が導入され、ダミーボディが使用されたかどうかを追跡します。
  • if !body || req.Body == nil の条件ブロック内で、req.ContentLength != 0 という追加の条件がチェックされます。
  • もし ContentLength がゼロでなければ、neverEnding('x') を基にした io.LimitReader が作成され、req.ContentLength で指定されたバイト数に制限されます。
  • このリーダーは ioutil.NopCloser でラップされ、一時的に req.Body に割り当てられます。これにより、Content-Length が存在するにもかかわらず req.Bodynil であることによる問題を回避します。
  • dummyBodytrue に設定されます。
 	if err != nil {
 		return nil, err
 	}
-	return buf.Bytes(), nil
+	dump := buf.Bytes()
+
+	// If we used a dummy body above, remove it now.
+	// TODO: if the req.ContentLength is large, we allocate memory
+	// unnecessarily just to slice it off here.  But this is just
+	// a debug function, so this is acceptable for now. We could
+	// discard the body earlier if this matters.
+	if dummyBody {
+		if i := bytes.Index(dump, []byte("\r\n\r\n")); i >= 0 {
+			dump = dump[:i+4]
+		}
+	}
+	return dump, nil
 }

ダンプ処理が完了した後、dummyBodytrue であれば、生成されたダンプからダミーボディの内容を削除します。

  • bytes.Index(dump, []byte("\r\n\r\n")) を使用して、HTTPヘッダとボディの区切りである空行 (\r\n\r\n) のインデックスを探します。
  • インデックスが見つかった場合、ダンプされたバイト列をそのインデックスの4バイト先(\r\n\r\n の直後)で切り詰めます。これにより、ヘッダのみが残り、ダミーボディとして追加された 'x' のシーケンスは出力から除外されます。
  • コメントにあるように、この方法は大きな Content-Length の場合に不要なメモリ割り当てが発生する可能性を指摘していますが、デバッグ関数であるため許容範囲とされています。

テストコードの変更

+	// Request with Body, but Dump requested without it.
+	{
+		Req: http.Request{
+			Method: "POST",
+			URL: &url.URL{
+				Scheme: "http",
+				Host:   "post.tld",
+				Path:   "/",
+			},
+			ContentLength: 6,
+			ProtoMajor:    1,
+			ProtoMinor:    1,
+		},
+
+		Body: []byte("abcdef"),
+
+		WantDumpOut: "POST / HTTP/1.1\r\n" +\
+			"Host: post.tld\r\n" +\
+			"User-Agent: Go 1.1 package http\r\n" +\
+			"Content-Length: 6\r\n" +\
+			"Accept-Encoding: gzip\r\n\r\n",
+
+		NoBody: true,
+	},

新しいテストケースが dumpTests スライスに追加されました。このテストケースは、ContentLength6 に設定されたPOSTリクエストをシミュレートし、NoBody: true を設定することで、DumpRequestOut がボディをダンプしないように指示します。WantDumpOut は、Content-Length: 6 ヘッダが含まれるが、ボディの内容は含まれないことを期待する出力文字列を定義しています。

 		if tt.WantDump != "" {
 			setBody()
-			dump, err := DumpRequest(&tt.Req, true)
+			dump, err := DumpRequest(&tt.Req, !tt.NoBody)
 			if err != nil {
 				t.Errorf("DumpRequest #%d: %s", i, err)
 				continue
@@ -118,7 +144,7 @@ func TestDumpRequest(t *testing.T) {
 
 		if tt.WantDumpOut != "" {
 			setBody()
-			dump, err := DumpRequestOut(&tt.Req, true)
+			dump, err := DumpRequestOut(&tt.Req, !tt.NoBody)
 			if err != nil {
 				t.Errorf("DumpRequestOut #%d: %s", i, err)
 				continue

既存のテストループ内で DumpRequestDumpRequestOut を呼び出す際に、body 引数に !tt.NoBody が渡されるように変更されました。これにより、新しく追加された NoBody フィールドを使って、各テストケースでボディをダンプするかどうかを制御できるようになります。

関連リンク

  • Go Issue #6471: このコミットが修正したとされる問題のトラッキング。ただし、直接的な検索結果は見つかりませんでした。
  • Go Gerrit Change 14920050: このコミットに対応するGerritの変更履歴。

参考にした情報源リンク