WebSocketを使ったtornadoサーバの前段にnginxリバースプロキシを置いてみた

nginxをリバースプロキシにして、バックエンドにtornadoで作成したWebSocketアプリケーションを動作させた時にハマりポイントがあったので備忘録として記録。

環境

  • CentOS 7.4
  • nginx 1.12.2
  • Python 3.6.5
  • tornado 5.0.2

check_origin関数が必要

結論から書くと、WebSocketHandlerクラスを継承したクラス内にcheck_origin関数を定義しないと動作しない。

class WebSocketHandler(tornado.websocket.WebSocketHandler):
def check_origin(self, origin):
return True

def open(self):
...

バージョン4.0から、この関数が必要になったみたい。

WebSocket connections from other origin sites are now rejected by default. To accept cross-origin websocket connections, override the new method WebSocketHandler.check_origin.(出典)

(和訳)Webページを取得したドメイン以外のサイトからのWebSocket接続は、デフォルトで拒否させるようになりました。異なるドメインのWebページからWebSocket接続を受け付けるにはcheck_origin関数をオーバーライドします。

check_origin関数がないと403エラーになる。

WARNING:tornado.access:403 GET /ws (::1) 0.51ms

セキュリティに注意

check_originをオーバーライドすることで、セキュリティ上のリスクを抱えることになる。そのため、オーバーライドする際には自身で対策を実装する必要がある。

This is an important security measure; don’t disable it without understanding the security implications. In particular, if your authentication is cookie-based, you must either restrict the origins allowed by check_origin() or implement your own XSRF-like protection for websocket connections. See these articles for more.(出典)

(和訳)これは重要なセキュリティ対策です。 セキュリティの意味を理解することなく無効にしないでください。 特に、認証がCookieベースの場合は、check_origin関数によって許可されたドメインを制限するか、websocket接続に対して独自のXSRFのような保護を実装する必要があります。 詳細については、これらの記事を参照してください。

実装例1: サブドメインは許可(公式サイトより

def check_origin(self, origin):
parsed_origin = urllib.parse.urlparse(origin)
return parsed_origin.netloc.endswith(".mydomain.com")

実装例2: 指定したドメインのみ許可

def check_origin(self, origin):
parsed_origin = urllib.parse.urlparse(origin)
return parsed_origin.netloc == "mydomain.com"

(おまけ)nginxの設定ファイル

とくになんの変哲もない普通のWebSocketプロキシ設定。

server {

(中略)

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_max_temp_file_size 0;

location /ws {
proxy_pass http://localhost:8080/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}

location / {
proxy_pass http://localhost:8080;
}
}