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

[インデックス 13118] ファイルの概要

コミット

commit f7277dac57c77fd596ef077beb1ab92ae8b20dce
Author: Alexey Borzenkov <snaury@gmail.com>
Date:   Tue May 22 12:44:24 2012 -0400

    net/url: better parsing of urls with @ symbol in authority
    
    Fixes #3439
    
    R=r, rsc, dsymonds, n13m3y3r
    CC=golang-dev
    https://golang.org/cl/6206090

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/f7277dac57c77fd596ef077beb1ab92ae8b20dce

元コミット内容

net/url: authority部分に@記号を含むURLのより良いパース処理

Issue #3439を修正。

変更の背景

このコミットは、Go言語の標準ライブラリnet/urlパッケージにおけるURLパースの不具合を修正するために行われました。具体的には、URLのauthority(認証情報とホスト名を含む部分)に@記号が含まれる場合に、パースが正しく行われない問題がありました。

元の実装では、authority文字列内の最初の@記号をユーザー情報とホスト名の区切りとして扱っていました。しかし、ユーザー名やパスワード自体にエスケープされていない@記号が含まれる場合、このロジックでは誤った区切り位置を特定し、結果としてホスト名がユーザー情報の一部として誤って解釈される可能性がありました。

例えば、http://user@gmail.com:password@hostname.com:80/pathのようなURLをパースする際に、user@gmail.comがユーザー名、passwordがパスワード、hostname.com:80がホスト名として正しく認識されるべきですが、元の実装では最初の@で分割されるため、userがユーザー名、gmail.com:password@hostname.com:80がホスト名として誤って解釈されていました。

この問題は、GitHubのGoリポジトリのIssue #3439「net/url: Parse does not parse username and password correctly」として報告されており、このコミットはその報告された問題を解決することを目的としています。

前提知識の解説

URLの構造とauthority部分

URL (Uniform Resource Locator) は、インターネット上のリソースの位置を示すための標準的な方法です。一般的なURLの構造は以下のようになります。

scheme://[userinfo@]host[:port][/path][?query][#fragment]

この中で、今回のコミットが関連するのはauthority部分、すなわち[userinfo@]host[:port]です。

  • scheme: プロトコル(例: http, https, ftp)。
  • userinfo: ユーザー名とパスワードを含むオプションの部分。通常はusername:passwordの形式で、ホスト名の前に@記号で区切られて配置されます。
  • host: サーバーのドメイン名またはIPアドレス。
  • port: サーバーのポート番号(オプション)。
  • path: サーバー上のリソースのパス。
  • query: クエリパラメータ(オプション)。
  • fragment: フラグメント識別子(オプション)。

@記号の役割とエスケープ

authority部分における@記号は、userinfohostを区切るための特別なデリミタです。RFC 3986 (Uniform Resource Identifier (URI): Generic Syntax) では、@は「サブデリミタ」として定義されており、URIの特定のコンポーネント内で特別な意味を持つ文字の一つです。

もしユーザー名やパスワード自体に@記号を含めたい場合は、その@記号はパーセントエンコーディング(URLエンコーディング)によって%40のようにエスケープされる必要があります。これにより、URLパーサーはエスケープされた@とデリミタとしての@を区別できます。

しかし、現実にはエスケープされていない@がユーザー名やパスワードに含まれるURLも存在し、堅牢なURLパーサーはこれらを適切に処理する必要があります。このコミットは、まさにその「エスケープされていない@がユーザー名やパスワードに含まれるケース」に対応するためのものです。

strings.Indexstrings.LastIndex

Go言語のstringsパッケージには、文字列内で部分文字列のインデックスを検索するための関数がいくつかあります。

  • strings.Index(s, substr string) int: 文字列s内でsubstrが最初に現れるインデックスを返します。見つからない場合は-1を返します。
  • strings.LastIndex(s, substr string) int: 文字列s内でsubstrが最後に現れるインデックスを返します。見つからない場合は-1を返します。

URLのauthority部分において、userinfohostを区切る@は、常にuserinfoの末尾に現れるため、複数の@が存在する場合(ユーザー名やパスワードに@が含まれる場合)、最後の@がデリミタであると判断するのが正しいパースロジックとなります。

技術的詳細

このコミットの核心は、net/urlパッケージ内のparseAuthority関数のロジック変更にあります。この関数は、URLのauthority文字列を受け取り、そこからユーザー情報(Userinfo)とホスト名(host)を抽出する役割を担っています。

変更前のparseAuthority関数は、strings.Index(authority, "@")を使用して、authority文字列内で@記号が最初に現れる位置を検索していました。この最初の@が見つかると、その位置で文字列を分割し、前半をユーザー情報、後半をホスト名として扱っていました。

しかし、このアプローチには問題がありました。例えば、http://j@ne:password@google.comのようなURLを考えてみましょう。

  • authority文字列はj@ne:password@google.comです。
  • 最初の@jの直後にあります。
  • 元のロジックでは、jをユーザー名、ne:password@google.comをホスト名として誤って解釈してしまいます。

正しい解釈は、j@neがユーザー名、passwordがパスワード、google.comがホスト名です。この場合、ユーザー名j@neとパスワードpasswordの後に続く@が、ユーザー情報とホスト名を区切るデリミタとなります。つまり、最後の@が真のデリミタなのです。

このコミットでは、この問題を解決するために、strings.Indexstrings.LastIndexに置き換えることで、authority文字列内の最後の@記号をユーザー情報とホスト名の区切りとして使用するように変更しました。これにより、ユーザー名やパスワードにエスケープされていない@記号が含まれていても、URLのauthority部分が正しくパースされるようになります。

また、この変更の正当性を保証するために、src/pkg/net/url/url_test.goに新しいテストケースが追加されました。これらのテストケースは、ユーザー名やパスワードに@記号が含まれる様々なシナリオを網羅しており、修正されたパースロジックが期待通りに機能することを確認しています。

コアとなるコードの変更箇所

変更は主にsrc/pkg/net/url/url.gosrc/pkg/net/url/url_test.goの2つのファイルで行われています。

src/pkg/net/url/url.go

--- a/src/pkg/net/url/url.go
+++ b/src/pkg/net/url/url.go
@@ -401,11 +401,12 @@ Error:
 }
 
 func parseAuthority(authority string) (user *Userinfo, host string, err error) {
-	if strings.Index(authority, "@") < 0 {
+	i := strings.LastIndex(authority, "@")
+	if i < 0 {
 		host = authority
 		return
 	}
-	userinfo, host := split(authority, '@', true)
+	userinfo, host := authority[:i], authority[i+1:]
 	if strings.Index(userinfo, ":") < 0 {
 		if userinfo, err = unescape(userinfo, encodeUserPassword); err != nil {
 			return

src/pkg/net/url/url_test.go

--- a/src/pkg/net/url/url_test.go
+++ b/src/pkg/net/url/url_test.go
@@ -188,6 +188,37 @@ var urltests = []URLTest{
 		},
 		"http://user:password@google.com",
 	},
+	// unescaped @ in username should not confuse host
+	{
+		"http://j@ne:password@google.com",
+		&URL{
+			Scheme: "http",
+			User:   UserPassword("j@ne", "password"),
+			Host:   "google.com",
+		},
+		"http://j%40ne:password@google.com",
+	},
+	// unescaped @ in password should not confuse host
+	{
+		"http://jane:p@ssword@google.com",
+		&URL{
+			Scheme: "http",
+			User:   UserPassword("jane", "p@ssword"),
+			Host:   "google.com",
+		},
+		"http://jane:p%40ssword@google.com",
+	},
+	{
+		"http://j@ne:password@google.com/p@th?q=@go",
+		&URL{
+			Scheme:   "http",
+			User:     UserPassword("j@ne", "password"),
+			Host:     "google.com",
+			Path:     "/p@th",
+			RawQuery: "q=@go",
+		},
+		"http://j%40ne:password@google.com/p@th?q=@go",
+	},
 	{
 		"http://www.google.com/?q=go+language#foo",
 		&URL{

コアとなるコードの解説

src/pkg/net/url/url.goの変更

parseAuthority関数内の変更は以下の通りです。

  1. i := strings.LastIndex(authority, "@")
    • 以前はstrings.Indexが使われていましたが、これをstrings.LastIndexに変更しました。これにより、authority文字列内で@記号が最後に現れる位置がiに格納されます。この最後の@が、ユーザー情報とホスト名を区切る正しいデリミタであると見なされます。
  2. if i < 0 { ... }
    • @記号が見つからない場合(i-1の場合)は、authority全体がホスト名であると判断し、そのままhost = authorityとして関数を終了します。これは変更前と同じロジックです。
  3. userinfo, host := authority[:i], authority[i+1:]
    • @記号が見つかった場合、authority文字列をiの位置で分割します。
    • authority[:i]は、文字列の先頭からiの直前までの部分(つまり、最後の@より前の部分)をuserinfoとして抽出します。
    • authority[i+1:]は、最後の@の直後から文字列の末尾までの部分をhostとして抽出します。
    • 以前はsplit関数が使われていましたが、strings.LastIndexでインデックスを取得したため、スライス操作で直接分割できるようになりました。これにより、より明確で効率的な分割が可能になります。

この変更により、ユーザー名やパスワードに@記号が含まれていても、最後の@が正しくデリミタとして機能し、URLのauthority部分が意図通りにパースされるようになります。

src/pkg/net/url/url_test.goの変更

テストファイルには、新しい3つのテストケースが追加されました。これらは、@記号がユーザー名やパスワードに含まれる場合のパースの正確性を検証します。

  1. "http://j@ne:password@google.com"
    • ユーザー名に@が含まれるケース。期待される結果は、ユーザー名がj@ne、パスワードがpassword、ホストがgoogle.comです。
  2. "http://jane:p@ssword@google.com"
    • パスワードに@が含まれるケース。期待される結果は、ユーザー名がjane、パスワードがp@ssword、ホストがgoogle.comです。
  3. "http://j@ne:password@google.com/p@th?q=@go"
    • ユーザー名に@が含まれ、さらにパスやクエリパラメータにも@が含まれる複合的なケース。これにより、authority部分のパースが他のURLコンポーネントに影響を与えないことも確認されます。

これらのテストケースは、修正されたparseAuthority関数が、@記号の扱いに起因する以前のバグを確実に修正し、RFCの仕様に沿った堅牢なURLパースを実現していることを保証します。また、UserPasswordヘルパー関数を使用して、ユーザー名とパスワードが正しく抽出されていることを検証しています。

関連リンク

参考にした情報源リンク