Java
Android

nginx(1.3.13) のリバースプロキシでNode.jsとSocket.IO for Android(weberknecht)をつないでみる

More than 1 year has passed since last update.

最初に注意していただきたいのは、今回扱ったSocket.IO for Androidで使用されているWebSocketライブラリのweberknechtは、古いプロトコル仕様にしか対応していませんので、最新の(RFC)プロトコル仕様とは互換性がありません。もしかすると、他のSocket.IO for Androidでは最新のプロトコル仕様に対応したライブラリを使用したものがあるかもしれません。
(私のマシン環境により80ポートなどが使用できないためnginxにはport:9000を使用し、Socket.IO(Node.js)にはport:9001を使用しています)

まず先に結論から。
nginxのプロキシの設定はlocation /socket.io/ { }を追加しその中にプロキシの設定を書く。

nginx.conf
location /socket.io/ {
        proxy_pass http://localhost:9001
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

weberknechtのWebSocketConnection.javaの108行目をコメントアウト。つづいて、WebSocketHandshake.javaの87行目の"Upgrade"を"upgrade"(先頭大文字から小文字へ)に変更。

WebsocketConnection.java
// handshake.verifyServerResponse(serverResponse); <-この行
WebSocketHandshake.java
      //} else if (!headers.get("Connection").equals("Upgrade")) { <-この行
        } else if (!headers.get("Connection").equals("upgrade")) {

以上でnginxのプロキシ経由でSocket.IO(Node.js)サーバーとSocket.IO for Androidの接続ができるようになります。

以下、説明です。

まずは、普通に?nginxのWebSocket用プロキシの設定を記述しました。

nginx.conf
server {
    listen       9000;
    server_name  localhost;
    location / {
        proxy_pass http://localhost:9001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
    #以下略
}

Socket.IOは最初にセッションIDやらタイムアウト、ハートビートタイムアウトなどをクライアントに渡すため、クライアントからHTTPでリクエストを投げているようです。

GET /socket.io/1/websocket/TdbG_diLv50gM9mudzGx HTTP/1.1
以下略

それに対するレスポンスが

HTTP/1.1 404 Not Found
Server: nginx/1.3.13
以下略

となり、この時点で接続に失敗してしまいます。
そこで、nginxの設定を以下のように/socket.io/というパスにプロキシの設定を行うようにしてみます。

nginx.conf
server {
    listen       9000;
    server_name  localhost;
    location / {
        #ここのプロキシの設定は削除
    }
    location /socket.io/ {
        #/socket.io/を追加し、プロキシの設定をこっちに移動
        proxy_pass http://localhost:9001
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
    location 
    #以下略
}

すると、Socket.IOサーバーのデバッグ出力に

setting request GET /socket.io/1/websocket/TdbG_diLv50gM9mudzGx

が出るようになりました。
ハンドシェイクリクエストも送信されているようですが、
WebsocketTransport.javaの129行目のwebsocket.connect()でエラーとなってしまいます。
そこで、ネットからweberknechtのソースを取得しどこでエラーが発生しているのかを調べてみました。
すると、WebSocketConnection.javaの108行目で呼び出しているhandshake.verifyServerResponse()でエラーが発生していました。この関数で何をやっているのかみてみると

verifyServerResponse()
public void verifyServerResponse(byte[] bytes) throws WebSocketException {
    if (!Arrays.equals(bytes, expectedServerResponse)) {
        throw new WebSocketException("not a WebSocket Server");
    }
}

バイト配列同士を比較しています。このバイト配列はハンドシェイクリクエストで渡したSec-WebSocket-Key1,Sec-WebSocket-Key2から算出されたものが入る予定ですが、関数に渡しているバイト配列の中身がすべて-1となっていました。

そこで実際にハンドシェイクレスポンスをみてみると

HTTP/1.1 101 WebSocket Protocol Handshake
Server: nginx/1.3.13
Date: Sun, 03 Mar 2013 06:24:27 GMT
Connection: upgrade
Upgrade: WebSocket
Sec-WebSocket-Origin: http://10.0.2.2
Sec-WebSocket-Location: ws://localhost:9001/socket.io/1/websocket/cSSS3xobljJX8Uf1i_EW

となっており、最後に追加されてるはず16バイトのデータがありません。
nginxのプロキシ処理で削られてしまっています。
なので、handshake.verifyServerResponse()は行わないようにコメントアウトします。
(RFC版のプロトコル仕様では、ハンドシェイクはヘッダだけで構成されているためコンテンツが削られたとしても問題なくハンドシェイクが行えます。)

これで再度、接続をためしてみるとまたエラーが発生しました。
こんどは、WebSocketConnection.javaの117行目のhandshake.verifyServerHandshakeHeaders()でエラーが派生しています。
この関数が何をやっているのかみてみると

verifyServerHandshakeHeaders()
public void verifyServerHandshakeHeaders(HashMap<String, String> headers)
            throws WebSocketException {
        if (!headers.get("Upgrade").equals("WebSocket")) {
            throw new WebSocketException(
                    "connection failed: missing header field in server handshake: Upgrade");
        } else if (!headers.get("Connection").equals("Upgrade")) {
            throw new WebSocketException(
                    "connection failed: missing header field in server handshake: Connection");
        } else if (!headers.get("Sec-WebSocket-Origin").equals(origin)) {
            throw new WebSocketException(
                    "connection failed: missing header field in server handshake: Sec-WebSocket-Origin");
        }
    }

ヘッダの各フィールドの値をチェックを行なっています。ここの

headers.get("Connection").equals("Upgrade")

で"Upgrade"と比較して違うという結果となっていました。headers.get("Connection")の値は"upgrade"と先頭小文字の文字列が入っていました。nginxのプロキシ設定をみてみると

proxy_set_header Connection "upgrade";

となっていましたので、"upgrade"を"Upgrade"に変更してみましたが結果は同じでConnectionフィールドの値は"upgrade"となっていました。
なので、.equals("Upgrade")を.equals("upgrade")に変更します。

"Upgrade"→"upgrade"
//} else if (!headers.get("Connection").equals("Upgrade")) {
} else if (!headers.get("Connection").equals("upgrade")) {

改めて、接続をためしてみるとめでたく接続出来ました。