[インデックス 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: closeConnection: keep-aliveConnection: close, keep-aliveTransfer-Encoding: chunkedAccept-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パッケージ)