2013年7月24日水曜日

Java におけるプロキシ経由のリモートIP 取得あれこれ

Servlet プログラミングで、リモートホストの IP の取得に HttpServletRequest#getRemoteAddr をしたところ、プロキシ経由からのアクセスだったためにプロキシサーバの IP が取得されてしまうという、さみしい事態になることがあります。

この対策として 3 パターンほど挙げます。
  1. HttpServletRequest#getHeader("x-forwaded-for") を利用
  2. (Tomcat6以降のみ)RemoteIpValve を利用
  3. Byteman を利用

プロキシの想定

今回はリバースプロキシとして、CentOS 6.4 に yum でインストールした Nginx を利用していると想定します。

インストールしていない場合は以下を実施ください。
# rpm -ivh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm
# yum install nginx

Nginx がインストールできたら /etc/nginx/conf.d/proxy.conf にプロキシ設定を記載します。
※ proxy-host, app-host の部分は環境に合わせて読み替えてください。
# cat /etc/nginx/conf.d/proxy.conf
server {
  listen 80;
  server_name proxy-host;

  location / {
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://app-host:8080;
  }
}

次節からリモートホストの IP 取得方法を記述していきます。

HttpServletRequest#getHeader("x-forwaded-for") を利用

Web で一番多く見かける方法です。HttpServletRequest#getRemoteAddr の代りに、 HttpServletRequest#getHeader("x-forwaded-for") の値を利用します。実際は以下のブログのようにフィルタにすると楽ですね(こちらは X-Real-IP の場合ですが)。

Keeping real user IP in Java web apps behind Nginx proxy

(Tomcat6以降のみ)RemoteIpValve を利用

@msfm さんに教えていただきました。ありがとうございます!

Tomcat 6 以降、または JBoss AS7/EAP6のみとなりますが、
org.apache.catalina.valves.RemoteIpValve を利用する方法です。

Tomcat であれば上記 API リファレンスを参考に、server.xml に Valve 要素を追加します。

JBoss AS7/EAP6 では、残念ながらCLI で設定できなさそうです。war アプリケーションの WEB-INF 配下に以下のような jboss-web.mxl を追加し、再ビルドします。

<jboss-web>
  <valve>
    <class-name>org.apache.catalina.valves.RemoteIpValve</class-name>
    <param>
      <param-name>remoteIpHeader</param-name>
      <param-value>x-forwarded-for</param-value>
    </param>
  </valve>
</jboss-web>

どちらの方法でも、ソースコードを変更することなく実施できます。

なお、WildFly の Web/Servlet コンテナであるところの Undertow では、まだこの機構はないようです。

Byteman を利用

Byteman を使って、アプリケーションのソースコードを変更せずに、
HttpServletRequest#getRemoteAddr の実装を書き換える方法です。
ザ・黒魔術という感じがしますね。

以下、手順です。

1. Byteman のインストール


2013/07/24 時点での最新版である 2.1.3 をインストールします。
# cd /opt
# wget -qO- -O tmp.zip http://downloads.jboss.org/byteman/2.1.3/byteman-download-2.1.3-bin.zip && unzip tmp.zip && rm -f tmp.zip && ln -s byteman-download-2.1.3 byteman

2. Byteman のルールを作成


x-forwarded-for.btm という名前で以下の内容で作成します。

今回作成した Byteman のルールですが、発想としては一番最初の HttpServletRequest#getHeader("x-forwaded-for")  を利用する方法と同じです。

リクエストヘッダに x-forwarded-for が存在する場合は、HttpServletRequest#getRemoteAddr の値を HttpServletRequest#getHeader("x-forwarded-for") の値に書き換えます。存在しない場合は何もしません。

3. Tomcat/JBoss 起動時に Byteman がアタッチされるように設定


Tomcat: $CATALINA_HOME/bin/setenv.sh
JBoss AS7/EAP6: $JBOSS_HOME/bin/standalone.conf

に以下を新規作成(または追記)します。
BYTEMAN_RULE 変数に代入する x-forwaded-for.btm のパスは適宜変更ください。

BYTEMAN_HOME=/opt/byteman
BYTEMAN_RULE=/path/to/x-forwarded-for.btm
BYTEMAN_OPTS="-javaagent:$BYTEMAN_HOME/lib/byteman.jar=listener:true,boot:$BYTEMAN_HOME/lib/byteman.jar"
if [ "x$BYTEMAN_RULE" != "x" ]; then
BYTEMAN_OPTS="${BYTEMAN_OPTS},script:$BYTEMAN_RULE"
fi
BYTEMAN_OPTS="$BYTEMAN_OPTS -Dorg.jboss.byteman.transform.all
-Dorg.jboss.byteman.debug"
JAVA_OPTS="$BYTEMAN_OPTS $JAVA_OPTS"

※ Tomcat の場合は、JAVA_OPTS を CATALINA_OPTS に変更してください。

4. Tomcat/JBoss AS7/EAP6 の起動


起動してみて、getRemoteAddr の返り値がクライアントのリモートホストの IP であれば成功です。

参考


課題

  • 多段プロキシに対応できない
  • 現状の Byteman ルールでは、X-Forwarded-For ヘッダを送ってくるプロキシが信頼できるプロキシかどうかチェック(IP アドレスの確認)を行っていない

2013年6月28日金曜日

Vagrant で CentOS 6.4 を利用する

発作的にまっさらな CentOS が欲しくて、でもはなからインストールするのはめんどくさい。
ということで Vagrant を試してみました。ちなみに、「ゔぇいぐらんと」と読むみたいですね。スクウェアにそんな感じのゲームがありましたね。

利用手順

VirtualBox のインストール


2013/06/27 現在、VirtualBox 4.2.14 だと動作しません。4.2.12 をご利用ください。

バックエンドの仮想化環境として、VirtualBox をインストールします。
なお、バックエンド環境は VirtualBox でなくてもよいようです。

Vagrant のインストール


http://downloads.vagrantup.com/ から最新版を選択し、各 OS に合った形式でインストール。

CentOS 6.4 32bit 版のゲストOSを作成する


今回はお試しなので、サイズの小さい 32bit 版にしました(といっても 600 MB くらいしか違わないようですが)。なお、公式から配布されているイメージは以下から確認できます。


上記でお望みのイメージが見つからない場合は自作するか、有志で公開しているものを利用させてもらうといった手があります。

Box の追加


適当な作業用ディレクトリで行います。以下、このディレクトリをプロジェクトディレクトリと呼びます。
$ mkdir -p ~/vagrant/centos64_32 && cd ~/vagrant/centos64_32
centos64_32 という名前で Box (仮想サーバ、ゲストOS)を追加します。第4引数の URL は、http://www.vagrantbox.es/ で確認した URL です。
$ vagrant box add centos64_32 http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.4-i386-v20130427.box

Box のセットアップ

$ vagrant init centos64_32
VagrantFile という設定ファイルが作成されます。

Box の起動

$ vagrant up
Virtual Box 側にもゲストOSが追加されたことが確認できます。

Box の利用

$ vagrant ssh
[vagrant@localhost ~]$
vagrant ユーザとしてログインできました。

基本的な操作

root になるには sudo -s または su - で


sudo -s であればノンパスで、su - であればパスワード vagrant で root ユーザになれます。

/vagrant は共有ディレクトリ


/vagrant で作られたファイルはプロジェクトディレクトリと共有されます。
$ ls
Vagrantfile
$ vagrant ssh
[vagrant@localhost ~]$ touch /vagrant/foo
[vagrant@localhost ~]$ ls /vagrant
Vagrantfile  foo
[vagrant@localhost ~]$ exit
$ ls
Vagrantfile foo

ssh の設定


現状ですと vagrant ssh を利用してログインする場合、カレントディレクトリはVagrantFile が存在するディレクトリである必要があります。これでは不便という事で、~/.ssh/config にホスト情報を追記して、どのディレクトリにいてもログインできるようにしておきます。
$ vagrant ssh-config --host centos >> ~/.ssh/config
$ cd /path/to # 適当なところへ移動
$ ssh centos
[vagrant@localhost ~]$

ネットワーク


VagrantFile を編集する事で、Box に任意の IP アドレスを設定する事ができます。以下の設定では 192.168.33.10 でホスト OS からアクセスすることができます。
# Create a private network, which allows host-only access to the machine
# using a specific IP.
config.vm.network :private_network, ip: "192.168.33.10" # コメントアウトを外す

プロビジョニング


Box を起動(vagrant up)する際に、
  1. yum upgrade  して
  2. epel レポジトリを追加して
  3. Apache httpd と nkf をインストール
するようにしてみたいと思います。サーバ環境構築の自動化の第一歩ですね。
以下のようなスクリプトを用意します。ファイル名は bootstrap.sh としておきます。
#!/usr/bin/env bash
yum upgrade -y
wget http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm
rpm -ivh epel-release-6-8.noarch.rpm
yum install -y httpd
yum install -y nkf

次に Vagrantfile に Box 起動時にこのシェルを実行するように記述します。
# Every Vagrant virtual environment requires a box to build off of.
  config.vm.box = "centos64_32"
  config.vm.provision :shell, :path => "bootstrap.sh"

Box を起動します。
$ vagrant destroy // すでにBoxを作成している場合
$ vagrant up

Box の起動とともに、bootstrap.sh に記述した内容が実行されていきます。
完了後、ログインします。
$ vagrant ssh
[vagrant@localhost ~]$ httpd -v
Server version: Apache/2.2.15 (Unix)
Server built:   May 13 2013 22:08:57
[vagrant@localhost ~]$ nkf --version
Network Kanji Filter Version 2.0.8 (2007-07-20)
[vagrant@localhost ~]$ sudo -s
[root@localhost vagrant]# yum upgrade
...
No Packages marked for Update
うまくいってるみたいですね。すごい。

参考

2013年6月23日日曜日

「JUnit実践入門」を読む会 第4回 参加報告

前回から Java 読書会 BOF に参加させていただいております。
現在は「JUnit 実践入門」が題材で、今回は第4回です。

Twitter ハッシュタグ
#javareading

参加者は(自分のテリトリの) Java EE 層は少なめで、地図ソフトウェアや組み込みのお話が聞けて非常に刺激的です。話題によっては高度な話まで突っ込まれることがあり、理解できなくて悔しい思いをすることも多いですが。。

読書会のスタイル

参加者各々が順番に、時間を区切って音読していくのが基本になります。
そしてここが肝ですが、途中で疑問に思ったところやメンバに聞いてみたいことなどを、遠慮なくぽろりと聞きます。すると議論が始まります。これが非常に楽しい。自分が口を挟むもよし、色々な意見を傾聴するもよし、です。

これは私だけかもしれませんが、自分ひとりで本を読んでいると、特に難しい内容になると顕著ですが、「まああとでわかるだろう」と読み飛ばしてしまいがちです。そして、だいたいきちんと理解せずに読了してしまいます。この読書会のやり方では、疑問に思ったところを相談できます。疑問がその場で解決することもありますし、もちろん書籍の内容だけでは判断できないものは保留になることもあります。そういった場合でも、あとでちゃんと調べてみようと指針を立てたり、議論のうちに自分が何を理解していないのか整理できることがあります。非常によいスタイルだと思います。

実は社内でも、Java 仮想マシン仕様 の読書会をこのやり方で実施しています。この仕様書は自分にとって相当難解で、気楽に周りに相談できるという場は非常にありがたいです。

第4回 所感

※書籍の内容とは関係ない内容も含みます。

初めて知ったこと

  • Java EE 7 の WebSocket はもう使われているシステムあり。LB や SSL アクセラレータをかましての動作実績もありそう。AP サーバは何使ってるか聞きそびれた。。Jetty あたりかなあ。次回聞いてみたい。
  • 匿名 Inner クラスでメソッドを実装する際に、実装しようとするメソッドの外側にあるローカル変数は final つきでないと読めない。例えば boolean をチェックしたい場合は Boolean bool = false; などと宣言してしまうと、Inner クラス中で bool = true; などとできなくなるので、AtomicBoolean などを使って凌ぐしかない。

困り事と提案

  • インターネット前提の Maven は使いづらい。
    • たしかにその通りだと思う。プログラマがインターネットの利用を制限されている環境は少なくないかもしれない。ただ、Sonatype などを使って、社内のプライベートリポジトリを用意することで、毎回だれか(インターネット接続許可のある一部のひと)がライブラリをダウンロードするのに走り回る必要はなくなるんじゃないかと思う。
  • システム依存や異常系のテストはどこまでやるか
    • 線引きが難しい。ただ、参加者の方も言ってらしたけど、結合試験で実施した方が早いものなんかは割り切りも大事かなと。

情報共有

Arquillian による DB 接続を伴うユニットテスト

書籍中では DBUnit を利用したコードが紹介されています(接続コードだけでなく、DB 起動・停止、テスト実行における前処理・後処理、テストデータの設定含む)。

JBoss などの Java EE AP サーバを利用して開発をする際に、DB 接続部分と開発用 DB の起動・停止に関して簡略化する方法をご紹介します。

Arquillian という、JUnit と協調するテスティングフレームワークがあります。
これは Java EE AP サーバを実際に起動させることで、Java EE の本物のコンテナ(EJB や CDI など)をテスト中で使えるようにするものです。

これが ユニットテストにおける DB接続をどう簡略化させるかというと、JPA(Java EE 標準の ORM 仕様) + インメモリデータベースの組み合わせが鍵になります。
JBoss や GlassFish であれば 、内部にインメモリデータベース(H2 / Derby)を持っており、サーバの起動とともにこれら DB が立ち上がります。これで起動・停止は自動化されます。あとは通常のデータソース設定として DB 接続定義を記載しておけば、プロダクションコードと同様の仕組みで DB 接続ができます。Maven の設定次第では テスト用と開発用の DB を分けておく事もできます。

テストコード中の DB 接続コードについては、JPA では EntityManager というインタフェースを利用してエンティティの操作を行いますが、このインスタンスは @PersistenceContext とアノテーションを付与することでインジェクションされます(参考)。JPA は Java SE でも利用できますが、こういったインジェクションが可能なのは AP サーバのコンテナがあってこそのものです。

JPA を使いたくない場合でも、@Inject(CDI) や @Resource といったインジェクション用アノテーションを利用して、javax.sql.Datasource を取得できますので、すぐに DB 処理に関するコードを書き始められます。

前処理・後処理に関しては、通常通り @Before・@After を利用します。

調べる事

  • Mockito の実装をちょっと読んでみたい。

2013年6月13日木曜日

PukiWiki Adv. を nginx で動かす

以下環境で PukiWiki Adv. を動かすためのメモ。
  • CentOS 6.4(さくらの VPS)
  • nginx 1.4.1
  • PHP 5.4.16
  • PukiWiki Adv. 1.0.3

なぜ Apache でなく nginx か

最初は PukiWiki Adv. を Apache httpd で動かそうと考えていましたが、.htaccess の設定がうまく読込めず、mod_rewrite を利用しての静的URL表現ができなかったため、nginx を試してみました。特にトラフィックが〜などの動機ではありませんので、性能面に関しての言及はありません。すんません。

nginx のインストール

今回の PukiWIki Adv. のために追加で必要そうなモジュールがいまいちよくわからなかったので、yum を使ってインストールしました。

# rpm -ivh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm
# vim /etc/yum.repos.d/nginx.repo
enabled の値を 0 に変更
# yum -y install nginx --enablerepo=nginx

チューニングなどはよしなに。

[参考]
CentOS-6へ最新版のnginxを3分でインストールする方法

PHP関連パッケージのインストール

現在、以下のパッケージがインストールされています。
remi や rpmforge、epel などのレポジトリを追加しておくと良いと思います。

# rpm -qa | grep php
php-common-5.4.16-1.el6.remi.x86_64
php-gd-5.4.16-1.el6.remi.x86_64
php-cli-5.4.16-1.el6.remi.x86_64
php-devel-5.4.16-1.el6.remi.x86_64
php-mbstring-5.4.16-1.el6.remi.x86_64
php-fpm-5.4.16-1.el6.remi.x86_64
php-5.4.16-1.el6.remi.x86_64

PHP-FPM のチューニング、がんばりましょう。

PukiWiki Adv. のインストール

# cd /var/www/html
# tar Jxvf /path/to/PukiWiki\ Adv.\ 1.0.3.tar.xz
# mv webroot pukiwiki
# mkdir misc
# mv COPYING.txt README.txt doc tools misc
# ls -l | awk '{ print $10 }'
misc
pukiwiki
wiki-common
wiki-data

PHP-FPM にプロキシさせる nginx の設定

/etc/nginx/conf.d/pukiwiki.conf を以下の内容で新規作成しました。
yourdomain は皆様のドメイン名に変更してください。

server {
    listen              80;
    server_name  yourdomain;
    root                /var/www/html/pukiwiki;
    index               index.php;

    location / {
        error_page      404 = @pukiwiki;
        log_not_found   off;
    }

    location @pukiwiki {
        rewrite ^/(.+)$         /index.php?$1   last;
        break;
    }

    location ~ \.php$ {
        include         fastcgi_params;

        fastcgi_pass    127.0.0.1:9000;
        fastcgi_index   index.php;
        fastcgi_param   SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    }
}

[参考]
Rewriteを用いたサーチエンジン最適化 - nginx

nginx と PHP-FPM の起動

# /etc/init.d/php-fpm start
# /etc/init.d/nginx start

感想

自分は nginx を今日初めて触りましたが、設定書式がプログラムチックなためか非常に直感的で、ほとんど悩まずに設定することができました。Apache も別に詳しくなく、PHP に至ってはほとんどよくわかってないのですが、とりあえず公開する所までの敷居は低いと思います。

参考URL・書籍

感想でえらそうにいってましたが、スムーズに構築できたのは下記ページや書籍のおかげです。ありがとうございます。

  • nginx
    • nginx 本家
  • WEB+DB PRESS Vol.72
    • nginx の設定内容から PHP アプリケーションとの連携までわかりやすく解説されています。