読者です 読者をやめる 読者になる 読者になる

破棄されたブログ

このブログは破棄されました。

cURL での WebSocket のデバッグ

TL;DR;

WebSocket は、そのプロトコルの性質上、 cURL で簡単なデバッグを行うことができる。

具体的には下記のようなコマンドになる。

$ curl -v -i -N \
  -H 'Sec-WebSocket-Version: 13' \
  -H "Sec-WebSocket-Key: $(head -c 16 /dev/urandom | base64)" \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  "http://example.com/path/to/endpoint"

プロトコルの概要

WebSocket のプロトコルは「ハンドシェイク (handshake)」と「データ転送 (data transfer)」の二部に分かれる。

また WebSocket は、 TCP 80 または 443 (TLS) を用いる。

まず、ハンドシェイクを行い、ハンドシェイクに成功するとデータ転送が行われる。 ハンドシェイクとデータ転送の間は同一のセッション内で行われているようだ。

ハンドシェイク

ハンドシェイクでは、 HTTP リクエスト送ることになる。

実際に echo.websocket.orgtelnet してみる。

$ telnet echo.websocket.org 80
Trying 174.129.224.73...
Connected to echo.websocket.org.
Escape character is '^]'.
GET / HTTP/1.1
Host: echo.websocket.org
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: k9iy+YTmD3sEvB3fgD1pXw==

HTTP/1.1 101 Web Socket Protocol Handshake
Connection: Upgrade
Date: Thu, 07 Jul 2016 03:48:18 GMT
Sec-WebSocket-Accept: 4SNcbjdDq+Yw8nzjFGOyxFDlxuQ=
Server: Kaazing Gateway
Upgrade: websocket

GET から Sec-WebSocket-Key までがクライアントのリクエスト、 それ以後がサーバのレスポンスである。

サーバから 101 レスポンスが返った以後はデータ転送になる。

リクエストヘッダーを一応見てみると、 GET, Host, Connection Upgrade, Sec-WebSocket-Version, Sec-WebSocket-Key という行がある。 RFC では、これらは指定がしなければならない (MUST) とされている。

Host は、通常の HTTP リクエストと同様である。

GET については注意が必要で、 WebSocket の URI は、通常、 ws または wss というスキーマで始まっており、 HTTP のスキーマとは異なる。 しかし、ハンドシェイクのリクエスト中の GETHost は、 HTTP の際での URI と一致していなければならない。

例えば、 ws://exmaple.com/path/to/endpoint であれば、リクエストヘッダは下記のようになる。

GET /path/to/endpoint HTTP/1.1
Host: exmaple.com
....

Connection Upgrade, Sec-WebSocket-Version については値が予め決まっていて、それを指定することになる。

Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13

Sec-WebSocket-Key は、 16 バイトのランダムな値を Base64 エンコードして指定する。前述の例では、 /dev/urandom から拾った値を突っ込んだ。

繰り返しになるが、これらのリクエスト送りサーバから 101 が返ると、データ転送が始まる。

より詳細には、 RFC 6455 の 4.1. Client Requirements を参照のこと。

cURL を用いる

ここまでの説明でなぜ cURLデバッグを行うことができるのかがわかる。

端的に言うと、 WebSocket は最初に HTTP リクエストを送り、同一セッション内でデータ転送が行われるためだ。ただし、 cURL ではデータ転送開始後は、データ受信をするのみとなるので、完全な WebSocket のクライアントとして用いるのは、現時点は難しい。

実際に同様のリクエスト cURL から送ってみる。

  • WebSocket のエンドポイントへの URIスキーマws から http へ変更し
  • 必要なヘッダーを指定する
  • GET のパスや Host については、 cURL が自動設定するため、手動で設定を行う必要はない
$ curl -vi -N -H 'Sec-WebSocket-Version: 13' -H "Sec-WebSocket-Key: $(head -c 16 /dev/urandom | base64)" -H "Connection: Upgrade" -H "Upgrade: websocket" http://echo.websocket.org/
* Hostname was NOT found in DNS cache
*   Trying 174.129.224.73...
* Connected to echo.websocket.org (174.129.224.73) port 80 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.38.0
> Host: echo.websocket.org
> Accept: */*
> Sec-WebSocket-Version: 13
> Sec-WebSocket-Key: XeG1/6zcQ8H6QU0sqrid6w==
> Connection: Upgrade
> Upgrade: websocket
>
< HTTP/1.1 101 Web Socket Protocol Handshake
HTTP/1.1 101 Web Socket Protocol Handshake
< Connection: Upgrade
Connection: Upgrade
< Date: Thu, 07 Jul 2016 03:52:59 GMT
Date: Thu, 07 Jul 2016 03:52:59 GMT
< Sec-WebSocket-Accept: uVd00vTXZesqG2efK3z2fLDiPYg=
Sec-WebSocket-Accept: uVd00vTXZesqG2efK3z2fLDiPYg=
* Server Kaazing Gateway is not blacklisted
< Server: Kaazing Gateway
Server: Kaazing Gateway
< Upgrade: websocket
Upgrade: websocket

^C

リクエストヘッダを手入力する必要がなくなったため、非常に楽になった

TLS

TLS を用いる場合は、 WebSocket のハンドシェイク前に TLS のハンドシェイクを行う必要がある。

TLS のハンドシェイクも cURL を使えば cURL が勝手に行ってくれる。 httphttps に変更するだけでよい。実際楽。

$ curl -vi -N -H 'Sec-WebSocket-Version: 13' -H "Sec-WebSocket-Key: $(head -c 16 /dev/urandom | base64)" -H "Connection: Upgrade" -H "Upgrade: websocket" https://echo.websocket.org/

例: cURL で Slack の RTM のエンドポイントへ接続する

$ _token=<SET-YOUR-TOKEN>
$ curl -v -A '' -i -N \
  -H 'Sec-WebSocket-Version: 13' \
  -H "Sec-WebSocket-Key: $(head -c 16 /dev/urandom | base64)" \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  "https://$(curl https://slack.com/api/rtm.start?token=${_token} | jq -r .url | sed -e 's|wss://||')"

参考資料

RFC 6455

広告を非表示にする