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

[インデックス 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でチェックするだけでした。この実装は、以下のような問題を引き起こしていました。

  1. 不正確なマッチング: foobarというヘッダー文字列に対してfooというトークンを検索した場合、strings.Containstrueを返してしまいますが、これはRFCの定義するトークン境界(スペースやカンマ)を考慮していません。つまり、foobarfooという独立したトークンを含んでいません。
  2. パフォーマンスの懸念: ヘッダー文字列全体を小文字に変換する操作は、特に大きなヘッダーや頻繁な呼び出しにおいてオーバーヘッドとなる可能性があります。

これらの問題は、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関数は、以下のロジックで動作します。

  1. 基本的なチェック: tokenvより長い場合や空の場合、即座にfalseを返します。vtokenと完全に一致する場合はtrueを返します(最適化)。
  2. ループによる検索: v文字列内でtokenの長さ分の部分文字列をスキャンします。
  3. 高速な最初の文字チェック: 各部分文字列の最初の文字が、tokenの最初の文字と大文字・小文字を区別せずに一致するかを高速にチェックします。これにより、strings.EqualFoldのようなコストの高い比較を避けることができます。b|0x20は、ASCII文字を小文字に変換する一般的なビット演算テクニックです。
  4. トークン境界のチェック:
    • 部分文字列の開始位置が、有効なトークン境界(スペース、カンマ、タブ)の後にあるか、または文字列の先頭であるかをisTokenBoundary関数を使って確認します。
    • 部分文字列の終了位置が、有効なトークン境界の前にあるか、または文字列の末尾であるかをisTokenBoundary関数を使って確認します。
  5. 最終的な比較: 上記の条件をすべて満たした場合にのみ、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を返してしまうという誤った挙動をしていました。これは、foofoobarの部分文字列ではあるものの、独立したトークンではないため、RFCの仕様に反します。

新しいhasToken関数は、isTokenBoundaryヘルパー関数と組み合わせて、トークンの前後にスペース、カンマ、タブなどの区切り文字があることを厳密にチェックします。これにより、foofoobarの一部としてではなく、foo, barfoo barのように独立したトークンとして存在する場合にのみtrueを返すようになります。

また、ループ内で最初の文字を高速にチェックし、strings.EqualFoldの呼び出し回数を減らすことで、パフォーマンスの向上も図られています。token引数が小文字であることを前提とすることで、token自体を小文字に変換するオーバーヘッドも回避しています。

この変更により、net/httpパッケージはHTTPヘッダーの解析において、より堅牢で正確な挙動を提供するようになりました。

関連リンク

参考にした情報源リンク

  • 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パッケージ)