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

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

このコミットは、Go言語の標準ライブラリであるnet/httpパッケージにおいて、Basic認証でパスワードが空の場合の挙動を修正するものです。具体的には、RFC 2617で定義されているBasic認証の仕様に従い、ユーザー名とパスワードを区切るコロン(:)が、パスワードが空の場合でもエンコードされた文字列に含まれるように変更されました。これにより、空のパスワードを持つBasic認証が正しく機能するようになります。また、この変更を検証するための新しいテストケースも追加されています。

コミット

commit d535bc7af3290c6f09eeb391e0ef00f374f9b743
Author: Alberto García Hierro <alberto@garciahierro.com>
Date:   Tue May 14 15:33:46 2013 -0700

    net/http: Fix basic authentication with empty password
    
            The encoded string must include the : separating the username
            and the password, even when the latter is empty. See
            http://www.ietf.org/rfc/rfc2617.txt for more information.
    
    R=golang-dev, bradfitz, adg
    CC=golang-dev
    https://golang.org/cl/8475043

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

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

元コミット内容

net/httpパッケージにおけるBasic認証の実装において、パスワードが空の場合に認証文字列のエンコードがRFC 2617の仕様に準拠していなかった問題を修正します。具体的には、ユーザー名とパスワードを区切るコロンが、パスワードが空の場合に省略されてしまう不具合がありました。このコミットは、パスワードが空であってもコロンが必ず含まれるように修正し、関連するテストを追加します。

変更の背景

HTTP Basic認証は、ユーザー名とパスワードをコロンで結合し、その文字列をBase64エンコードしてAuthorizationヘッダーに含めることで行われます。RFC 2617のセクション2(4ページ目末尾)には、「認証を受けるために、クライアントはユーザーIDとパスワードを、単一のコロン(:)文字で区切って、Base64エンコードされた文字列として資格情報内に送信する」と明記されています。

しかし、Goのnet/url.User型が提供するString()メソッドは、パスワードが設定されていない場合にコロンを含まない文字列を返していました。例えば、ユーザー名が"gopher"でパスワードが空の場合、u.String()は"gopher"を返していました。RFCの仕様では"gopher:"のようにコロンを含める必要があるため、この不一致が問題となっていました。

この不具合により、パスワードが空のBasic認証を要求するサーバーに対して、GoのHTTPクライアントが正しく認証情報を送信できないという問題が発生していました。このコミットは、このRFCの要件を満たすために、パスワードが空の場合でも明示的にコロンを追加するように修正することで、相互運用性の問題を解決することを目的としています。

前提知識の解説

HTTP Basic認証

HTTP Basic認証は、HTTPプロトコルでクライアントを認証するための最もシンプルな方法の一つです。

  1. サーバーからの要求: サーバーは、認証が必要なリソースへのアクセス要求を受け取ると、WWW-AuthenticateヘッダーにBasicスキームとレルム(認証領域)を含んだ401 Unauthorizedレスポンスを返します。
  2. クライアントの応答: クライアントは、ユーザー名とパスワードをコロン(:)で結合した文字列(例: username:password)を作成します。
  3. Base64エンコード: この文字列をBase64エンコードします。
  4. Authorizationヘッダー: エンコードされた文字列をAuthorizationヘッダーにBasic プレフィックスを付けて含め、再度リクエストを送信します(例: Authorization: Basic <base64_encoded_string>)。

RFC 2617

RFC 2617は「HTTP認証:基本認証とダイジェスト認証」を定義する標準ドキュメントです。このRFCは、HTTP/1.1における認証メカニズムの詳細を規定しており、特にBasic認証のエンコード形式に関する厳密なルールが含まれています。このコミットの背景にある問題は、RFC 2617のセクション2に記載されている「ユーザーIDとパスワードは単一のコロンで区切られる」という要件に直接関連しています。

Go言語のnet/httpパッケージ

net/httpパッケージは、Go言語でHTTPクライアントとサーバーを実装するための主要なパッケージです。このパッケージは、HTTPリクエストの作成、レスポンスの処理、ヘッダーの操作など、HTTP通信に必要な基本的な機能を提供します。

url.UserUserInfo.String()

Go言語のnet/urlパッケージには、URLのユーザー情報(ユーザー名とパスワード)を扱うためのUser型とUserInfo型があります。

  • url.User(username string): ユーザー名のみを持つUserInfoを生成します。
  • url.UserPassword(username, password string): ユーザー名とパスワードを持つUserInfoを生成します。
  • UserInfo.String(): UserInfoオブジェクトの文字列表現を返します。このメソッドの以前の挙動が、パスワードが空の場合にコロンを含まない原因でした。
  • UserInfo.Password(): パスワードと、パスワードが設定されているかどうかを示すブール値を返します。

base64.URLEncoding.EncodeToString

encoding/base64パッケージは、Base64エンコードとデコードの機能を提供します。URLEncodingは、URLセーフなBase64エンコード(+-に、/_に、パディング文字=を省略)を行うためのエンコーダです。EncodeToStringメソッドは、バイトスライスをBase64エンコードされた文字列に変換します。

技術的詳細

このコミットの技術的な核心は、HTTP Basic認証の仕様(RFC 2617)とGo言語のnet/url.UserInfo.String()メソッドの挙動の間の不一致を解消することにあります。

従来のnet/httpクライアントの実装では、リクエストのURL.Userフィールドから認証情報を取得し、そのString()メソッドの結果を直接Base64エンコードしていました。

req.Header.Set("Authorization", "Basic "+base64.URLEncoding.EncodeToString([]byte(u.String())))

ここで、u.String()UserInfo.String()を呼び出します。UserInfo.String()は、パスワードが設定されている場合にのみユーザー名とパスワードをコロンで区切って返します(例: username:password)。しかし、パスワードが空の場合(例: url.User("username")で作成された場合)、UserInfo.String()は単にユーザー名のみを返していました(例: username)。

RFC 2617の仕様では、パスワードが空であっても、ユーザー名とパスワードを区切るコロンは必須です。つまり、"username"ではなく"username:"という形式でBase64エンコードされるべきでした。

このコミットでは、この問題を解決するために、u.String()の結果を直接使用するのではなく、一度変数authに格納し、その後にパスワードが空であるかどうかをu.Password()メソッドで確認するロジックを追加しました。

auth := u.String()
if _, hasPassword := u.Password(); !hasPassword {
    auth += ":"
}
req.Header.Set("Authorization", "Basic "+base64.URLEncoding.EncodeToString([]byte(auth)))

u.Password()は、パスワード文字列と、パスワードが設定されているかどうかのブール値(hasPassword)を返します。!hasPasswordが真の場合、つまりパスワードが空の場合にのみ、明示的にコロンをauth文字列に追加します。これにより、base64.URLEncoding.EncodeToStringに渡される文字列が常にRFC 2617の要件を満たすようになります。

この修正により、GoのHTTPクライアントは、パスワードが空のBasic認証を要求するサーバーに対しても、正しく認証情報を送信できるようになり、より広範なHTTPサーバーとの互換性が確保されました。

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

src/pkg/net/http/client.go

--- a/src/pkg/net/http/client.go
+++ b/src/pkg/net/http/client.go
@@ -161,7 +161,18 @@ func send(req *Request, t RoundTripper) (resp *Response, err error) {
 	}\n 
 	if u := req.URL.User; u != nil {
-		req.Header.Set("Authorization", "Basic "+base64.URLEncoding.EncodeToString([]byte(u.String())))\n
+		auth := u.String()
+		// UserInfo.String() only returns the colon when the
+		// password is set, so we must add it here.
+		//
+		// See 2 (end of page 4) http://www.ietf.org/rfc/rfc2617.txt
+		// "To receive authorization, the client sends the userid and password,
+		// separated by a single colon (\":\") character, within a base64
+		// encoded string in the credentials."
+		if _, hasPassword := u.Password(); !hasPassword {
+			auth += ":"
+		}
+		req.Header.Set("Authorization", "Basic "+base64.URLEncoding.EncodeToString([]byte(auth)))
 	}
 	resp, err = t.RoundTrip(req)
 	if err != nil {

src/pkg/net/http/client_test.go

--- a/src/pkg/net/http/client_test.go
+++ b/src/pkg/net/http/client_test.go
@@ -10,6 +10,7 @@ import (
 	"bytes"
 	"crypto/tls"
 	"crypto/x509"
+	"encoding/base64"
 	"errors"
 	"fmt"
 	"io"
@@ -700,3 +701,37 @@ func TestClientHeadContentLength(t *testing.T) {
 		}
 	}
 }
+
+func TestEmptyPasswordAuth(t *testing.T) {
+	defer afterTest(t)
+	gopher := "gopher"
+	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+		auth := r.Header.Get("Authorization")
+		if strings.HasPrefix(auth, "Basic ") {
+			encoded := auth[6:]
+			decoded, err := base64.StdEncoding.DecodeString(encoded)
+			if err != nil {
+				t.Fatal(err)
+			}
+			expected := gopher + ":"
+			s := string(decoded)
+			if expected != s {
+				t.Errorf("Invalid Authorization header. Got %q, wanted %q", s, expected)
+			}
+		} else {
+			t.Errorf("Invalid auth %q", auth)
+		}
+	}))
+	defer ts.Close()
+	c := &Client{}
+	req, err := NewRequest("GET", ts.URL, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	req.URL.User = url.User(gopher)
+	resp, err := c.Do(req)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer resp.Body.Close()
+}

コアとなるコードの解説

src/pkg/net/http/client.goの変更

このファイルでは、HTTPリクエストを送信するsend関数内のBasic認証処理が変更されています。

  • 変更前:

    req.Header.Set("Authorization", "Basic "+base64.URLEncoding.EncodeToString([]byte(u.String())))
    

    ここでは、req.URL.Useru)のString()メソッドが返す文字列を直接Base64エンコードしていました。前述の通り、u.String()はパスワードが空の場合にコロンを含まないため、RFCの要件を満たしていませんでした。

  • 変更後:

    auth := u.String()
    // UserInfo.String() only returns the colon when the
    // password is set, so we must add it here.
    // ... (RFC 2617の引用コメント) ...
    if _, hasPassword := u.Password(); !hasPassword {
        auth += ":"
    }
    req.Header.Set("Authorization", "Basic "+base64.URLEncoding.EncodeToString([]byte(auth)))
    
    1. auth := u.String(): まず、u.String()の結果をauth変数に格納します。
    2. if _, hasPassword := u.Password(); !hasPassword { ... }: u.Password()メソッドを呼び出し、パスワードが設定されているかどうかを示すブール値hasPasswordを取得します。!hasPasswordが真の場合(つまり、パスワードが空の場合)に条件ブロックが実行されます。
    3. auth += ":": パスワードが空の場合、auth文字列の末尾に明示的にコロンを追加します。
    4. req.Header.Set("Authorization", "Basic "+base64.URLEncoding.EncodeToString([]byte(auth))): 最後に、修正されたauth文字列をBase64エンコードし、Authorizationヘッダーに設定します。

この変更により、パスワードが空の場合でも、エンコードされる文字列が常にusername:の形式となり、RFC 2617の仕様に準拠するようになりました。

src/pkg/net/http/client_test.goの変更

このファイルでは、新しいテスト関数TestEmptyPasswordAuthが追加されています。

  • import "encoding/base64": 新しいテストでBase64デコードを行うために、encoding/base64パッケージがインポートされています。
  • TestEmptyPasswordAuth関数:
    1. gopher := "gopher": テスト用のユーザー名を定義します。
    2. ts := httptest.NewServer(...): テスト用のHTTPサーバーをセットアップします。このサーバーは、受信したリクエストのAuthorizationヘッダーを検証します。
      • サーバー側のハンドラでは、AuthorizationヘッダーからBasic プレフィックスを取り除き、残りのBase64エンコードされた部分をデコードします。
      • デコードされた文字列がgopher:(ユーザー名とコロン)と一致するかどうかを検証します。一致しない場合はエラーを報告します。
    3. c := &Client{}: 新しいHTTPクライアントインスタンスを作成します。
    4. req, err := NewRequest("GET", ts.URL, nil): テストサーバーへのGETリクエストを作成します。
    5. req.URL.User = url.User(gopher): ここが重要です。url.User(gopher)を使用して、パスワードが空のユーザー情報をリクエストのURLに設定します。
    6. resp, err := c.Do(req): 作成したリクエストをクライアントで実行します。
    7. defer resp.Body.Close(): レスポンスボディを閉じます。

このテストは、パスワードが空のユーザー情報を持つリクエストが、修正されたnet/httpクライアントによって正しくBasic認証ヘッダー(gopher:がBase64エンコードされたもの)を生成し、送信することを確認します。これにより、修正が意図通りに機能していることが保証されます。

関連リンク

参考にした情報源リンク

  • RFC 2617 - HTTP Authentication: Basic and Digest Access Authentication: http://www.ietf.org/rfc/rfc2617.txt
  • Go言語のnet/httpパッケージドキュメント
  • Go言語のnet/urlパッケージドキュメント
  • Go言語のencoding/base64パッケージドキュメント