[インデックス 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.