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

[インデックス 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 メソッドの実装と、それに付随するヘルパー関数の追加にあります。

  1. 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 型に変換し、結果として返します。
  2. ヘルパー関数の追加:

    • shouldSend(https bool, host, path string) bool:
      • クッキーエントリ e が、指定された hostpath に対して送信されるべきかを判断します。
      • domainMatchpathMatch の両方が 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に従い、パスの長さが長いものから順に、次に作成時刻が古いものから順にソートするロジックを提供します。
  3. テストスイートの拡充:

    • jar_test.go に大量のテストケースが追加されています。
    • updateAndDeleteTests: クッキーの更新と削除に関するテスト。Max-AgeExpires 属性による削除、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

このファイルには、TestBasicsTestUpdateAndDeleteTestExpirationTestChromiumBasicsTestChromiumDomainTestChromiumDeletion など、多数の新しいテストケースが追加されています。特に注目すべきは、Chromiumプロジェクトから移植されたテストケース群です。

コアとなるコードの解説

jar.go の変更点

  1. import "sort" の追加: クッキーをRFC 6265の規定に従ってソートするために、Go標準ライブラリの sort パッケージがインポートされました。
  2. shouldSend 関数:
    • この関数は、特定の entry (クッキー) が、与えられた hostpath のリクエストに対して送信されるべきかどうかを判断します。
    • e.domainMatch(host): クッキーのドメインがリクエストのホストと一致するかをチェックします。
    • e.pathMatch(path): クッキーのパスがリクエストのパスと一致するかをチェックします。
    • (https || !e.Secure): リクエストがHTTPSであるか、またはクッキーが Secure 属性を持たない場合に true となります。これにより、Secure クッキーはHTTPSでのみ送信され、非Secure クッキーはHTTP/HTTPSの両方で送信されるというルールが適用されます。
  3. domainMatch 関数:
    • RFC 6265のセクション5.1.3「domain-match」アルゴリズムを実装しています。
    • e.Domain == host: クッキーのドメインとリクエストのホストが完全に一致する場合。
    • !e.HostOnly && strings.HasSuffix(host, "."+e.Domain): クッキーがホストオンリーでない(つまり、ドメインクッキーである)場合、リクエストのホストがクッキーのドメインのサブドメインであるかをチェックします。例えば、クッキーのドメインが example.com で、ホストが www.example.com の場合、この条件が true になります。
  4. 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 にマッチ)。
  5. 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に厳密に準拠しています。これにより、より具体的なパスを持つクッキーが、より一般的なパスを持つクッキーよりも優先して送信されるようになります。
  6. 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に準拠し、実際のウェブ環境で期待される堅牢な動作をすることを保証するために不可欠です。

関連リンク

参考にした情報源リンク