[インデックス 10171] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/textproto パッケージ内の Reader 型の readLineSlice メソッドに対する変更と、それに関連するテストの追加を含んでいます。主な目的は、HTTPヘッダーにおける長い行が原因で発生するHTTP 400エラー(Bad Request)を防止することです。これは、HTTPプロトコルにおけるヘッダーの行の長さ制限や、行の折り返し(line folding)の処理に関連する問題に対処するためのものです。
コミット
textproto: prevent long lines in HTTP headers from causing HTTP 400 responses.
This fixes the issue without an extra copy in the average case.
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f753e3facda2a9845caf7e8aed0e8a122d6b6e48
元コミット内容
commit f753e3facda2a9845caf7e8aed0e8a122d6b6e48
Author: Mike Solomon <msolo@gmail.com>
Date: Tue Nov 1 10:31:29 2011 -0700
textproto: prevent long lines in HTTP headers from causing HTTP 400 responses.
This fixes the issue without an extra copy in the average case.
R=golang-dev, ality, bradfitz
CC=golang-dev
https://golang.org/cl/5272049
---
src/pkg/net/textproto/reader.go | 18 ++++++++++++++++--
src/pkg/net/textproto/reader_test.go | 17 +++++++++++++++++
2 files changed, 33 insertions(+), 2 deletions(-)
diff --git a/src/pkg/net/textproto/reader.go b/src/pkg/net/textproto/reader.go
index ece9a99ffb..98b39276b8 100644
--- a/src/pkg/net/textproto/reader.go
+++ b/src/pkg/net/textproto/reader.go
@@ -50,8 +50,22 @@ func (r *Reader) ReadLineBytes() ([]byte, os.Error) {
func (r *Reader) readLineSlice() ([]byte, os.Error) {
r.closeDot()
- line, _, err := r.R.ReadLine()
- return line, err
+ var line []byte
+ for {
+ l, more, err := r.R.ReadLine()
+ if err != nil {
+ return nil, err
+ }
+ // Avoid the copy if the first call produced a full line.
+ if line == nil && !more {
+ return l, nil
+ }
+ line = append(line, l...)
+ if !more {
+ break
+ }
+ }
+ return line, nil
}
// ReadContinuedLine reads a possibly continued line from r,
diff --git a/src/pkg/net/textproto/reader_test.go b/src/pkg/net/textproto/reader_test.go
index 23ebc3f61e..a087e29d91 100644
--- a/src/pkg/net/textproto/reader_test.go
+++ b/src/pkg/net/textproto/reader_test.go
@@ -139,6 +139,23 @@ func TestReadMIMEHeader(t *testing.T) {
}\n }\n \n+func TestLargeReadMIMEHeader(t *testing.T) {
+\tdata := make([]byte, 16*1024)
+\tfor i := 0; i < len(data); i++ {
+\t\tdata[i] = 'x'
+\t}
+\tsdata := string(data)
+\tr := reader("Cookie: " + sdata + "\r\n\n")
+\tm, err := r.ReadMIMEHeader()
+\tif err != nil {
+\t\tt.Fatalf("ReadMIMEHeader: %v", err)
+\t}
+\tcookie := m.Get("Cookie")
+\tif cookie != sdata {
+\t\tt.Fatalf("ReadMIMEHeader: %v bytes, want %v bytes", len(cookie), len(sdata))\n+\t}
+}\n+\n type readResponseTest struct {
in string
inCode int
変更の背景
このコミットの背景には、HTTPヘッダーの処理における一般的な問題があります。HTTP/1.1の仕様(RFC 2616)では、ヘッダーフィールドの行の長さについて明確な上限は設けられていませんが、多くのHTTPサーバーやプロキシは、セキュリティやリソース保護の観点から、ヘッダーの行の長さに内部的な制限を設けています。
従来の net/textproto パッケージの readLineSlice メソッドは、基盤となる bufio.Reader の ReadLine メソッドを直接呼び出していました。bufio.Reader.ReadLine は、行がバッファに収まらない場合に more フラグを true にして部分的な行を返すことがあります。しかし、readLineSlice はこの more フラグを適切に処理せず、最初の ReadLine の呼び出しで返された内容をそのまま行として扱っていました。
この挙動は、特に非常に長いHTTPヘッダー(例えば、大きなCookieヘッダーなど)が送信された場合に問題を引き起こしました。もしヘッダーの1行が bufio.Reader の内部バッファサイズを超えた場合、ReadLine はその行を複数回に分けて返す可能性があります。しかし、readLineSlice が最初の部分的な行しか読み取らないため、ヘッダーが不完全に解析され、結果としてHTTP 400 Bad Requestエラーがクライアントに返される可能性がありました。
このコミットは、このような状況下でHTTP 400エラーが発生するのを防ぐことを目的としています。具体的には、readLineSlice が ReadLine から返される more フラグをチェックし、行が完全に読み取られるまで繰り返し読み込みを行うように修正されています。これにより、長いヘッダー行も正しく結合され、完全な形で処理されるようになります。
また、コミットメッセージには「This fixes the issue without an extra copy in the average case.」とあります。これは、一般的なケース(行が一度の ReadLine 呼び出しで完全に読み取れる場合)では、余分なメモリコピーが発生しないように最適化されていることを示唆しています。
前提知識の解説
HTTPヘッダーと行の折り返し(Line Folding)
HTTP/1.1のヘッダーフィールドは、フィールド名: フィールド値 の形式で構成されます。歴史的に、HTTP/1.0やそれ以前のプロトコルでは、ヘッダーフィールドの値を複数行にわたって記述するために「行の折り返し(Line Folding)」というメカニズムが使用されていました。これは、行の途中にスペースまたはタブ文字が続く改行(CRLF)を挿入することで、論理的には1行のヘッダーを物理的に複数行に分割するものです。
しかし、RFC 7230 (HTTP/1.1 Message Syntax and Routing) のセクション 3.2.4 "Field Parsing" では、行の折り返しは非推奨とされており、HTTPメッセージパーサーはこれらを単一の連続した行として扱うべきであるとされています。現代のHTTP実装では、行の折り返しはほとんど使用されず、代わりに非常に長いヘッダー値は単一の長い行として送信されることが一般的です。
HTTP 400 Bad Request
HTTP 400 Bad Requestは、クライアントが送信したリクエストがサーバーによって理解できない、または不正であると判断された場合に返されるHTTPステータスコードです。これには様々な原因がありますが、以下のようなケースが含まれます。
- 不正な構文: HTTPプロトコルの構文規則に違反している場合。
- 無効なリクエストメッセージフレーム: Content-Lengthヘッダーと実際のボディの長さが一致しないなど。
- 不正なヘッダーフィールド: ヘッダーフィールドの値が期待される形式でない、または長すぎる場合。
本コミットの文脈では、後者の「不正なヘッダーフィールド」が問題となっていました。特に、サーバー側がヘッダーの行の長さに制限を設けている場合や、パーサーが長い行を正しく処理できない場合に、400エラーが発生しやすくなります。
bufio.Reader と ReadLine
Go言語の bufio パッケージは、バッファリングされたI/O操作を提供します。bufio.Reader は、入力ストリームからデータを効率的に読み取るための型です。
bufio.Reader の ReadLine メソッドは、入力から1行を読み取ります。このメソッドは、以下の3つの値を返します。
line []byte: 読み取られた行のバイトスライス。more bool: 行がバッファに収まらず、まだ読み取るべきデータが残っている場合にtrue。err error: 読み取り中に発生したエラー。
more が true の場合、呼び出し元は ReadLine を再度呼び出して、残りの行を読み取る必要があります。このコミットの修正前は、net/textproto/reader.go の readLineSlice がこの more フラグを適切に処理していなかったため、長い行が途中で切れてしまう問題が発生していました。
技術的詳細
このコミットは、src/pkg/net/textproto/reader.go ファイル内の Reader 型の readLineSlice メソッドのロジックを変更しています。
変更前:
func (r *Reader) readLineSlice() ([]byte, os.Error) {
r.closeDot()
line, _, err := r.R.ReadLine() // ReadLineのmoreフラグを無視
return line, err
}
変更前のコードでは、r.R.ReadLine() の戻り値のうち、more フラグ(2番目の戻り値)がアンダースコア _ で破棄されていました。これは、ReadLine が行を複数回に分けて返す可能性があるにもかかわらず、最初の呼び出しで返された部分的な行を完全な行として扱っていたことを意味します。
変更後:
func (r *Reader) readLineSlice() ([]byte, os.Error) {
r.closeDot()
var line []byte // 読み取った行を結合するためのスライス
for {
l, more, err := r.R.ReadLine() // ReadLineを繰り返し呼び出す
if err != nil {
return nil, err
}
// Avoid the copy if the first call produced a full line.
if line == nil && !more { // 最初の呼び出しで完全な行が読み取れた場合
return l, nil // コピーせずにそのまま返す
}
line = append(line, l...) // 部分的な行を結合
if !more { // 行が完全に読み取れた場合
break // ループを終了
}
}
return line, nil
}
変更後のコードでは、readLineSlice は for ループを使用して r.R.ReadLine() を繰り返し呼び出すようになりました。
var line []byteで、最終的に結合された行を保持するためのバイトスライスを宣言します。forループ内でl, more, err := r.R.ReadLine()を呼び出します。- エラーが発生した場合は、すぐにエラーを返します。
- 最適化:
if line == nil && !moreの条件は、readLineSliceが最初にReadLineを呼び出した際に、その呼び出しで完全な行が読み取れた(moreがfalse)場合の最適化です。この場合、lineスライスへの余分なコピーを避けて、lを直接返します。これは、ほとんどのHTTPヘッダー行が短く、一度の読み取りで完結する「平均的なケース」でのパフォーマンスを向上させます。 line = append(line, l...)は、ReadLineから返された部分的な行lを、lineスライスに結合します。if !moreの条件は、ReadLineがmoreフラグをfalseで返した場合、つまり行が完全に読み取られたことを示します。この時点でループをbreakします。- ループが終了すると、完全に結合された
lineスライスを返します。
この変更により、readLineSlice は、bufio.Reader.ReadLine が複数回に分けて返す可能性のある長い行も、完全に読み取り、結合して返すことができるようになりました。これにより、HTTPヘッダーの解析がより堅牢になり、長いヘッダー行によるHTTP 400エラーの発生を防ぎます。
また、src/pkg/net/textproto/reader_test.go に TestLargeReadMIMEHeader という新しいテストケースが追加されています。このテストは、16KBという非常に長いCookieヘッダーを生成し、それが ReadMIMEHeader によって正しく読み取られることを検証します。これは、修正が意図した通りに機能していることを確認するための重要なテストです。
コアとなるコードの変更箇所
src/pkg/net/textproto/reader.go
--- a/src/pkg/net/textproto/reader.go
+++ b/src/pkg/net/textproto/reader.go
@@ -50,8 +50,22 @@ func (r *Reader) ReadLineBytes() ([]byte, os.Error) {
func (r *Reader) readLineSlice() ([]byte, os.Error) {
r.closeDot()
- line, _, err := r.R.ReadLine()
- return line, err
+ var line []byte
+ for {
+ l, more, err := r.R.ReadLine()
+ if err != nil {
+ return nil, err
+ }
+ // Avoid the copy if the first call produced a full line.
+ if line == nil && !more {
+ return l, nil
+ }
+ line = append(line, l...)
+ if !more {
+ break
+ }
+ }
+ return line, nil
}
src/pkg/net/textproto/reader_test.go
--- a/src/pkg/net/textproto/reader_test.go
+++ b/src/pkg/net/textproto/reader_test.go
@@ -139,6 +139,23 @@ func TestReadMIMEHeader(t *testing.T) {
}\n }\n \n+func TestLargeReadMIMEHeader(t *testing.T) {
+\tdata := make([]byte, 16*1024)
+\tfor i := 0; i < len(data); i++ {
+\t\tdata[i] = 'x'
+\t}
+\tsdata := string(data)
+\tr := reader("Cookie: " + sdata + "\r\n\n")
+\tm, err := r.ReadMIMEHeader()
+\tif err != nil {
+\t\tt.Fatalf("ReadMIMEHeader: %v", err)
+\t}
+\tcookie := m.Get("Cookie")
+\tif cookie != sdata {
+\t\tt.Fatalf("ReadMIMEHeader: %v bytes, want %v bytes", len(cookie), len(sdata))\n+\t}
+}\n+\n type readResponseTest struct {
in string
inCode int
コアとなるコードの解説
src/pkg/net/textproto/reader.go の readLineSlice メソッド
このメソッドは、net/textproto パッケージがHTTPヘッダーなどのテキストベースのプロトコルメッセージを解析する際に、1行を読み取るための内部ヘルパー関数です。
- 変更前:
r.R.ReadLine()を一度だけ呼び出し、その結果をそのまま返していました。bufio.Reader.ReadLineが行を複数回に分けて返す可能性があることを考慮していませんでした。 - 変更後:
var line []byteで、読み取った行全体を格納するためのバイトスライスを初期化します。forループを使用して、r.R.ReadLine()を繰り返し呼び出します。l, more, err := r.R.ReadLine():lは今回読み取った部分的な行、moreはまだ行の続きがあるかを示すブール値、errはエラーです。if err != nil: 読み取り中にエラーが発生した場合、すぐにエラーを返します。if line == nil && !more: これは重要な最適化です。もしlineスライスがまだ空(つまり、これがReadLineの最初の呼び出し)で、かつmoreがfalse(つまり、最初の呼び出しで完全な行が読み取れた)ならば、余分なメモリコピーをせずにlを直接返します。これにより、ほとんどの短いヘッダー行の処理が効率的になります。line = append(line, l...):l(今回読み取った部分的な行)をlineスライスに結合します。これにより、複数回に分けて読み取られた行が1つの完全な行として構築されます。if !more:moreがfalseになった場合、行全体が読み取られたことを意味するため、ループを終了します。- 最終的に、結合された完全な行
lineを返します。
この修正により、readLineSlice は、bufio.Reader のバッファサイズを超えるような非常に長いヘッダー行でも、正しく読み取り、結合して処理できるようになりました。
src/pkg/net/textproto/reader_test.go の TestLargeReadMIMEHeader 関数
このテスト関数は、readLineSlice の変更が正しく機能していることを検証するために追加されました。
data := make([]byte, 16*1024): 16KB(16384バイト)のバイトスライスを作成します。これは、一般的なbufio.Readerのデフォルトバッファサイズ(通常4KB)よりもはるかに大きく、行が複数回に分けて読み取られる状況をシミュレートします。for i := 0; i < len(data); i++ { data[i] = 'x' }: 作成したスライスを 'x' で埋めます。sdata := string(data): バイトスライスを文字列に変換します。r := reader("Cookie: " + sdata + "\r\n\n"):Cookieヘッダーとして、非常に長いsdataを含むHTTPリクエストの文字列を作成し、それを読み取るためのtextproto.Readerを初期化します。m, err := r.ReadMIMEHeader():ReadMIMEHeaderメソッドを呼び出して、HTTPヘッダーを解析します。このメソッドは内部でreadLineSliceを使用します。if err != nil: エラーが発生した場合、テストを失敗させます。cookie := m.Get("Cookie"): 解析されたヘッダーからCookieの値を取得します。if cookie != sdata: 取得したCookieの値が、元の長い文字列sdataと一致しない場合、テストを失敗させます。これは、長いヘッダーが正しく読み取られ、結合されたことを検証します。
このテストは、このコミットが解決しようとしている問題(長いヘッダー行の不完全な読み取り)を直接的に検証しており、修正の有効性を示しています。
関連リンク
- Go言語の
net/textprotoパッケージのドキュメント: https://pkg.go.dev/net/textproto - Go言語の
bufioパッケージのドキュメント: https://pkg.go.dev/bufio - RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing: https://datatracker.ietf.org/doc/html/rfc7230 (特にセクション 3.2.4 "Field Parsing" を参照)
参考にした情報源リンク
- Go言語のソースコード (GitHub): https://github.com/golang/go
- RFC 7230: https://datatracker.ietf.org/doc/html/rfc7230
- HTTP 400 Bad Request の一般的な原因に関する情報 (例: MDN Web Docs, Stack Overflow など)
bufio.Reader.ReadLineの動作に関するGo言語のドキュメントや解説記事 I have generated the comprehensive technical explanation for commit 10171 as requested, following all the specified sections and details.