[インデックス 13176] ファイルの概要
このコミットは、Go言語の標準ライブラリnet/http
パッケージにおけるHTTPヘッダーのトークン解析ロジックを改善し、より正確かつ高速なhasToken
関数を導入するものです。特に、HTTPヘッダー内のカンマ区切りやスペース区切りのトークンを正しく識別するための修正が含まれています。
コミット
commit 469e3a91d450fb29778ba0d37377ddb40a58f1d5
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon May 28 10:55:39 2012 -0700
net/http: correct and faster hasToken
Fixes #3535
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/6245060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/469e3a91d450fb29778ba0d37377ddb40a58f1d5
元コミット内容
このコミットの目的は、net/http
パッケージ内のhasToken
関数の実装を修正し、パフォーマンスを向上させることです。具体的には、HTTPヘッダーのConnection
フィールドなどで使用されるトークン(例: close
, keep-alive
)の存在チェックが、RFCの仕様に準拠していなかった問題を解決します。
変更の背景
Go言語のnet/http
パッケージは、HTTPプロトコルを扱うための基盤を提供します。HTTPヘッダーには、カンマやスペースで区切られた複数の「トークン」が含まれることがあります。例えば、Connection: close, keep-alive
のような形式です。
以前のhasToken
関数は、単にヘッダー文字列を小文字に変換し、指定されたトークンが部分文字列として含まれているかをstrings.Contains
でチェックするだけでした。この実装は、以下のような問題を引き起こしていました。
- 不正確なマッチング:
foobar
というヘッダー文字列に対してfoo
というトークンを検索した場合、strings.Contains
はtrue
を返してしまいますが、これはRFCの定義するトークン境界(スペースやカンマ)を考慮していません。つまり、foobar
はfoo
という独立したトークンを含んでいません。 - パフォーマンスの懸念: ヘッダー文字列全体を小文字に変換する操作は、特に大きなヘッダーや頻繁な呼び出しにおいてオーバーヘッドとなる可能性があります。
これらの問題は、Go issue #3535として報告されており、このコミットはその問題を解決するために行われました。
前提知識の解説
HTTPヘッダーとトークン
HTTP/1.1の仕様(RFC 2616, 現在はRFC 7230-7235に分割)では、一部のヘッダーフィールドの値が「トークン」のリストとして定義されています。トークンは、特定の文字セット(英数字と一部の記号)で構成される単語であり、通常はカンマ,
やスペース
で区切られます。
例:
Connection: close
Connection: keep-alive
Connection: close, keep-alive
Transfer-Encoding: chunked
Accept-Encoding: gzip, deflate
これらのヘッダーを正しく解析するには、単なる部分文字列検索ではなく、トークンの境界(スペース、カンマ、タブなど)を考慮した上で、大文字・小文字を区別しない比較を行う必要があります。
Go言語のtextproto
パッケージ
textproto
パッケージは、MIMEスタイルのテキストプロトコル(HTTP、NNTP、SMTPなど)を解析するための低レベルな機能を提供します。textproto.CanonicalMIMEHeaderKey
関数は、HTTPヘッダー名を標準的な大文字・小文字の形式(例: Content-Type
)に変換するために使用されます。
strings.EqualFold
Go言語のstrings.EqualFold(s, t string)
関数は、Unicodeのデフォルトケースマッピングに従って、2つの文字列がASCIIの大文字・小文字を区別せずに等しいかどうかを報告します。これは、HTTPヘッダーのトークン比較において非常に有用です。
技術的詳細
このコミットでは、net/http/header.go
に新しいhasToken
関数とヘルパー関数isTokenBoundary
が追加されました。
新しいhasToken
関数
// hasToken returns whether token appears with v, ASCII
// case-insensitive, with space or comma boundaries.
// token must be all lowercase.
// v may contain mixed cased.
func hasToken(v, token string) bool {
if len(token) > len(v) || token == "" {
return false
}
if v == token { // 最適化: vがtokenと完全に一致する場合
return true
}
for sp := 0; sp <= len(v)-len(token); sp++ {
// Check that first character is good.
if b := v[sp]; b != token[0] && b|0x20 != token[0] { // 最初の文字が一致しない、かつ大文字・小文字を無視しても一致しない場合
continue // 次の開始位置へ
}
// Check that start pos is on a valid token boundary.
if sp > 0 && !isTokenBoundary(v[sp-1]) { // トークンの開始位置が境界でない場合
continue // 次の開始位置へ
}
// Check that end pos is on a valid token boundary.
if endPos := sp + len(token); endPos != len(v) && !isTokenBoundary(v[endPos]) { // トークンの終了位置が境界でない場合
continue // 次の開始位置へ
}
if strings.EqualFold(v[sp:sp+len(token)], token) { // 大文字・小文字を区別せずにトークンが一致する場合
return true
}
}
return false
}
この新しいhasToken
関数は、以下のロジックで動作します。
- 基本的なチェック:
token
がv
より長い場合や空の場合、即座にfalse
を返します。v
がtoken
と完全に一致する場合はtrue
を返します(最適化)。 - ループによる検索:
v
文字列内でtoken
の長さ分の部分文字列をスキャンします。 - 高速な最初の文字チェック: 各部分文字列の最初の文字が、
token
の最初の文字と大文字・小文字を区別せずに一致するかを高速にチェックします。これにより、strings.EqualFold
のようなコストの高い比較を避けることができます。b|0x20
は、ASCII文字を小文字に変換する一般的なビット演算テクニックです。 - トークン境界のチェック:
- 部分文字列の開始位置が、有効なトークン境界(スペース、カンマ、タブ)の後にあるか、または文字列の先頭であるかを
isTokenBoundary
関数を使って確認します。 - 部分文字列の終了位置が、有効なトークン境界の前にあるか、または文字列の末尾であるかを
isTokenBoundary
関数を使って確認します。
- 部分文字列の開始位置が、有効なトークン境界(スペース、カンマ、タブ)の後にあるか、または文字列の先頭であるかを
- 最終的な比較: 上記の条件をすべて満たした場合にのみ、
strings.EqualFold
を使って部分文字列とtoken
が大文字・小文字を区別せずに一致するかを最終的に確認します。
isTokenBoundary
ヘルパー関数
func isTokenBoundary(b byte) bool {
return b == ' ' || b == ',' || b == '\t'
}
この関数は、与えられたバイトがHTTPトークンの有効な境界文字(スペース、カンマ、タブ)であるかをシンプルにチェックします。
テストの追加
src/pkg/net/http/header_test.go
に、新しいhasToken
関数の正確性を検証するための広範なテストケースが追加されました。これには、以下のようなシナリオが含まれます。
- 空文字列
- 完全一致
- 前後にスペースがある場合
- カンマ区切りで複数のトークンがある場合
- 大文字・小文字が混在している場合
- 部分文字列として含まれるが、独立したトークンではない場合(例:
foobar
中のfoo
)
これらのテストケースは、新しいhasToken
関数がRFCの仕様に厳密に従って動作することを保証します。
既存コードの変更
src/pkg/net/http/request.go
から、古いhasToken
関数の実装が削除されました。これは、新しいhasToken
関数がnet/http/header.go
に移動され、より汎用的に使用されるようになったためです。wantsClose()
のような関数は、新しいhasToken
関数を呼び出すように変更されています。
コアとなるコードの変更箇所
src/pkg/net/http/header.go
hasToken
関数が新規追加されました。isTokenBoundary
ヘルパー関数が新規追加されました。
src/pkg/net/http/header_test.go
hasTokenTest
構造体とhasTokenTests
スライスが新規追加されました。TestHasToken
関数が新規追加され、hasToken
関数のテストケースが定義されました。
src/pkg/net/http/request.go
- 既存の
hasToken
関数の実装が削除されました。
コアとなるコードの解説
このコミットの核心は、net/http/header.go
に追加されたhasToken
関数です。この関数は、HTTPヘッダーの値を解析し、特定のトークンがRFCの定義に従って存在するかどうかを判断するためのものです。
以前の単純なstrings.Contains(strings.ToLower(s), token)
という実装は、Connection: foobar
のようなヘッダーに対してhasToken("foobar", "foo")
がtrue
を返してしまうという誤った挙動をしていました。これは、foo
がfoobar
の部分文字列ではあるものの、独立したトークンではないため、RFCの仕様に反します。
新しいhasToken
関数は、isTokenBoundary
ヘルパー関数と組み合わせて、トークンの前後にスペース、カンマ、タブなどの区切り文字があることを厳密にチェックします。これにより、foo
がfoobar
の一部としてではなく、foo, bar
やfoo bar
のように独立したトークンとして存在する場合にのみtrue
を返すようになります。
また、ループ内で最初の文字を高速にチェックし、strings.EqualFold
の呼び出し回数を減らすことで、パフォーマンスの向上も図られています。token
引数が小文字であることを前提とすることで、token
自体を小文字に変換するオーバーヘッドも回避しています。
この変更により、net/http
パッケージはHTTPヘッダーの解析において、より堅牢で正確な挙動を提供するようになりました。
関連リンク
- Go issue #3535: http://golang.org/issue/3535 (コミットメッセージに記載されているリンク)
- Go CL 6245060: https://golang.org/cl/6245060 (コミットメッセージに記載されているコードレビューリンク)
参考にした情報源リンク
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (特にセクション 2.2 Basic Rules, 4.2 Message Headers, 14.10 Connection Field)
- 現在ではRFC 7230-7235に分割されていますが、当時の参照はRFC 2616が一般的でした。
- Go言語の
strings
パッケージドキュメント:strings.EqualFold
- Go言語の
textproto
パッケージドキュメント:textproto.CanonicalMIMEHeaderKey
- Go言語のソースコード(
net/http
パッケージ)