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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおいて、HTTPリクエストにおける「Trailer」(トレーラーヘッダー)のサポートを追加するものです。特に、チャンク転送エンコーディングを使用するHTTPリクエストにおいて、ボディの読み込み完了後にトレーラーヘッダーを適切に処理できるようになります。

コミット

commit b14ee23f9b85b6c838207ccc2d67287fb0e56bb4
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Nov 4 09:17:46 2011 -0700

    http: support Trailers in ReadRequest
    
    Available after closing Request.Body.
    
    R=adg, rsc
    CC=golang-dev
    https://golang.org/cl/5348041

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

https://github.com/golang/go/commit/b14ee23f9b85b6c838207ccc2d67287fb0e56bb4

元コミット内容

http: support Trailers in ReadRequest (HTTP: ReadRequestにおけるトレーラーのサポート)

Available after closing Request.Body. (Request.Bodyをクローズした後に利用可能)

変更の背景

HTTP/1.1では、メッセージボディの転送エンコーディングとして「チャンク転送エンコーディング(Chunked Transfer Encoding)」が定義されています。これは、メッセージボディのサイズが事前に不明な場合や、動的に生成される場合に特に有用です。チャンク転送エンコーディングでは、メッセージボディが複数の「チャンク」に分割され、各チャンクは自身のサイズ情報と共に送信されます。メッセージボディの終端は、サイズが0のチャンクで示されます。

このチャンク転送エンコーディングの仕様には、オプションで「Trailer」(トレーラーヘッダー)を含めることが可能です。トレーラーヘッダーは、メッセージボディの、つまり最後の0バイトチャンクの後に続くヘッダーフィールドです。これは、メッセージボディの生成が完了するまで計算できないようなメタデータ(例: コンテンツのハッシュ値、デジタル署名など)を送信するために使用されます。

このコミット以前のGoのnet/httpパッケージでは、HTTPリクエストのトレーラーヘッダーを適切にパースし、アプリケーションからアクセスできるようにする機能が不足していました。そのため、トレーラーヘッダーを利用するHTTPリクエストをGoのサーバーが受信した場合、その情報が失われる可能性がありました。このコミットは、この不足を解消し、Request.Bodyが完全に読み込まれた後(またはクローズされた後)にトレーラーヘッダーにアクセスできるようにすることを目的としています。

前提知識の解説

HTTP/1.1 チャンク転送エンコーディング (Chunked Transfer Encoding)

HTTP/1.1のメッセージボディ転送方法の一つで、Transfer-Encoding: chunked ヘッダーによって示されます。 特徴は以下の通りです。

  • 動的なコンテンツサイズ: 送信するメッセージボディのサイズを事前に知る必要がありません。
  • チャンク: メッセージボディは、サイズを示す行とデータ本体からなる複数の「チャンク」に分割されます。
  • 終端: 最後のチャンクはサイズが0であり、その後にオプションでトレーラーヘッダーが続きます。
  • トレーラーヘッダー: メッセージボディの後に続くヘッダーフィールド。メッセージボディの生成が完了するまで決定できない情報(例: Content-MD5Expiresなど)を送信するために使用されます。

HTTP Trailer (トレーラーヘッダー)

HTTPメッセージのボディの後に続くヘッダーフィールドのセットです。通常、HTTPヘッダーはメッセージボディの前に来ますが、チャンク転送エンコーディングの場合に限り、ボディの後に特定のヘッダーを配置できます。これは、メッセージボディ全体が生成されるまでその値が計算できないようなヘッダー(例: ボディのハッシュ値)を送信する際に特に有用です。

io.EOF

Go言語のioパッケージで定義されているエラー定数です。これは、入力の終わりに達したことを示します。Readメソッドがこれ以上データを読み込めない場合に返されます。このコミットでは、Request.Bodyの読み込みがio.EOFに達したときに、トレーラーヘッダーの読み込みを開始するトリガーとして利用されています。

bufio.Reader.Peek

bufioパッケージのReader型が持つメソッドで、入力ストリームから実際に読み込むことなく、次のNバイトを覗き見ることができます。これは、ストリームの先頭を検査して、次にどのようなデータが来るかを判断するのに役立ちます。このコミットでは、トレーラーヘッダーの終端を示すCRLF(\r\n)や、トレーラーヘッダーの開始を判断するために使用され、特にサービス拒否(DoS)攻撃を防ぐための安全策として利用されています。

net/textproto.NewReader().ReadMIMEHeader()

net/textprotoパッケージは、MIMEスタイルのヘッダーやテキストプロトコルを解析するための機能を提供します。NewReaderbufio.Readerをラップし、ReadMIMEHeaderはHTTPヘッダーのようなキーと値のペアのブロックを読み込み、map[string][]string形式で返します。このコミットでは、トレーラーヘッダーをパースするためにこの関数が利用されています。

技術的詳細

このコミットの主要な変更点は、src/pkg/net/http/transfer.goファイルに集中しています。

  1. body.Readメソッドの変更:

    • body構造体は、HTTPメッセージボディの読み込みを管理します。
    • Readメソッドは、ボディのデータを読み込む際に、io.EOF(ファイルの終端)に達したかどうかをチェックするようになりました。
    • io.EOFに達し、かつb.hdr(ヘッダー情報)が存在する場合、b.readTrailer()メソッドが呼び出されます。これにより、ボディの読み込みが完了した直後にトレーラーヘッダーのパースが試みられます。
  2. body.readTrailer()メソッドの追加:

    • この新しいメソッドは、トレーラーヘッダーを読み込むための中心的なロジックを含んでいます。
    • 空のトレーラーの処理: まず、次の2バイトが\r\n(単一のCRLF)であるかをPeekで確認します。これは、トレーラーヘッダーが存在しない場合の一般的なケースです。存在しない場合は、単にCRLFを読み飛ばして終了します。
    • DoS攻撃対策: トレーラーヘッダーのサイズが無制限になることによるサービス拒否攻撃を防ぐため、seeUpcomingDoubleCRLF関数が導入されました。この関数は、bufio.Readerの内部バッファサイズ(通常4KB)の範囲内で、ヘッダーの終端を示す\r\n\r\n(二重CRLF)が存在するかをPeekを繰り返して確認します。これにより、非常に長いトレーラーヘッダーが送られてきた場合に、メモリを大量に消費する前にエラーを返すことができます。
    • トレーラーヘッダーのパース: textproto.NewReader(b.r).ReadMIMEHeader()を使用して、実際のトレーラーヘッダーを読み込みます。この関数は、MIMEヘッダーの形式で記述されたキーと値のペアをパースし、map[string][]stringとして返します。
    • Request.Trailerへの設定: パースされたトレーラーヘッダーは、RequestまたはResponseオブジェクトのTrailerフィールドに設定されます。
  3. body.Close()メソッドの変更:

    • body.Close()メソッドは、ボディが完全に消費されていない場合に、残りのボディをioutil.Discardにコピーして読み捨てるようになりました。この処理中にbody.Readが呼び出され、最終的にio.EOFに達することでreadTrailer()がトリガーされ、トレーラーヘッダーが読み込まれるようになります。
    • 以前存在したトレーラー読み込みに関するTODOコメントが削除され、このコミットでその機能が実装されたことを示しています。
  4. src/pkg/net/http/request.goの変更:

    • Request構造体のTrailerフィールドに関するコメントが更新され、「サーバーリクエストの場合、TrailerBodyがクローズまたは完全に消費された後にのみ設定される」という重要な情報が追加されました。
    • chunkedReader.beginChunk()から、トレーラーのCRLFを処理する古いループが削除されました。これは、新しいbody.readTrailer()メソッドにトレーラー処理の責任が移管されたためです。
  5. src/pkg/net/http/readrequest_test.goの変更:

    • reqTest構造体にTrailer Headerフィールドが追加され、テストケースでトレーラーヘッダーの期待値を指定できるようになりました。
    • チャンク転送エンコーディングとトレーラーヘッダーを含む新しいテストケースが追加されました。これにより、トレーラーヘッダーが正しくパースされ、Request.Trailerに設定されることが検証されます。
    • テストの検証ロジックにreflect.DeepEqualが使用され、パースされたトレーラーヘッダーが期待値と一致するかどうかが厳密にチェックされるようになりました。

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

src/pkg/net/http/transfer.go

// body.Read メソッドの変更
func (b *body) Read(p []byte) (n int, err error) {
	if b.closed {
		return 0, ErrBodyReadAfterClose
	}
	n, err = b.Reader.Read(p)

	// Read the final trailer once we hit EOF.
	if err == io.EOF && b.hdr != nil {
		err = b.readTrailer() // EOF時にreadTrailer()を呼び出す
		b.hdr = nil
	}
	return n, err
}

// 新しいヘルパー関数
var (
	singleCRLF = []byte("\r\n")
	doubleCRLF = []byte("\r\n\r\n")
)

// DoS対策のためのPeek関数
func seeUpcomingDoubleCRLF(r *bufio.Reader) bool {
	for peekSize := 4; ; peekSize++ {
		buf, err := r.Peek(peekSize)
		if bytes.HasSuffix(buf, doubleCRLF) {
			return true
		}
		if err != nil {
			break
		}
	}
	return false
}

// トレーラーヘッダーを読み込む新しいメソッド
func (b *body) readTrailer() error {
	// The common case, since nobody uses trailers.
	buf, _ := b.r.Peek(2)
	if bytes.Equal(buf, singleCRLF) {
		b.r.ReadByte()
		b.r.ReadByte()
		return nil
	}

	// Make sure there's a header terminator coming up, to prevent
	// a DoS with an unbounded size Trailer.  It's not easy to
	// slip in a LimitReader here, as textproto.NewReader requires
	// a concrete *bufio.Reader.  Also, we can't get all the way
	// back up to our conn's LimitedReader that *might* be backing
	// this bufio.Reader.  Instead, a hack: we iteratively Peek up
	// to the bufio.Reader's max size, looking for a double CRLF.
	// This limits the trailer to the underlying buffer size, typically 4kB.
	if !seeUpcomingDoubleCRLF(b.r) {
		return errors.New("http: suspiciously long trailer after chunked body")
	}

	hdr, err := textproto.NewReader(b.r).ReadMIMEHeader()
	if err != nil {
		return err
	}
	switch rr := b.hdr.(type) {
	case *Request:
		rr.Trailer = Header(hdr)
	case *Response:
		rr.Trailer = Header(hdr)
	}
	return nil
}

// body.Close メソッドの変更
func (b *body) Close() error {
	if b.closed {
		return nil
	}
	b.closed = true
	if b.Reader == nil {
		return nil
	}

	// Fully consume the body, which will also lead to us reading
	// the trailer headers after the body, if present.
	if _, err := io.Copy(ioutil.Discard, b); err != nil {
		return err
	}

	return nil
}

コアとなるコードの解説

上記のコードスニペットは、Goのnet/httpパッケージにおけるHTTPトレーラーヘッダーの処理を可能にするための主要な変更を示しています。

  1. body.Readの変更:

    • bodyはHTTPメッセージボディを表現する内部構造体です。そのReadメソッドは、クライアントがボディからデータを読み取る際に呼び出されます。
    • n, err = b.Reader.Read(p)で実際のボディデータを読み込んだ後、if err == io.EOF && b.hdr != nilという条件が追加されています。これは、ボディの読み込みが終端(EOF)に達し、かつヘッダー情報(b.hdr)が存在する場合(つまり、リクエストまたはレスポンスのボディである場合)に、トレーラーヘッダーの読み込み処理を開始することを示しています。
    • err = b.readTrailer()が呼び出され、トレーラーヘッダーのパースが試みられます。この呼び出しが成功すれば、Request.TrailerまたはResponse.Trailerフィールドにトレーラーヘッダーが設定されます。
  2. singleCRLFdoubleCRLF:

    • これらは、HTTPプロトコルにおける行末(\r\n)とヘッダーブロックの終端(\r\n\r\n)を表すバイトスライスです。トレーラーヘッダーの有無や終端を効率的に検出するために使用されます。
  3. seeUpcomingDoubleCRLF関数:

    • この関数は、サービス拒否(DoS)攻撃を防ぐための重要な安全策です。
    • bufio.ReaderPeekメソッドを繰り返し使用して、入力ストリームの先読みを行います。
    • 目的は、トレーラーヘッダーの終端を示す\r\n\r\nが、bufio.Readerの内部バッファサイズ(通常4KB)の範囲内に存在するかどうかを確認することです。
    • もし、この範囲内に終端が見つからない場合、それは異常に長いトレーラーヘッダーが送られてきている可能性があり、readTrailerメソッドはエラーを返して処理を中断します。これにより、悪意のあるクライアントが無限に長いトレーラーヘッダーを送信してサーバーのメモリを枯渇させることを防ぎます。
  4. body.readTrailer()メソッド:

    • このメソッドは、トレーラーヘッダーの実際のパースロジックをカプセル化しています。
    • まず、b.r.Peek(2)で次の2バイトを覗き見し、それが\r\n(単一のCRLF)であるかどうかをチェックします。これは、トレーラーヘッダーが存在しない場合の最も一般的なケースです。存在しない場合は、単にCRLFを読み飛ばしてnilエラーを返します。
    • トレーラーヘッダーが存在する可能性がある場合、seeUpcomingDoubleCRLF(b.r)を呼び出してDoS対策のチェックを行います。
    • チェックを通過した場合、textproto.NewReader(b.r).ReadMIMEHeader()を使用して、残りの入力ストリームからMIMEヘッダー形式でトレーラーヘッダーを読み込みます。この関数は、ヘッダーのキーと値のペアをパースし、map[string][]stringとして返します。
    • 最後に、パースされたヘッダーを、b.hdrが指す*Requestまたは*ResponseオブジェクトのTrailerフィールドに設定します。
  5. body.Close()の変更:

    • body.Close()メソッドは、ボディが完全に読み込まれていない場合に、残りのデータをio.Copy(ioutil.Discard, b)によって読み捨てます。
    • この読み捨て処理中に、body.Readメソッドが繰り返し呼び出され、最終的にボディの終端に達した際に、前述のb.readTrailer()がトリガーされることになります。これにより、ボディが明示的にクローズされた場合でも、トレーラーヘッダーが確実にパースされるようになります。

これらの変更により、Goのnet/httpパッケージは、HTTP/1.1のチャンク転送エンコーディングにおけるトレーラーヘッダーを適切に処理し、アプリケーションからアクセスできるようにする堅牢なメカニズムを獲得しました。

関連リンク

参考にした情報源リンク