[インデックス 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 2396
(https://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.go
とsrc/pkg/websocket/websocket_test.go
の2つのファイルに変更を加えています。
src/pkg/websocket/server.go
の変更
server.go
では、newServerConn
関数内でWebSocketハンドシェイクの処理が行われます。変更のポイントは以下の通りです。
- エラーレスポンスのフラッシュ:
- 既存のエラー処理パス(
SupportedProtocolVersion
が一致しない場合や、その他のエラーが発生した場合)において、buf.Flush()
が追加されました。これにより、エラーメッセージがバッファリングされたまま接続が閉じられることを防ぎ、クライアントに確実にエラーレスポンスが送信されるようになります。
- 既存のエラー処理パス(
- 不正なハンドシェイクに対する
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
関数内のテストロジックが変更されました。
- テストの期待値の変更:
- 以前は、不正なWebSocketリクエスト(通常のHTTP GETリクエスト)を送信した場合、サーバーがWebSocket接続を「中止する」ことを期待し、
http.Get
がエラーを返すことを検証していました。具体的には、io.ErrUnexpectedEOF
エラーを期待していました。 - 変更後、新しいWebSocket仕様に合わせて、サーバーが
400 Bad Request
のHTTPレスポンスを返すことを期待するようにテストが修正されました。
- 以前は、不正なWebSocketリクエスト(通常のHTTP GETリクエスト)を送信した場合、サーバーがWebSocket接続を「中止する」ことを期待し、
- レスポンスの検証:
http.Get
の戻り値としてresp
(HTTPレスポンス)を受け取るように変更されました。resp.StatusCode
がhttp.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)