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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける Server 構造体に ErrorLog フィールドを追加し、TLSハンドシェイクエラーを適切にログに記録するように変更するものです。これにより、HTTPサーバーがTLSハンドシェイク中に発生するエラーを捕捉し、開発者や運用者が問題を特定しやすくなります。

コミット

commit 281088b1f087aff44c4a928734871fe94f82b88b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Feb 28 12:12:51 2014 -0800

    net/http: add Server.ErrorLog; log and test TLS handshake errors
    
    Fixes #7291
    
    LGTM=agl
    R=golang-codereviews, agl
    CC=agl, golang-codereviews
    https://golang.org/cl/70250044

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

https://github.com/golang/go/commit/281088b1f087aff44c4a928734871fe94f82b88b

元コミット内容

net/http: add Server.ErrorLog; log and test TLS handshake errors

このコミットは、net/http パッケージの ServerErrorLog フィールドを追加し、TLSハンドシェイクエラーをログに記録し、そのテストを追加するものです。

変更の背景

この変更は、Go issue #7291「net/http: server TLS handshake failures are silent」を修正するために行われました。以前の net/http パッケージでは、HTTPサーバーがTLSハンドシェイク中にエラーに遭遇した場合、そのエラーが適切にログに記録されず、サイレントに失敗していました。これにより、開発者や運用者はTLS接続の問題をデバッグすることが困難でした。

具体的には、serve() 関数内で tlsConn.Handshake() が呼び出されていましたが、そこで発生したエラーが外部に公開されず、デバッグに役立つ情報が得られませんでした。このコミットは、この問題を解決し、TLSハンドシェイクエラーを Server.ErrorLog を通じてログに出力することで、可視性を向上させることを目的としています。

前提知識の解説

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

net/http パッケージは、Go言語でHTTPクライアントとサーバーを実装するための標準ライブラリです。このパッケージを使用することで、WebアプリケーションやAPIサーバーを簡単に構築できます。

http.Server 構造体

http.Server は、HTTPサーバーの設定をカプセル化する構造体です。ポート、ハンドラー、タイムアウトなどの設定を保持します。このコミット以前は、エラーロギングのための専用フィールドはありませんでした。

log.Logger

log パッケージは、Go言語の標準ロギングライブラリです。log.Logger は、ログメッセージの出力先(io.Writer)、プレフィックス、フラグなどを設定できるロガーです。

TLSハンドシェイク

TLS (Transport Layer Security) ハンドシェイクは、クライアントとサーバーが安全な通信チャネルを確立するために行う一連のプロトコルです。このプロセスには、証明書の交換、暗号スイートのネゴシエーション、鍵の生成などが含まれます。ハンドシェイク中にエラーが発生すると、安全な接続を確立できません。

httptest パッケージ

httptest パッケージは、HTTPテストを容易にするためのユーティリティを提供します。httptest.NewTLSServer は、テスト用のTLSサーバーを簡単に起動できる関数です。

技術的詳細

このコミットの主要な変更点は以下の通りです。

  1. http.Server への ErrorLog フィールドの追加: http.Server 構造体に ErrorLog *log.Logger フィールドが追加されました。これにより、サーバー固有のエラーロガーを設定できるようになります。ErrorLognil の場合、デフォルトで log パッケージの標準ロガー(os.Stderr に出力)が使用されます。

  2. Server.logf ヘルパーメソッドの導入: Server 構造体に logf というプライベートなヘルパーメソッドが追加されました。このメソッドは、Server.ErrorLog が設定されていればそれを使用し、そうでなければ log.Printf を使用してログメッセージを出力します。これにより、サーバー内の様々な場所からのエラーロギングを一元的に管理できるようになりました。

  3. TLSハンドシェイクエラーのロギング: conn.serve() メソッド内で、TLSハンドシェイク (tlsConn.Handshake()) が失敗した場合に、そのエラーを c.server.logf を使用してログに記録するようになりました。これにより、TLS接続の問題が発生した際に、具体的なエラーメッセージがログに出力されるようになります。

  4. 既存のロギング箇所の logf への移行: response 構造体内の WriteHeaderWrite メソッド、conn.serve() 内のパニック処理など、既存の log.Printlog.Printf を使用していたエラーロギング箇所が、新しく導入された c.conn.server.logf または c.server.logf を使用するように変更されました。これにより、サーバー全体のエラーロギングが ErrorLog フィールドによって制御されるようになりました。

  5. テストの追加: client_test.goserve_test.go に、TLSハンドシェイクエラーが ErrorLog に適切に記録されることを検証するテストが追加されました。

    • TestClientInsecureTransportTestClientWithIncorrectTLSServerName では、不正な証明書やサーバー名によるTLSハンドシェイクエラーが ErrorLog に出力されることを確認しています。
    • TestTLSHandshakeTimeout では、TLSハンドシェイクのタイムアウトエラーが ErrorLog に出力されることを確認しています。 これらのテストでは、カスタムの io.WriterErrorLog に設定し、そのライターに書き込まれたログメッセージの内容を検証しています。

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

src/pkg/net/http/server.go

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -615,11 +615,11 @@ const maxPostHandlerReadBytes = 256 << 10
 
 func (w *response) WriteHeader(code int) {
 	if w.conn.hijacked() {
-		log.Print("http: response.WriteHeader on hijacked connection")
+		w.conn.server.logf("http: response.WriteHeader on hijacked connection")
 		return
 	}
 	if w.wroteHeader {
-		log.Print("http: multiple response.WriteHeader calls")
+		w.conn.server.logf("http: multiple response.WriteHeader calls")
 		return
 	}
 	w.wroteHeader = true
@@ -634,7 +634,7 @@ func (w *response) WriteHeader(code int) {
 		if err == nil && v >= 0 {
 			w.contentLength = v
 		} else {
-			log.Printf("http: invalid Content-Length of %q", cl)
+			w.conn.server.logf("http: invalid Content-Length of %q", cl)
 			w.handlerHeader.Del("Content-Length")
 		}
 	}
@@ -817,7 +817,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
 	if hasCL && hasTE && te != "identity" {
 		// TODO: return an error if WriteHeader gets a return parameter
 		// For now just ignore the Content-Length.
-		log.Printf("http: WriteHeader called with both Transfer-Encoding of %q and a Content-Length of %d",
+		w.conn.server.logf("http: WriteHeader called with both Transfer-Encoding of %q and a Content-Length of %d",
 		\tte, w.contentLength)
 		delHeader("Content-Length")
 		hasCL = false
@@ -963,7 +963,7 @@ func (w *response) WriteString(data string) (n int, err error) {
 // either dataB or dataS is non-zero.
 func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
 	if w.conn.hijacked() {
-		log.Print("http: response.Write on hijacked connection")
+		w.conn.server.logf("http: response.Write on hijacked connection")
 		return 0, ErrHijacked
 	}
 	if !w.wroteHeader {
@@ -1096,7 +1096,7 @@ func (c *conn) serve() {
 			const size = 64 << 10
 			buf := make([]byte, size)
 			buf = buf[:runtime.Stack(buf, false)]
-			log.Printf("http: panic serving %v: %v\\n%s", c.remoteAddr, err, buf)
+			c.server.logf("http: panic serving %v: %v\\n%s", c.remoteAddr, err, buf)
 		}
 		if !c.hijacked() {
 			c.close()
@@ -1112,6 +1112,7 @@ func (c *conn) serve() {\n 			c.rwc.SetWriteDeadline(time.Now().Add(d))\n 		}\n 		if err := tlsConn.Handshake(); err != nil {\n+			c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)\n 			return\n 		}\n 		c.tlsState = new(tls.ConnectionState)\
@@ -1604,6 +1605,12 @@ type Server struct {\n 	// ConnState type and associated constants for details.\n 	ConnState func(net.Conn, ConnState)\n \n+\t// ErrorLog specifies an optional logger for errors accepting\n+\t// connections and unexpected behavior from handlers.\n+\t// If nil, logging goes to os.Stderr via the log package's\n+\t// standard logger.\n+\tErrorLog *log.Logger\n+\n \tdisableKeepAlives int32 // accessed atomically.\n }\n \n@@ -1704,7 +1711,7 @@ func (srv *Server) Serve(l net.Listener) error {\n 			if max := 1 * time.Second; tempDelay > max {\n 				tempDelay = max\n 			}\n-			log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay)\n+			srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)\n 			time.Sleep(tempDelay)\n 			continue\n 		}\n@@ -1735,6 +1742,14 @@ func (s *Server) SetKeepAlivesEnabled(v bool) {\n 	}\n }\n \n+func (s *Server) logf(format string, args ...interface{}) {\n+\tif s.ErrorLog != nil {\n+\t\ts.ErrorLog.Printf(format, args...)\n+\t} else {\n+\t\tlog.Printf(format, args...)\n+\t}\n+}\n+\n // ListenAndServe listens on the TCP network address addr\n // and then calls Serve with handler to handle requests\n // on incoming connections.  Handler is typically nil,\n```

### `src/pkg/net/http/client_test.go` および `src/pkg/net/http/serve_test.go`

これらのファイルには、`Server.ErrorLog` を設定し、TLSハンドシェイクエラーがログに記録されることを検証するテストコードが追加されています。具体的には、`httptest.NewTLSServer` や `httptest.NewUnstartedServer` で作成したテストサーバーの `Config.ErrorLog` にカスタムの `log.Logger` を設定し、そのロガーが期待するエラーメッセージを受け取ったかどうかを `chanWriter` を使って確認しています。

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

### `Server` 構造体への `ErrorLog` フィールド追加

```go
type Server struct {
	// ... 既存のフィールド ...

	// ErrorLog specifies an optional logger for errors accepting
	// connections and unexpected behavior from handlers.
	// If nil, logging goes to os.Stderr via the log package's
	// standard logger.
	ErrorLog *log.Logger

	disableKeepAlives int32 // accessed atomically.
}

この変更により、http.Server のインスタンスを作成する際に、カスタムのロガーを ErrorLog フィールドに設定できるようになりました。これにより、アプリケーションのロギングシステムとHTTPサーバーのエラーログを統合したり、特定のエラーログを別のファイルやサービスにルーティングしたりすることが可能になります。

logf ヘルパーメソッド

func (s *Server) logf(format string, args ...interface{}) {
	if s.ErrorLog != nil {
		s.ErrorLog.Printf(format, args...)
	} else {
		log.Printf(format, args...)
	}
}

logf メソッドは、Server 内部でエラーをログに記録する際に使用される統一されたインターフェースを提供します。このメソッドは、Server.ErrorLog が設定されているかどうかをチェックし、設定されていればそのロガーを使用し、そうでなければ標準の log.Printf を使用します。これにより、ロギングの動作を外部から簡単にカスタマイズできるようになりました。

TLSハンドシェイクエラーのロギング

// src/pkg/net/http/server.go の conn.serve() メソッド内
// ...
		if err := tlsConn.Handshake(); err != nil {
			c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
			return
		}
// ...

このコードスニペットは、TLSハンドシェイクが失敗した場合に、そのエラーを Server.ErrorLog に記録する部分です。tlsConn.Handshake() がエラーを返した場合、c.server.logf を呼び出して、エラーメッセージとリモートアドレスをログに出力します。これにより、TLS接続の問題が発生した際に、どのクライアントからの接続で、どのようなエラーが発生したのかを明確に把握できるようになりました。

既存のロギング箇所の変更

コミットの差分を見ると、response.WriteHeaderresponse.Writeconn.serve 内のパニック処理、Server.Serve 内の Accept エラー処理など、既存の log.Printlog.Printf を使用していた箇所が、すべて c.conn.server.logf または srv.logf を使用するように変更されています。これにより、HTTPサーバーからのすべてのエラーログが Server.ErrorLog の設定に従って出力されるようになり、ロギングの一貫性と制御性が向上しました。

関連リンク

参考にした情報源リンク