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

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

このコミットは、Go言語の標準ライブラリであるnet/httpパッケージにおける、HTTPトレーラー(Trailer)の読み込み時のエラーハンドリングの改善を目的としています。具体的には、トレーラーの読み込み中に発生しうるEOF(End Of File)エラーやその他の読み込みエラーに対するチェックを追加し、堅牢性を向上させています。また、関連するテストケースも追加されています。

コミット

commit 56bcef02fca3299bfd162fea1c647754cd071714
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Nov 13 22:38:25 2012 -0800

    net/http: add missing error checking reading trailers
    
    This is a simplified version of earlier versions of this CL
    and now only fixes obviously incorrect things, without
    changing the locking on bodyEOFReader.
    
    I'd like to see if this is sufficient before changing the
    locking.
    
    Update #4191
    
    R=golang-dev, rsc, dave
    CC=golang-dev
    https://golang.org/cl/6739055

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

https://github.com/golang/go/commit/56bcef02fca3299bfd162fea1c647754cd071714

元コミット内容

net/http: add missing error checking reading trailers (net/http: トレーラー読み込み時の不足しているエラーチェックを追加)

このコミットは、以前の変更セット(CL: Change List)の簡略版であり、bodyEOFReaderのロック機構を変更することなく、明らかな誤りを修正することに焦点を当てています。コミットの意図としては、この修正が十分であるかを確認し、必要であれば後続の変更でロック機構の改善を検討するというものです。

変更の背景

この変更は、GoのIssue #4191に関連しています。Issue #4191は、HTTPレスポンスボディの読み込みが途中で終了した場合(例えば、サーバーが予期せず接続を閉じた場合など)に、net/httpクライアントが適切にエラーを処理しない可能性があるという問題提起でした。特に、HTTP/1.1のチャンク転送エンコーディングにおいて、ボディの終端を示すゼロ長チャンクの後に続く可能性のあるトレーラーヘッダーの処理が問題となっていました。

従来のnet/httpの実装では、トレーラーを読み込む際に、ストリームの終端(EOF)に達した場合や、予期せぬデータが来た場合に、適切なエラーハンドリングが行われていない可能性がありました。これにより、クライアントがハングアップしたり、不正な状態になったりする脆弱性が存在しました。

このコミットは、特にトレーラーの読み込み部分に焦点を当て、EOFやその他の読み込みエラーが発生した際に、より堅牢なエラーチェックを行うことで、これらの潜在的な問題を解決しようとしています。これは、HTTP通信の信頼性と安定性を向上させるための重要な修正です。

前提知識の解説

HTTP/1.1 トレーラー(Trailer)

HTTP/1.1では、メッセージボディの後にヘッダーフィールドを追加する「トレーラー」という機能があります。これは主にチャンク転送エンコーディングで使用され、メッセージボディの転送が完了した後に、メッセージ全体のハッシュ値やデジタル署名など、ボディの内容に依存するヘッダー情報を送信するために利用されます。

トレーラーは、HTTPヘッダーとは異なり、メッセージボディの後に現れるため、受信側はボディ全体を読み終えるまでトレーラーの内容を知ることができません。そのため、トレーラーを適切に処理するには、ボディの読み込みが完了した後に、ストリームから追加のヘッダー情報を読み取るロジックが必要になります。

EOF(End Of File)エラー

プログラミングにおいて、EOFは「ファイルの終端」または「ストリームの終端」を意味します。ネットワーク通信においては、接続が閉じられたり、送信側がデータの送信を完了したりした場合に、受信側がEOFを受け取ります。

Go言語のioパッケージでは、io.EOFというエラーが定義されており、これは読み込み操作がストリームの終端に達したことを示すために返されます。ネットワークプログラミングでは、io.EOFを適切に処理することが非常に重要です。予期せぬEOFは、通信プロトコルの違反や、相手側からの予期せぬ切断を示す場合があり、これらを適切にエラーとして扱うことで、アプリケーションの堅牢性を高めることができます。

bufio.ReaderPeekメソッド

bufio.Readerは、Go言語でバッファリングされたI/Oを提供する構造体です。これにより、少量のデータを頻繁に読み書きする際のパフォーマンスが向上します。

Peek(n int)メソッドは、bufio.Readerの重要な機能の一つです。これは、実際にバッファからデータを消費することなく、次のnバイトを覗き見ることができます。Peekは、次に読み込むデータの内容に基づいて、その後の処理を決定する際に非常に便利です。例えば、特定のバイトシーケンスが続くかどうかを確認するために使用されます。

Peekは、要求されたバイト数(n)がバッファに存在しない場合、または基になるリーダーから読み込めない場合にエラーを返します。特に、ストリームの終端に達した場合、io.EOFを返す可能性があります。このコミットでは、Peekの戻り値としてエラーを適切にチェックすることが重要になっています。

技術的詳細

このコミットの主要な変更点は、src/pkg/net/http/transfer.go内の(*body).readTrailer()メソッドにおけるエラーハンドリングの強化です。

readTrailer()メソッドは、HTTPレスポンスボディの読み込みが完了した後、トレーラーヘッダーが存在するかどうかを確認し、存在する場合はそれを読み込む役割を担っています。

変更前は、b.r.Peek(2)の戻り値であるエラーが無視されていました。Peekは、バッファに十分なデータがない場合や、基になるリーダーがEOFを返した場合にエラーを返します。このエラーを無視すると、トレーラーの終端を示すCRLF(\r\n)シーケンスを期待しているにもかかわらず、実際にはストリームが終了している場合に、不正な動作を引き起こす可能性がありました。

このコミットでは、以下の点が修正されています。

  1. Peekのエラーチェック: b.r.Peek(2)の戻り値としてエラーを受け取り、そのエラーがnilでない場合は、すぐにそのエラーを返すように変更されました。これにより、トレーラーの読み込みを開始する前に、ストリームの状態が不正である場合に早期にエラーを検出できます。
  2. len(buf) < 2のチェック: Peekがエラーを返さなかった場合でも、返されたバッファの長さが2未満である場合(つまり、CRLFシーケンスを完全に読み込めない場合)には、新しいエラーerrTrailerEOFを返すようにしました。これは、部分的なデータしか利用できない状況を明示的にエラーとして扱います。
  3. textproto.NewReader(b.r).ReadMIMEHeader()のEOFチェック: トレーラーヘッダー自体を読み込むReadMIMEHeader()io.EOFを返した場合、それをerrTrailerEOFにラップして返すように変更されました。これは、トレーラーの途中でストリームが終了した場合も、より具体的なエラーとして報告するためです。

これらの変更により、readTrailer()は、トレーラーの読み込み中に発生する可能性のある様々なエラーシナリオ(特にEOF関連)に対して、より堅牢に対応できるようになりました。

また、src/pkg/net/http/client_test.goでは、c.Get(ts.URL)の呼び出し後にエラーチェックが追加され、HTTPクライアントのテストがより堅牢になっています。

さらに、src/pkg/net/http/transfer_test.goという新しいテストファイルが追加され、TestBodyReadBadTrailerというテストケースが導入されました。このテストは、ボディの読み込み中にトレーラーが不正な形式であるか、または予期せぬEOFが発生した場合に、bodyリーダーが適切にエラーを返すことを検証します。これは、今回の修正が意図通りに機能していることを確認するための重要なテストです。

src/pkg/net/http/transport.goでは、responseIsKeepAlive関数が削除されています。これは、以前のコミットでkeep-aliveのロジックが変更されたため、不要になったものと考えられます。また、hasBodyに関するコメントが追加されており、HEADリクエストの扱いに関する潜在的な矛盾が指摘されていますが、これはこのコミットの直接的な修正範囲外です。

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

src/pkg/net/http/transfer.go

--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -567,14 +567,22 @@ func seeUpcomingDoubleCRLF(r *bufio.Reader) bool {
 	return false
 }
 
+var errTrailerEOF = errors.New("http: unexpected EOF reading trailer")
+
 func (b *body) readTrailer() error {
 	// The common case, since nobody uses trailers.
-	buf, _ := b.r.Peek(2)
+	buf, err := b.r.Peek(2)
 	if bytes.Equal(buf, singleCRLF) {
 		b.r.ReadByte()
 		b.r.ReadByte()
 		return nil
 	}
+	if len(buf) < 2 {
+		return errTrailerEOF
+	}
+	if err != nil {
+		return err
+	}
 
 	// Make sure there's a header terminator coming up, to prevent
 	// a DoS with an unbounded size Trailer.  It's not easy to
@@ -590,6 +598,9 @@ func (b *body) readTrailer() error {
 
 	h
dr, err := textproto.NewReader(b.r).ReadMIMEHeader()
 	if err != nil {
+		if err == io.EOF {
+			return errTrailerEOF
+		}
 		return err
 	}
 	switch rr := b.hdr.(type) {

src/pkg/net/http/transfer_test.go (新規ファイル)

// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package http

import (
	"bufio"
	"strings"
	"testing"
)

func TestBodyReadBadTrailer(t *testing.T) {
	b := &body{
		Reader: strings.NewReader("foobar"),
		hdr:    true, // force reading the trailer
		r:      bufio.NewReader(strings.NewReader("")),
	}
	buf := make([]byte, 7)
	n, err := b.Read(buf[:3])
	got := string(buf[:n])
	if got != "foo" || err != nil {
		t.Fatalf(`first Read = %n (%q), %v; want 3 ("foo")`, n, got, err)
	}

	n, err = b.Read(buf[:])
	got = string(buf[:n])
	if got != "bar" || err != nil {
		t.Fatalf(`second Read = %n (%q), %v; want 3 ("bar")`, n, got, err)
	}

	n, err = b.Read(buf[:])
	got = string(buf[:n])
	if err == nil {
		t.Errorf("final Read was successful (%q), expected error from trailer read", got)
	}
}

コアとなるコードの解説

src/pkg/net/http/transfer.go の変更

readTrailer()関数は、HTTPレスポンスのボディが終了した後に、オプションで続くトレーラーヘッダーを読み込むためのものです。

  1. var errTrailerEOF = errors.New("http: unexpected EOF reading trailer"): トレーラーの読み込み中に予期せぬEOFが発生した場合に返す、新しいエラー変数を定義しています。これにより、具体的なエラーメッセージを提供し、デバッグを容易にします。

  2. buf, err := b.r.Peek(2): トレーラーの開始を示すCRLFシーケンス(\r\n)を検出するために、bufio.ReaderPeek(2)メソッドを使用して次の2バイトを覗き見ます。変更前は、このPeekが返すエラーが無視されていました。

  3. if len(buf) < 2 { return errTrailerEOF }: Peekがエラーを返さなかった場合でも、バッファに2バイト未満のデータしか存在しない場合(つまり、ストリームの終端に達しており、CRLFシーケンスが不完全である場合)には、errTrailerEOFを返します。これは、部分的なデータしか読み込めない状況を明示的にエラーとして扱います。

  4. if err != nil { return err }: Peekがエラーを返した場合、そのエラーをそのまま返します。これにより、基になるリーダーからの読み込みエラー(例えば、ネットワーク接続の切断など)が適切に伝播されるようになります。

  5. if err == io.EOF { return errTrailerEOF }: textproto.NewReader(b.r).ReadMIMEHeader()は、実際のトレーラーヘッダーを解析する部分です。この関数がio.EOFを返した場合、それはトレーラーヘッダーの途中でストリームが終了したことを意味します。この場合、汎用的なio.EOFではなく、より具体的なerrTrailerEOFを返すことで、トレーラー読み込み時の問題であることを明確にします。

これらの変更により、readTrailer()は、トレーラーの有無を判断するためのPeek操作、および実際のトレーラーヘッダーの読み込み操作の両方において、EOFやその他の読み込みエラーをより厳密にチェックし、適切なエラーを返すようになりました。これにより、HTTPクライアントが不正なトレーラーデータや予期せぬストリームの終端に遭遇した場合でも、より堅牢に動作することが期待されます。

src/pkg/net/http/transfer_test.go の新規追加

TestBodyReadBadTrailerは、bodyリーダーが不正なトレーラーデータや予期せぬEOFに遭遇した際に、正しくエラーを返すことを検証するテストです。

  • b := &body{ ... }: テスト用のbody構造体を初期化します。Readerには「foobar」というデータが設定され、hdr: trueとすることでトレーラーの読み込みが強制されます。r(内部のbufio.Reader)には空の文字列が設定されており、トレーラーを読み込もうとするとすぐにEOFに達するように意図されています。
  • n, err := b.Read(buf[:3])n, err = b.Read(buf[:]): 「foobar」というボディデータを読み込みます。
  • if err == nil { t.Errorf(...) }: 最後のRead呼び出しで、トレーラーの読み込みが試みられ、その際にエラーが発生することを期待しています。もしエラーが発生しなかった場合、テストは失敗します。

このテストは、readTrailer()関数が、トレーラーの読み込み中に予期せぬEOFに遭遇した場合に、適切にエラーを返すことを保証します。

関連リンク

参考にした情報源リンク