2012-08-29

Play 的に正しいと思われる解決方法が明らかになったのでメモ。Play framework 1.2.5 が対象です。また、フロントの Web サーバは nginx を想定しています。

リバースプロキシと SSL

Play framework を使って開発された Web アプリケーションを本番環境で運用する場合、Play アプリの AP サーバを直接外部に対して公開するのではなく、Apache や Nginx などの Web サーバを前方に立てて、その Web サーバからの HTTP リクエスト(リバースプロキシ)として、Play アプリの AP サーバに HTTP リクエストが到達するようなサーバ構成を取ることが一般的と思われます。

Play に限った話ではありませんが、このようなサーバ構成において SSL (HTTPS) による通信をサポートするとなると、フロントの Web サーバで SSL をデコードし、Web サーバと AP サーバの間を HTTP プロトコルで通信をすることがきっと多いことでしょう。

Play framework でのリダイレクトと HTTPS に関わる問題

ところで、Play framework の play.mvc.Controller クラス (MVC アーキテクチャでいうところのコントローラに相当するもの) には、HTTP ステータスコード 301/302 によりリダイレクトを行う Controller.redirect() メソッドが実装されています。このメソッドを使って redirect("Users.list"); とすると、次のような HTTP レスポンスがクライアントに返却され、その結果、http://localhost:9000/Users/list の URL へのリダイレクトが行われることになります。

HTTP/1.1 302 Found
Server: nginx/1.2.2
Date: Mon, 27 Aug 2012 16:30:14 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 0
Connection: close
Location: http://localhost:9000/Users/list
Cache-Control: no-cache

この Controller.redirect() によるリダイレクトですが、前述したリバースプロキシかつフロントの Web サーバで SSL デコードをする構成で行うと、リダイレクト先の URL が https で始まる URL ではなく、http で始まる URL となってしまいます。これにより、Web アプリケーションを利用している最中のリダイレクトにより、思いがけず HTTPS から HTTP に切り替わってしまうという悩ましい問題が発生し得ることになります。この問題は、Play アプリとフロントの Web サーバとの間のやりとりが HTTP ベースで行われていることによります。

問題の解決方法

この問題を解決する方法として、

  • フロント Web サーバの設定と Play アプリの application.conf の設定を変更する
  • Controller.redirect() メソッドを呼び出す直前で、request.isSecure = Boolean.TRUE とする
などの方法があります。後者の手段は、フロント Web サーバの設定を変更できない残念な状況の最終手段として残しておくとして、Play 的には前者の手段での対応がおすすめされているようです。そういうわけで、前者の方法について以下説明していきます。

application.conf の設定を変更する

リダイレクト先の URL を生成する Play framework の当該処理の実装を見てみるとわかりますが、スキームを http にするか https にするかの判断は、play.mvc.Http クラスの Http#parseXForwarded() メソッドHttp#isRequestSecure() メソッド の処理結果が強く関係してきます。特に、後者のメソッドが実行されて true を返すような処理となる場合に、リダイレクト先 URL のスキームが https となるので、つまりはそのような望ましい結果となるように条件を揃えてあげればいいことになります。

同式を true にするために変更すべき application.conf の設定は、以下の2つになります。

  • XForwardedSupport=all を追記する ... これにより、Play アプリに渡される HTTP リクエストのヘッダに X-Forwarded-For フィールドが存在すれば、無条件に Http#isRequestSecure() が実行されるようになります。
  • XForwardedProto=https とする ... これを設定することにより、X-Forwarded-Proto フィールドや X-Forwarded-Ssl フィールドの値によらず、X-Forwarded-For フィールドが定義された状況下での Play アプリに対するリクエストを HTTPS でリクエストされたものとして扱うようになります。Web アプリ全体を HTTPS で提供する場合に適した設定となります。
このあたりの設定は実は Play のドキュメント にも書かれています(私は、この内容から今回の目的に対する解決策を汲み取ることができませんでした…)。

nginx.conf の設定を変更する

nginx.conf の設定は簡単で、リバースプロキシの設定において proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; を仕込んであげれば OK です。

解決方法は以上となります。もっとこと細かに HTTP / HTTPS を制御したい、としても、基本は play.mvc.Http クラスの、特に前述した2つのメソッドの実装を眺めれば解決するものと思います。

2012-08-17

業務システムとかを開発していると、たまにこの手の処理がしたくなるのですが、

Date date = new Date();
for (int i = 0; i < 10; i++) {
  Calendar cal = Calendar.getInstance();
  cal.setTime(date);
  cal.add(Calendar.HOUR, 1);
  date = cal.getTime();

  // ここに date オブジェクトを利用した処理が書かれる...
}
こんなコードを書きたくない人向けの Iterator / Iterable クラスがこちらになります。