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

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

このコミットは、Go言語の標準ライブラリであるnet/smtpパッケージにおける認証エラーの取り扱いを改善するものです。具体的には、SMTP認証プロセス中に発生したエラーが適切に保持されず、破棄されてしまう問題を修正しています。これにより、認証失敗時のデバッグやエラーハンドリングがより正確に行えるようになります。

コミット

commit 64441d6d6605a9fbf27f163afbc359d9cb1af0cc
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date:   Fri Jun 28 12:24:45 2013 -0700

    net/smtp: preserve Auth errors
    
    If authentication failed, the initial error was being thrown away.
    
    Fixes #5700.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/10744043

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

https://github.com/golang/go/commit/64441d6d6605a9fbf27f163afbc359d9cb1af0cc

元コミット内容

このコミットの元の内容は以下の通りです。

    net/smtp: preserve Auth errors
    
    If authentication failed, the initial error was being thrown away.
    
    Fixes #5700.

これは、net/smtpパッケージにおいて、認証プロセス中に発生したエラーが適切に保持されずに破棄されてしまうという問題に対する修正であることを示しています。特に、GitHub Issue #5700を解決することを目的としています。

変更の背景

この変更の背景には、Go言語のnet/smtpパッケージがSMTPサーバーとの認証を行う際に、認証失敗時にサーバーから返されるエラーメッセージが適切に処理されず、失われてしまうという問題がありました。具体的には、Issue #5700「Truncated multiline error response from smtp.SendMail」で報告されたように、smtp.SendMailがSMTPサーバーからの複数行にわたるエラー応答を途中で切り捨ててしまい、認証失敗の原因を特定することが困難になるという状況が発生していました。

SMTPプロトコルでは、認証プロセス中にサーバーがエラーコード(例: 535 Invalid credentials)とそれに続く詳細なメッセージを返すことがあります。しかし、既存の実装では、このエラー情報がAuthインターフェースのNextメソッドに渡される前に、内部で上書きされたり、適切に伝播されなかったりするケースがありました。その結果、アプリケーション側では認証が失敗したことしか分からず、具体的な失敗理由(例: 無効な認証情報、アカウントロックなど)を把握できないという問題が生じていました。

このコミットは、このエラー情報の破棄を防ぎ、認証プロセス中に発生したエラーがAuthインターフェースを通じて適切に呼び出し元に伝達されるようにすることで、デバッグの容易性とエラーハンドリングの精度を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. SMTP (Simple Mail Transfer Protocol):

    • 電子メールの送信に使用されるインターネット標準プロトコルです。クライアント(メール送信者)とサーバー(メールサーバー)間でメールを転送する際に使用されます。
    • SMTPセッションは、通常、クライアントがサーバーに接続し、EHLO/HELOコマンドで自己紹介し、MAIL FROM、RCPT TO、DATAなどのコマンドでメールの内容を送信する流れで進行します。
    • サーバーは、各コマンドに対して数値コード(例: 250 OK, 550 Requested action not taken)とテキストメッセージで応答します。
  2. SMTP認証 (SMTP AUTH):

    • SMTPサーバーがメール送信を許可する前に、クライアントの身元を確認するためのメカニズムです。これにより、スパム送信などを防ぎます。
    • AUTHコマンドに続いて、PLAIN、LOGIN、CRAM-MD5などの認証メカニズムが使用されます。
    • 認証プロセスは、クライアントとサーバー間でチャレンジ・レスポンス形式でやり取りされることがあります。例えば、サーバーがチャレンジ(例: 334 VXNlcm5hbWU6)を送り、クライアントがそれに対する応答(例: ユーザー名やパスワードのBase64エンコード)を返す、といった形です。
  3. Go言語のnet/smtpパッケージ:

    • Go言語でSMTPクライアントを実装するための標準ライブラリです。
    • Client構造体はSMTPサーバーとの接続を管理し、EHLO、Auth、Mail、Rcpt、Dataなどのメソッドを提供します。
    • Authインターフェース: SMTP認証メカニズムを抽象化するためのインターフェースです。
      type Auth interface {
          Start(server *ServerInfo) (proto string, toServer []byte, err error)
          Next(fromServer []byte, more bool) (toServer []byte, err error)
      }
      
      • Start: 認証プロセスの開始時に呼び出され、使用する認証プロトコルと最初のクライアント応答を返します。
      • Next: サーバーからの応答を受け取り、次のクライアント応答を生成します。moreがtrueの場合、サーバーはさらなる応答を期待しています。このメソッドがエラーを返すと、認証プロセスは中断されます。
  4. net/textprotoパッケージ:

    • Go言語でテキストベースのネットワークプロトコル(HTTP、SMTP、NNTPなど)を扱うための低レベルなユーティリティを提供するパッケージです。
    • textproto.Error構造体は、プロトコルエラーを表すために使用されます。これには、数値コード(例: SMTPの応答コード)とメッセージが含まれます。
  5. エラーハンドリングの重要性:

    • Go言語では、エラーは戻り値として明示的に扱われます。関数がエラーを返す可能性がある場合、そのエラーを適切にチェックし、処理することが重要です。
    • 特にネットワーク通信においては、様々な理由でエラーが発生する可能性があるため、エラーメッセージを正確に把握することは問題の診断に不可欠です。

技術的詳細

このコミットの技術的な核心は、net/smtpパッケージのClient.Authメソッドにおけるエラー処理ロジックの変更にあります。

Client.Authメソッドは、SMTPサーバーとの認証プロセスを管理します。このプロセスは、クライアントがAUTHコマンドを送信し、サーバーからの応答に基づいてAuthインターフェースのNextメソッドを繰り返し呼び出すことで進行します。

変更前のコードでは、Client.Authメソッド内でサーバーからの応答を処理する際に、textproto.Error型のerr変数が使用されていました。このerr変数は、サーバーからの応答コードがエラーを示す場合(例: 5xx番台)に設定されます。しかし、その後のa.Next(msg, code == 334)の呼び出しにおいて、a.Nextが返すエラーが、既に設定されていたerr変数を上書きしてしまう可能性がありました。

具体的には、以下のようなシナリオが考えられます。

  1. クライアントがAUTHコマンドを送信。
  2. サーバーが認証失敗を示すエラーコード(例: 535 "Invalid credentials")を返す。この時点で、err変数にはこのサーバーエラーが設定される。
  3. しかし、その直後にa.Next(msg, code == 334)が呼び出される。もしa.Nextメソッドが何らかの理由でnilではないエラーを返した場合、そのエラーが元のサーバーエラーを上書きしてしまう。
  4. 結果として、Client.Authメソッドから返されるエラーは、サーバーが返した認証失敗の具体的なエラーではなく、a.Nextが生成した別のエラー、あるいはnilになってしまう可能性があった。

この問題は、特にSMTPサーバーが認証失敗時に詳細なエラーメッセージを返す場合(例: "535 Invalid credentials\nplease see www.example.com")に顕著でした。元のコードでは、この詳細なエラーがa.Nextの呼び出しによって失われ、デバッグが困難になっていました。

このコミットでは、a.Nextメソッドを呼び出す前に、既存のerr変数がnilであるかどうかをチェックする条件を追加することで、この問題を解決しています。

if err == nil {
    resp, err = a.Next(msg, code == 334)
}

この変更により、もしサーバーが既にエラーを返している場合(つまり、errnilではない場合)、a.Nextは呼び出されません。これにより、サーバーから返された認証失敗のエラー情報が保持され、Client.Authメソッドの最終的な戻り値として適切に伝播されるようになります。

また、このコミットにはテストケースTestAuthFailedが追加されており、認証失敗時にサーバーから返されたエラーメッセージが正しくキャプチャされることを検証しています。このテストは、擬似的なSMTPサーバーとクライアントのやり取りをシミュレートし、認証失敗時のエラーメッセージが期待通りに取得できることを確認しています。

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

変更はsrc/pkg/net/smtp/smtp.goファイルとsrc/pkg/net/smtp/smtp_test.goファイルにあります。

src/pkg/net/smtp/smtp.go

--- a/src/pkg/net/smtp/smtp.go
+++ b/src/pkg/net/smtp/smtp.go
@@ -196,7 +196,9 @@ func (c *Client) Auth(a Auth) error {
 		default:
 			err = &textproto.Error{Code: code, Msg: msg64}
 		}
-		resp, err = a.Next(msg, code == 334)
+		if err == nil {
+			resp, err = a.Next(msg, code == 334)
+		}
 		if err != nil {
 			// abort the AUTH
 			c.cmd(501, "*")

src/pkg/net/smtp/smtp_test.go

テストケースTestAuthFailedが追加されています。このテストは、認証失敗時のSMTPサーバーの応答をシミュレートし、Client.Authが正しいエラーを返すことを検証します。

--- a/src/pkg/net/smtp/smtp_test.go
+++ b/src/pkg/net/smtp/smtp_test.go
@@ -504,3 +504,47 @@ SendMail is working for me.\n .\n QUIT\n `
+\n+func TestAuthFailed(t *testing.T) {\n+	server := strings.Join(strings.Split(authFailedServer, "\\n"), "\\r\\n")\n+	client := strings.Join(strings.Split(authFailedClient, "\\n"), "\\r\\n")\n+	var cmdbuf bytes.Buffer\n+	bcmdbuf := bufio.NewWriter(&cmdbuf)\n+	var fake faker\n+	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)\n+	c, err := NewClient(fake, "fake.host")\n+	if err != nil {\n+		t.Fatalf("NewClient: %v", err)\n+	}\n+	defer c.Close()\n+\n+\tc.tls = true\n+\tc.serverName = "smtp.google.com"\n+\terr = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))\n+\n+\tif err == nil {\n+\t\tt.Error("Auth: expected error; got none")\n+\t} else if err.Error() != "535 Invalid credentials\\nplease see www.example.com" {\n+\t\tt.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\\nplease see www.example.com")\n+\t}\n+\n+\tbcmdbuf.Flush()\n+\tactualcmds := cmdbuf.String()\n+\tif client != actualcmds {\n+\t\tt.Errorf("Got:\\n%s\\nExpected:\\n%s", actualcmds, client)\n+\t}\n+}\n+\n+var authFailedServer = `220 hello world\n+250-mx.google.com at your service\n+250 AUTH LOGIN PLAIN\n+535-Invalid credentials\n+535 please see www.example.com\n+221 Goodbye\n+`\n+\n+var authFailedClient = `EHLO localhost\n+AUTH PLAIN AHVzZXIAcGFzcw==\n+*\n+QUIT\n+`\n```

## コアとなるコードの解説

### `src/pkg/net/smtp/smtp.go`の変更点

変更の中心は、`Client.Auth`メソッド内の以下の部分です。

```go
 		default:
 			err = &textproto.Error{Code: code, Msg: msg64}
 		}
-		resp, err = a.Next(msg, code == 334)
+		if err == nil {
+			resp, err = a.Next(msg, code == 334)
+		}
 		if err != nil {
 			// abort the AUTH
 			c.cmd(501, "*")
  • 変更前: resp, err = a.Next(msg, code == 334)

    • この行は、サーバーからの応答(msg)と、サーバーがさらなる応答を期待しているか(code == 334、これはSMTPのチャレンジコード)に基づいて、AuthインターフェースのNextメソッドを呼び出します。
    • 問題は、この呼び出しが常に実行され、a.Nextが返すエラーが、その直前のerr = &textproto.Error{...}で設定されたエラーを無条件に上書きしてしまう可能性があったことです。もしサーバーが認証失敗のエラーを返した後、a.Nextがエラーを返さなかった場合、元のサーバーエラーが失われてしまいます。
  • 変更後:

    if err == nil {
        resp, err = a.Next(msg, code == 334)
    }
    
    • この変更により、a.Nextメソッドの呼び出しが条件付きになりました。
    • if err == nilという条件は、「もし現時点でエラーがまだ発生していない場合のみ、a.Nextを呼び出す」という意味です。
    • これにより、もしSMTPサーバーが既に認証失敗のエラー(例: 535)を返しており、そのエラーがerr変数に設定されている場合、a.Nextは呼び出されません。その結果、サーバーから返された認証失敗の具体的なエラー情報が保持され、Client.Authメソッドの呼び出し元に正確に伝達されるようになります。
    • この修正は、エラーが「破棄される」ことを防ぎ、認証プロセスにおけるエラーハンドリングの堅牢性を向上させます。

src/pkg/net/smtp/smtp_test.goの追加テスト

TestAuthFailed関数は、この修正が正しく機能することを検証するための重要なテストケースです。

  • authFailedServerauthFailedClient:

    • これらは、擬似的なSMTPサーバーとクライアントのやり取りを文字列として定義しています。
    • authFailedServerは、サーバーがAUTH LOGIN PLAINコマンドに応答して535-Invalid credentials535 please see www.example.comというエラーを返すシナリオをシミュレートしています。これは、認証失敗時に詳細なエラーメッセージが返される典型的なケースです。
    • authFailedClientは、クライアントがEHLOAUTH PLAINコマンドを送信し、その後QUITする流れを示しています。
  • faker構造体とNewClient:

    • テストでは、実際のネットワーク接続の代わりにfaker構造体を使用しています。これは、io.ReadWriterインターフェースを実装しており、事前に定義されたサーバー応答を読み込み、クライアントのコマンドをバッファに書き込むことができます。
    • NewClient(fake, "fake.host")は、この擬似的な接続を使用してsmtp.Clientインスタンスを作成します。
  • c.Auth(PlainAuth(...)):

    • PlainAuthは、ユーザー名とパスワードを用いたプレーンテキスト認証メカニズムを実装するAuthインターフェースのインスタンスを返します。
    • このテストでは、意図的に認証が失敗するように設定されています。
  • エラーの検証:

    if err == nil {
        t.Error("Auth: expected error; got none")
    } else if err.Error() != "535 Invalid credentials\\nplease see www.example.com" {
        t.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\\nplease www.example.com")
    }
    
    • この部分がテストの核心です。c.Authがエラーを返さない場合はテストが失敗します。
    • さらに重要なのは、返されたエラーメッセージが期待される「535 Invalid credentials\nplease see www.example.com」と完全に一致するかどうかを検証している点です。これにより、認証失敗時にサーバーから返された詳細なエラーメッセージが、修正によって正しく保持されていることが確認されます。
  • コマンドの検証:

    bcmdbuf.Flush()
    actualcmds := cmdbuf.String()
    if client != actualcmds {
        t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
    }
    
    • この部分は、クライアントがSMTPサーバーに送信したコマンドが、期待されるauthFailedClientの文字列と一致するかどうかを検証しています。これにより、認証プロセス中のクライアントの挙動が正しいことも確認されます。

このテストケースの追加は、修正が意図した通りに機能し、認証失敗時のエラー情報が正確に伝達されることを保証する上で不可欠です。

関連リンク

参考にした情報源リンク