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

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

このコミットは、Go言語のwebsocketパッケージにおいて、不正なWebSocketリクエストに対するサーバーの応答方法を改善するものです。具体的には、WebSocketのハンドシェイクがプロトコル仕様に合致しない場合に、接続を単に閉じるのではなく、適切なHTTPエラーレスポンス(例: 400 Bad Request)を返すように変更されました。これにより、クライアント側がサーバーからのエラーをより明確に認識できるようになります。

コミット

  • コミットハッシュ: de3d64725288c4baa083b804d51f4d7ef35ea130
  • Author: Fumitoshi Ukai ukai@google.com
  • Date: Thu Nov 3 14:13:39 2011 +1100

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

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

元コミット内容

    websocket: return an error HTTP response for bad websocket request.
    
    websocket spec had changed server-side requiements to return
    an HTTP response with an appropriate error code (such as 400 Bad
    Request) when it finds client did not send a handshake that matches
    websocket protocol, rather than just closing connection.
    It needs to flush out response before closing connection.
    Fixes issues 2396.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5318072

変更の背景

この変更の背景には、WebSocketプロトコル仕様の更新があります。以前の仕様では、サーバーが不正なWebSocketハンドシェイクリクエストを検出した場合、単に接続を閉じるだけでよいとされていました。しかし、新しい仕様では、クライアントがWebSocketプロトコルに合致しないハンドシェイクを送信した場合、サーバーは適切なエラーコード(例: 400 Bad Request)を含むHTTPレスポンスを返すことが求められるようになりました。

この変更は、issue 2396https://github.com/golang/go/issues/2396)を修正するために行われました。このissueでは、不正なWebSocketリクエストに対してサーバーがHTTPエラーレスポンスを返さないため、クライアント側でデバッグが困難であるという問題が指摘されていました。サーバーが明確なエラーレスポンスを返すことで、クライアントは問題の原因を特定しやすくなり、より堅牢なアプリケーションを構築できるようになります。

また、レスポンスを送信した後、接続を閉じる前にバッファをフラッシュする必要があるという点も重要な変更点です。これは、レスポンスデータが実際にネットワークに送信されることを保証するために必要です。

前提知識の解説

WebSocketプロトコルとハンドシェイク

WebSocketは、Webブラウザとサーバー間で全二重通信チャネルを確立するためのプロトコルです。HTTPとは異なり、一度接続が確立されると、クライアントとサーバーは独立してデータを送受信できます。

WebSocket接続を確立するためには、「ハンドシェイク」と呼ばれるプロセスが必要です。これは、クライアントがHTTPリクエストを送信し、サーバーがHTTPレスポンスを返すことで行われます。このハンドシェイクには、WebSocketプロトコルに特有のヘッダー(例: Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Keyなど)が含まれており、これらが正しく交換されることでWebSocket接続が確立されます。

HTTPステータスコード

HTTPステータスコードは、HTTPリクエストの結果を示す3桁の数字です。このコミットで特に重要なのは以下のコードです。

  • 400 Bad Request: サーバーがクライアントからのリクエストを理解できない、または処理できない場合に返されます。構文が不正である、リクエストメッセージの形式が不正である、などの理由が考えられます。WebSocketハンドシェイクがプロトコル仕様に合致しない場合、これは適切なエラーコードとなります。

bufio.Writer.Flush()

Go言語のbufioパッケージは、バッファリングされたI/Oを提供します。bufio.Writerは、データを内部バッファに書き込み、バッファがいっぱいになったとき、または明示的にフラッシュされたときに、基になるio.Writerに書き込みます。

Flush()メソッドは、バッファに蓄積されたすべてのデータを、基になるio.Writerに強制的に書き出すために使用されます。このコミットでは、エラーレスポンスを送信した後、接続を閉じる前にFlush()を呼び出すことで、エラーレスポンスが確実にクライアントに送信されるようにしています。もしFlush()が呼び出されないと、バッファ内のデータがネットワークに送信される前に接続が閉じられてしまい、クライアントはエラーレスポンスを受け取れない可能性があります。

技術的詳細

このコミットは、主にsrc/pkg/websocket/server.gosrc/pkg/websocket/websocket_test.goの2つのファイルに変更を加えています。

src/pkg/websocket/server.goの変更

server.goでは、newServerConn関数内でWebSocketハンドシェイクの処理が行われます。変更のポイントは以下の通りです。

  1. エラーレスポンスのフラッシュ:
    • 既存のエラー処理パス(SupportedProtocolVersionが一致しない場合や、その他のエラーが発生した場合)において、buf.Flush()が追加されました。これにより、エラーメッセージがバッファリングされたまま接続が閉じられることを防ぎ、クライアントに確実にエラーレスポンスが送信されるようになります。
  2. 不正なハンドシェイクに対する400 Bad Requestの返却:
    • hs.AcceptHandshake(buf.Writer)がエラーを返した場合、以前は単にreturnしていました。
    • 変更後、このエラーパスにcode = http.StatusBadRequestを設定し、fmt.Fprintf(buf, "HTTP/1.1 %03d %s\\r\\n", code, http.StatusText(code))400 Bad RequestのHTTPステータスラインを書き込み、さらにbuf.WriteString("\\r\\n")でヘッダーの終わりを示し、最後にbuf.Flush()を呼び出してレスポンスをフラッシュするように変更されました。これにより、WebSocketハンドシェイクがプロトコルに違反している場合に、サーバーが明確なHTTPエラーレスポンスを返すようになります。

src/pkg/websocket/websocket_test.goの変更

websocket_test.goでは、TestHTTP関数内のテストロジックが変更されました。

  1. テストの期待値の変更:
    • 以前は、不正なWebSocketリクエスト(通常のHTTP GETリクエスト)を送信した場合、サーバーがWebSocket接続を「中止する」ことを期待し、http.Getがエラーを返すことを検証していました。具体的には、io.ErrUnexpectedEOFエラーを期待していました。
    • 変更後、新しいWebSocket仕様に合わせて、サーバーが400 Bad RequestのHTTPレスポンスを返すことを期待するようにテストが修正されました。
  2. レスポンスの検証:
    • http.Getの戻り値としてresp(HTTPレスポンス)を受け取るように変更されました。
    • resp.StatusCodehttp.StatusBadRequest(400)と等しいことを検証するようになりました。これにより、サーバーが期待通りにHTTPエラーレスポンスを返しているかを確認します。

これらの変更により、Go言語のwebsocketパッケージは、最新のWebSocketプロトコル仕様に準拠し、不正なリクエストに対するサーバーの振る舞いがより明確でデバッグしやすくなりました。

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

diff --git a/src/pkg/websocket/server.go b/src/pkg/websocket/server.go
index 9420c47191..8f16517c03 100644
--- a/src/pkg/websocket/server.go
+++ b/src/pkg/websocket/server.go
@@ -20,6 +20,7 @@ func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Requ
 		fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\\r\\n", SupportedProtocolVersion)
 		buf.WriteString("\\r\\n")
 		buf.WriteString(err.Error())
+		buf.Flush()\n 		return
 	}
 	if err != nil {
@@ -34,12 +35,17 @@ func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Requ
 		fmt.Fprintf(buf, "HTTP/1.1 %03d %s\\r\\n", code, http.StatusText(code))\n 		buf.WriteString("\\r\\n")
 		buf.WriteString(err.Error())
+		buf.Flush()\n 		return
 	}
 	config.Protocol = nil
 
 	err = hs.AcceptHandshake(buf.Writer)
 	if err != nil {
+		code = http.StatusBadRequest
+		fmt.Fprintf(buf, "HTTP/1.1 %03d %s\\r\\n", code, http.StatusText(code))\n+		buf.WriteString("\\r\\n")
+		buf.Flush()\n 		return
 	}
 	conn = hs.NewServerConn(buf, rwc, req)
diff --git a/src/pkg/websocket/websocket_test.go b/src/pkg/websocket/websocket_test.go
index 69b5335cfa..25fe264673 100644
--- a/src/pkg/websocket/websocket_test.go
+++ b/src/pkg/websocket/websocket_test.go
@@ -200,20 +200,19 @@ func TestHTTP(t *testing.T) {
 	once.Do(startServer)
 
 	// If the client did not send a handshake that matches the protocol
-\t// specification, the server should abort the WebSocket connection.
-\t_, err := http.Get(fmt.Sprintf("http://%s/echo", serverAddr))\n-\tif err == nil {\n-\t\tt.Error("Get: unexpected success")
+\t// specification, the server MUST return an HTTP respose with an
+\t// appropriate error code (such as 400 Bad Request)
+\tresp, err := http.Get(fmt.Sprintf("http://%s/echo", serverAddr))\n+\tif err != nil {\n+\t\tt.Errorf("Get: error %#v", err)
 \t\treturn
 \t}
-\turlerr, ok := err.(*url.Error)\n-\tif !ok {\n-\t\tt.Errorf("Get: not url.Error %#v", err)
+\tif resp == nil {\n+\t\tt.Error("Get: resp is null")
 \t\treturn
 \t}
-\tif urlerr.Err != io.ErrUnexpectedEOF {\n-\t\tt.Errorf("Get: error %#v", err)
-\t\treturn
+\tif resp.StatusCode != http.StatusBadRequest {\n+\t\tt.Errorf("Get: expected %q got %q", http.StatusBadRequest, resp.StatusCode)
 \t}
 }\n \n```

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

### `src/pkg/websocket/server.go`

-   **`+ buf.Flush()`の追加**:
    -   `SupportedProtocolVersion`が一致しない場合のエラーパスと、その他の一般的なエラーパスの両方に`buf.Flush()`が追加されました。これにより、エラーメッセージがバッファに留まることなく、すぐにクライアントに送信されることが保証されます。
-   **不正なハンドシェイク処理の追加**:
    -   `err = hs.AcceptHandshake(buf.Writer)`がエラーを返した場合のブロックが大幅に変更されました。
    -   `+ code = http.StatusBadRequest`: HTTPステータスコードを`400 Bad Request`に設定します。
    -   `+ fmt.Fprintf(buf, "HTTP/1.1 %03d %s\\r\\n", code, http.StatusText(code))`: HTTPステータスライン(例: `HTTP/1.1 400 Bad Request`)をバッファに書き込みます。
    -   `+ buf.WriteString("\\r\\n")`: HTTPヘッダーの終わりを示す空行を書き込みます。
    -   `+ buf.Flush()`: これらすべてのレスポンスデータをクライアントにフラッシュします。
    -   これにより、WebSocketハンドシェイクが失敗した場合に、サーバーは単に接続を閉じるのではなく、明確なHTTPエラーレスポンスを返すようになります。

### `src/pkg/websocket/websocket_test.go`

-   **コメントの変更**:
    -   テストの意図を示すコメントが「サーバーはWebSocket接続を中止すべき」から「サーバーは適切なエラーコード(例: 400 Bad Request)を含むHTTPレスポンスを返すべき」に変更されました。これは、新しいWebSocket仕様に合わせたテストの目的を明確にしています。
-   **`http.Get`の戻り値の変更**:
    -   以前は`_, err := http.Get(...)`としてエラーのみをチェックしていましたが、`resp, err := http.Get(...)`に変更され、HTTPレスポンスオブジェクトも取得するようになりました。
-   **エラーチェックロジックの変更**:
    -   以前は`err == nil`の場合にエラーを報告し、`url.Error`型にキャストして`io.ErrUnexpectedEOF`を期待していました。これは、接続が予期せず閉じられることを期待するテストでした。
    -   新しいロジックでは、まず`err != nil`でネットワークレベルのエラーがないかを確認します。
    -   次に、`resp == nil`でないことを確認します。
    -   最も重要な変更は、`resp.StatusCode != http.StatusBadRequest`をチェックする点です。これにより、サーバーが`400 Bad Request`ステータスコードを返したかどうかを直接検証します。これは、新しい仕様に準拠したサーバーの振る舞いをテストするための適切なアサーションです。

これらの変更により、Go言語のWebSocket実装は、不正なハンドシェイクに対するサーバーの応答を改善し、より堅牢でデバッグしやすいものになりました。

## 関連リンク

-   GitHubコミットページ: [https://github.com/golang/go/commit/de3d64725288c4baa083b804d51f4d7ef35ea130](https://github.com/golang/go/commit/de3d64725288c4baa083b804d51f4d7ef35ea130)
-   Go Issue 2396: [https://github.com/golang/go/issues/2396](https://github.com/golang/go/issues/2396)
-   Go CL 5318072: [https://golang.org/cl/5318072](https://golang.org/cl/5318072)

## 参考にした情報源リンク

-   [WebSocket Protocol (RFC 6455)](https://datatracker.ietf.org/doc/html/rfc6455) (特にハンドシェイクとエラー処理に関するセクション)
-   [HTTP Status Codes - MDN Web Docs](https://developer.mozilla.org/ja/docs/Web/HTTP/Status)
-   [Go言語 `bufio`パッケージのドキュメント](https://pkg.go.dev/bufio)
-   [Go言語 `net/http`パッケージのドキュメント](https://pkg.go.dev/net/http)