[インデックス 15239] ファイルの概要
このコミットは、Go言語の実験的な exp/cookiejar パッケージにおいて、HTTPクッキーの管理機能とそれに対応するテストスイートを実装したものです。具体的には、http.CookieJar インターフェースの Cookies メソッドの実装と、クッキーのドメインマッチング、パスマッチング、ソートロジックが追加されています。また、テストスイートはChromiumプロジェクトから移植されたテストケースを含んでおり、実装の堅牢性を高めています。
コミット
commit 8e7d156237cc1409aa6b955cf4307d2c4992ab29
Author: Volker Dobler <dr.volker.dobler@gmail.com>
Date: Thu Feb 14 19:41:58 2013 +1100
exp/cookiejar: implement Cookies and provided tests
This CL provides the implementation of Cookies and
the complete test suite. Several tests have been ported
from the Chromium project as a cross check.
R=nigeltao, rsc, bradfitz
CC=golang-dev
https://golang.org/cl/7311073
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8e7d156237cc1409aa6b955cf4307d2c4992ab29
元コミット内容
exp/cookiejar: implement Cookies and provided tests
この変更は、Cookies メソッドの実装と完全なテストスイートを提供します。いくつかのテストは、相互チェックとしてChromiumプロジェクトから移植されました。
変更の背景
このコミットが行われた当時、Go言語の標準ライブラリには、HTTPクッキーを効率的かつRFCに準拠して管理するための包括的な CookieJar の実装が不足していました。net/http パッケージには CookieJar インターフェースは存在していましたが、その具体的な実装は提供されていませんでした。ウェブアプリケーションやHTTPクライアントが適切にクッキーを処理するためには、クッキーの保存、取得、有効期限、ドメイン/パスのマッチング、セキュリティ属性(Secure, HttpOnly)の考慮など、複雑なロジックが必要となります。
このコミットは、これらの要件を満たす exp/cookiejar パッケージの一部として、http.CookieJar インターフェースの主要メソッドである Cookies の実装を提供することを目的としています。特に、クッキーの選択ロジック(どのクッキーをリクエストに含めるべきか)と、RFC 6265に準拠したソート順序の実装が重要な課題でした。また、実装の正確性を保証するために、既存の堅牢なクッキー管理システムであるChromiumプロジェクトのテストケースを移植し、相互検証を行うことが決定されました。
前提知識の解説
HTTPクッキー (HTTP Cookies)
HTTPクッキーは、ウェブサーバーがユーザーのウェブブラウザに送信する小さなデータ片です。ブラウザはこれらのクッキーを保存し、同じサーバーへの後続のリクエストでそれらを送り返します。これにより、サーバーはユーザーの状態を記憶したり、ユーザーを識別したりすることができます。
クッキーには様々な属性があります。
- Name-Value Pair:
name=valueの形式で、クッキーのデータ本体です。 - Expires / Max-Age: クッキーの有効期限を指定します。
Expiresは特定の日時、Max-Ageは現在からの秒数で指定します。これらが設定されていない場合、セッションクッキーとなり、ブラウザを閉じると削除されます。 - Domain: クッキーが送信されるドメインを指定します。例えば
example.comと設定すると、example.comおよびそのサブドメイン(www.example.com,sub.example.comなど)に送信されます。先頭にドットがない場合(例:www.example.com)、そのホスト名にのみ送信されます。 - Path: クッキーが送信されるパスを指定します。例えば
/docsと設定すると、/docsおよびそのサブパス(/docs/index.htmlなど)に送信されます。 - Secure: この属性が設定されている場合、クッキーはHTTPS接続でのみ送信されます。
- HttpOnly: この属性が設定されている場合、JavaScriptからクッキーにアクセスできなくなります。これにより、クロスサイトスクリプティング (XSS) 攻撃によるクッキーの盗難を防ぐことができます。
- SameSite: クロスサイトリクエストフォージェリ (CSRF) 攻撃を防ぐための属性です。このコミットの時点ではまだ広く普及していませんでしたが、現代のウェブでは重要なセキュリティ属性です。
RFC 6265
RFC 6265は、HTTP State Management Mechanism、すなわちHTTPクッキーの動作を定義する標準仕様です。このRFCは、クッキーのセット、保存、送信に関する詳細なルールを定めており、特にドメインマッチング、パスマッチング、クッキーのソート順序など、複雑なロジックの基準となります。このコミットでは、RFC 6265のセクション5.1.3 (domain-match) とセクション5.1.4 (path-match)、およびセクション5.4 (クッキーのソート) に厳密に準拠した実装が行われています。
Go言語の net/http および net/url パッケージ
net/http: Go言語のHTTPクライアントおよびサーバーを構築するための主要なパッケージです。http.Cookie構造体やhttp.CookieJarインターフェースなどが定義されています。net/url: URLの解析と構築を行うためのパッケージです。クッキーのドメインやパスのマッチングには、URLのホスト名やパスを正確に解析する能力が不可欠です。
Chromiumプロジェクト
Chromiumは、Google Chromeブラウザのオープンソース基盤です。Chromiumプロジェクトは、ウェブ技術の最先端を実装しており、そのクッキー管理システムは非常に堅牢で、RFCに厳密に準拠しています。このコミットでChromiumのテストケースを移植したことは、Goの cookiejar 実装の正確性と互換性を検証するための重要な手段となっています。
技術的詳細
このコミットの主要な技術的詳細は、exp/cookiejar パッケージ内の Jar 型の Cookies メソッドの実装と、それに付随するヘルパー関数の追加にあります。
-
Cookiesメソッドの実装:http.CookieJarインターフェースのCookies(u *url.URL) []*http.Cookieメソッドを実装しています。このメソッドは、指定されたURLに対して送信すべきクッキーのリストを返します。- まず、URLのスキームがHTTPまたはHTTPSでない場合は空のスライスを返します。これはセキュリティ上の理由と、クッキーが主にHTTP/HTTPSで使用されるためです。
Jar内部のクッキーエントリ (entry型) を走査し、現在の時刻 (now) と比較して有効期限切れの永続クッキーを削除します。shouldSendヘルパー関数を使用して、各クッキーが現在のホストとパスに対して送信可能かどうかを判断します。- 送信可能なクッキーを
selectedスライスに集めます。 - RFC 6265のセクション5.4に従い、
selectedスライス内のクッキーをソートします。このソートは、パスの長さが長いものから順に行われ、パスの長さが同じ場合は作成時刻が古いものから順に行われます。 - ソートされたクッキーを
*http.Cookie型に変換し、結果として返します。
-
ヘルパー関数の追加:
shouldSend(https bool, host, path string) bool:- クッキーエントリ
eが、指定されたhostとpathに対して送信されるべきかを判断します。 domainMatchとpathMatchの両方がtrueであり、かつリクエストがHTTPSであるか、またはクッキーがSecure属性を持たない場合にtrueを返します。
- クッキーエントリ
domainMatch(host string) bool:- RFC 6265のセクション5.1.3「domain-match」を実装します。
- クッキーの
Domain属性がホストと完全に一致するか、またはクッキーがホストオンリーでない場合に、ホストがクッキーのドメインのサブドメインであるかをチェックします。
pathMatch(requestPath string) bool:- RFC 6265のセクション5.1.4「path-match」を実装します。
- リクエストパスがクッキーの
Path属性と完全に一致するか、またはリクエストパスがクッキーのパスで始まり、かつクッキーのパスがスラッシュで終わるか、リクエストパスの次の文字がスラッシュであるかをチェックします。
byPathLength型とsort.Interface実装:byPathLengthは[]entry型のエイリアスで、sort.Interfaceインターフェース(Len,Less,Swapメソッド)を実装します。Lessメソッドは、RFC 6265のセクション5.4のポイント2に従い、パスの長さが長いものから順に、次に作成時刻が古いものから順にソートするロジックを提供します。
-
テストスイートの拡充:
jar_test.goに大量のテストケースが追加されています。updateAndDeleteTests: クッキーの更新と削除に関するテスト。Max-AgeやExpires属性による削除、Secureフラグのクリア、異なるドメインやパスでのクッキーの挙動などを検証します。TestExpiration: クッキーの有効期限切れが正しく処理されるかを検証します。chromiumBasicsTests,chromiumDomainTests,chromiumDeletionTests: Chromiumプロジェクトのcookie_store_unittest.hから移植されたテストケース群です。これらは、ドメインマッチングの厳密なルール、IPアドレスに対するクッキーの挙動、非ドットホスト名とTLDの処理、ドメイン属性のケースインセンシティブ性、パスのマッチングなど、非常に詳細なエッジケースをカバーしています。これにより、Goのcookiejar実装がウェブの現実世界で遭遇する様々なシナリオに正確に対応できることが保証されます。
コアとなるコードの変更箇所
src/pkg/exp/cookiejar/jar.go
--- a/src/pkg/exp/cookiejar/jar.go
+++ b/src/pkg/exp/cookiejar/jar.go
@@ -11,6 +11,7 @@ import (
"net"
"net/http"
"net/url"
+ "sort"
"strings"
"sync"
"time"
@@ -97,6 +98,52 @@ func (e *entry) id() string {
return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.Name)
}
+// shouldSend determines whether e's cookie qualifies to be included in a
+// request to host/path. It is the caller's responsibility to check if the
+// cookie is expired.
+func (e *entry) shouldSend(https bool, host, path string) bool {
+ return e.domainMatch(host) && e.pathMatch(path) && (https || !e.Secure)
+}
+
+// domainMatch implements "domain-match" of RFC 6265 section 5.1.3.
+func (e *entry) domainMatch(host string) bool {
+ if e.Domain == host {
+ return true
+ }
+ return !e.HostOnly && strings.HasSuffix(host, "."+e.Domain)
+}
+
+// pathMatch implements "path-match" according to RFC 6265 section 5.1.4.
+func (e *entry) pathMatch(requestPath string) bool {
+ if requestPath == e.Path {
+ return true
+ }
+ if strings.HasPrefix(requestPath, e.Path) {
+ if e.Path[len(e.Path)-1] == '/' {
+ return true // The "/any/" matches "/any/path" case.
+ } else if requestPath[len(e.Path)] == '/' {
+ return true // The "/any" matches "/any/path" case.
+ }
+ }
+ return false
+}
+
+// byPathLength is a []entry sort.Interface that sorts according to RFC 6265
+// section 5.4 point 2: by longest path and then by earliest creation time.
+type byPathLength []entry
+
+func (s byPathLength) Len() int { return len(s) }
+
+func (s byPathLength) Less(i, j int) bool {
+ in, jn := len(s[i].Path), len(s[j].Path)
+ if in == jn {
+ return s[i].Creation.Before(s[j].Creation)
+ }
+ return in > jn
+}
+
+func (s byPathLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
// Cookies implements the Cookies method of the http.CookieJar interface.
//
// It returns an empty slice if the URL's scheme is not HTTP or HTTPS.
@@ -118,10 +165,28 @@ func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
return cookies
}
+ now := time.Now()
+ https := u.Scheme == "https"
+ path := u.Path
+ if path == "" {
+ path = "/"
+ }
+
modified := false
- for _, _ = range submap {
- // TODO: handle expired cookies
- // TODO: handle selection of cookies
+ var selected []entry
+ for id, e := range submap {
+ if e.Persistent && !e.Expires.After(now) {
+ delete(submap, id)
+ modified = true
+ continue
+ }
+ if !e.shouldSend(https, host, path) {
+ continue
+ }
+ e.LastAccess = now
+ submap[id] = e
+ selected = append(selected, e)
+ modified = true
}
if modified {
if len(submap) == 0 {
@@ -131,7 +196,10 @@ func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {
}
}
- // TODO: proper sorting based on Path length (and Creation)
+ sort.Sort(byPathLength(selected))
+ for _, e := range selected {
+ cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value})
+ }
return cookies
}
src/pkg/exp/cookiejar/jar_test.go
このファイルには、TestBasics、TestUpdateAndDelete、TestExpiration、TestChromiumBasics、TestChromiumDomain、TestChromiumDeletion など、多数の新しいテストケースが追加されています。特に注目すべきは、Chromiumプロジェクトから移植されたテストケース群です。
コアとなるコードの解説
jar.go の変更点
import "sort"の追加: クッキーをRFC 6265の規定に従ってソートするために、Go標準ライブラリのsortパッケージがインポートされました。shouldSend関数:- この関数は、特定の
entry(クッキー) が、与えられたhostとpathのリクエストに対して送信されるべきかどうかを判断します。 e.domainMatch(host): クッキーのドメインがリクエストのホストと一致するかをチェックします。e.pathMatch(path): クッキーのパスがリクエストのパスと一致するかをチェックします。(https || !e.Secure): リクエストがHTTPSであるか、またはクッキーがSecure属性を持たない場合にtrueとなります。これにより、SecureクッキーはHTTPSでのみ送信され、非SecureクッキーはHTTP/HTTPSの両方で送信されるというルールが適用されます。
- この関数は、特定の
domainMatch関数:- RFC 6265のセクション5.1.3「domain-match」アルゴリズムを実装しています。
e.Domain == host: クッキーのドメインとリクエストのホストが完全に一致する場合。!e.HostOnly && strings.HasSuffix(host, "."+e.Domain): クッキーがホストオンリーでない(つまり、ドメインクッキーである)場合、リクエストのホストがクッキーのドメインのサブドメインであるかをチェックします。例えば、クッキーのドメインがexample.comで、ホストがwww.example.comの場合、この条件がtrueになります。
pathMatch関数:- RFC 6265のセクション5.1.4「path-match」アルゴリズムを実装しています。
requestPath == e.Path: リクエストパスとクッキーのパスが完全に一致する場合。strings.HasPrefix(requestPath, e.Path): リクエストパスがクッキーのパスで始まる場合。e.Path[len(e.Path)-1] == '/': クッキーのパスがスラッシュで終わる場合(例:/any/は/any/pathにマッチ)。requestPath[len(e.Path)] == '/': クッキーのパスがスラッシュで終わらないが、リクエストパスの次の文字がスラッシュである場合(例:/anyは/any/pathにマッチ)。
byPathLength型とsort.Interface実装:- これは、クッキーをソートするためのカスタムソートロジックを提供します。
Less(i, j int) bool: 比較ロジックを定義します。- まず、パスの長さで比較し、長いパスを持つクッキーが優先されます (
in > jn)。 - パスの長さが同じ場合は、クッキーの作成時刻 (
Creation) で比較し、古いクッキーが優先されます (s[i].Creation.Before(s[j].Creation))。
- まず、パスの長さで比較し、長いパスを持つクッキーが優先されます (
- このソート順序は、RFC 6265のセクション5.4「The User Agent Sends Cookies to the Origin Server」のポイント2に厳密に準拠しています。これにより、より具体的なパスを持つクッキーが、より一般的なパスを持つクッキーよりも優先して送信されるようになります。
Cookiesメソッドのロジック:now := time.Now(): 現在時刻を取得し、有効期限切れのクッキーを判断するために使用します。https := u.Scheme == "https": リクエストがHTTPSかどうかを判断します。path := u.Path; if path == "" { path = "/" }: リクエストパスが空の場合、デフォルトでルートパス/を使用します。- クッキーエントリの
submapをイテレートします。e.Persistent && !e.Expires.After(now): 永続クッキーが有効期限切れの場合、submapから削除し、modifiedフラグをtrueに設定します。!e.shouldSend(https, host, path):shouldSend関数がfalseを返す場合、そのクッキーは選択されません。e.LastAccess = now: クッキーがアクセスされた時刻を更新します。selected = append(selected, e): 送信対象となるクッキーをselectedスライスに追加します。
sort.Sort(byPathLength(selected)):selectedスライス内のクッキーを、上記で定義したbyPathLengthのロジックに従ってソートします。- ソートされた
selectedクッキーを*http.Cookie型に変換し、最終的な結果スライスcookiesに追加して返します。
jar_test.go の変更点
jarTest構造体のrunメソッドの変更:jar.content()メソッドが削除され、テスト内で直接クッキーの内容を文字列に変換するロジックが追加されました。これにより、テストの独立性が高まり、Jarの内部実装に依存しない形で内容を検証できるようになりました。jar.SetCookiesの呼び出しでmustParseURL(test.fromURL)を直接使用するように変更され、冗長な変数uが削除されました。t.Errorf()の呼び出しがコメントアウトされていた箇所が有効化され、Cookiesメソッドの実装が完了したことを示しています。
- 新しいテストケースの追加:
updateAndDeleteTests: クッキーの更新、削除(Max-Age=-1や過去のExpires)、Secureフラグの挙動、異なるドメインやパスでのクッキーの相互作用など、動的なクッキー管理のシナリオを網羅しています。TestExpiration: 時間経過によるクッキーの有効期限切れが正しく処理されることを検証します。- Chromiumプロジェクトからの移植テスト:
chromiumBasicsTests,chromiumDomainTests,chromiumDeletionTestsは、Chromiumブラウザのクッキー管理のテストスイートから厳選されたものです。これらは、以下のような非常に詳細なエッジケースをカバーしています。- ドメイン属性の末尾のドットの扱い。
- 有効なサブドメインと無効なドメインのマッチング。
- IPアドレスに対するクッキーの挙動。
- 非ドットホスト名とトップレベルドメイン (TLD) の処理。
- ドメイン属性のケースインセンシティブ性。
- パスのマッチングの厳密なルール。
- ホストクッキーとドメインクッキーの挙動の違い。
- セッションクッキーと永続クッキーの削除ロジック。
これらのテストケースは、Goの cookiejar 実装がRFC 6265に準拠し、実際のウェブ環境で期待される堅牢な動作をすることを保証するために不可欠です。
関連リンク
- RFC 6265 - HTTP State Management Mechanism: https://datatracker.ietf.org/doc/html/rfc6265
- Go言語
net/httpパッケージ: https://pkg.go.dev/net/http - Go言語
net/urlパッケージ: https://pkg.go.dev/net/url
参考にした情報源リンク
- Chromium
cookie_store_unittest.h(当時のバージョン): http://src.chromium.org/viewvc/chrome/trunk/src/net/cookies/cookie_store_unittest.h?revision=159685&content-type=text/plain- このリンクは当時のChromiumのテストファイルの具体的なリビジョンを示しており、Goの
cookiejarテストがここから移植されたことを裏付けています。
- このリンクは当時のChromiumのテストファイルの具体的なリビジョンを示しており、Goの
- Goのコードレビューシステム (Gerrit) のCL (Change List) ページ: https://golang.org/cl/7311073
- このリンクは、このコミットに対応するGoの公式コードレビューページであり、詳細な議論や変更履歴を確認できます。
- HTTP Cookie - MDN Web Docs: https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies
- HTTPクッキーの基本的な概念と属性について理解を深めるための一般的な情報源。
- Go言語の
sortパッケージ: https://pkg.go.dev/sort- Goにおけるソートインターフェースとカスタムソートの実装方法に関する情報。