mixiアプリでRESTful APIを使う

今さらながらmixiアプリでRESTful APIが使えることに気づいたわけで、早速試してみました。

mixiデベロッパー向けサイトに、サンプルコードも付いて詳しく説明されているので、これを参考にして書いていけば問題なさそうです。

実際のリクエストには、「Consumer Key」と「Consumer Secret」を使い、2-legged OAuthによって適切に署名をする必要があります。
「Consumer Key」と「Consumer Secret」は、アプリごとに割り当てられており、mixiアプリ設定画面より、確認することができます。

注意する点は「xoauth_requestor_id」をリクエストに付加する必要があるということです。公式サイトから引用です。

RESTful APIにアクセスする際に、xoauth_requestor_idパラメータによって、誰の権限でアクセスを行うかを指定することが必要になります。パラメータ値は、対象ユーザのIDとなります。このユーザIDについて、「一定時間内にWebブラウザで対象のmixiアプリを起動したユーザ」のIDのみ指定することが可能です。

対象ユーザが対象のmixiアプリを起動していない、もしくは起動から一定時間が経過した後にRESTful APIにアクセスを行った際には、HTTPレスポンスコードとして「401 Unauthorized」が返却されます。

対象ユーザのID、すなわちコンテナから送られてくる「opensocial_viewer_id」を指定することになります。

下記のようなコードで動かすことが出来ました。署名にはoauthライブラリを使います。

  require 'oauth'
consumer = OAuth::Consumer.new(「アプリのConsumer Key」,
「アプリのConsumer Secret」,
{:site => 'http://api.mixi-platform.com'})
end_point = 'http://api.mixi-platform.com/os/0.8'
req = consumer.request(:get, end_point + '/people/@me/@self' + '?xoauth_requestor_id=' + opensocial_viewer_id)
JSON.parse(req.body) # JSONデータをオブジェクト化

公式にはRubyのコードがなかったようなので載せておきます。

PHPにPOSTでBase64の文字列を渡すときは注意

RubyのZlibで圧縮したデータをBase64にして、PHPのスクリプトにPOSTで渡すってことをやっていたのですが、圧縮データを展開できる時と、できない時があって悩んでしまった。

よくよく調べると、そもそもBase64をデコード出来ていなかった。それで調べてみると、PHPのドキュメントの下に書いてありました。
PHP: base64_decode – Manual
どうやら、POSTでデータを渡すと、Base64の「+」記号が勝手にスペースに変換されてしまうらしい。
以下のように修正したところ無事に動きました。

## str_replace でスペースを+に置換
$data = base64_decode(str_replace(' ', '+', $data));
## ヘッダ分を差し引いてあげないとだめなのね・・・
$_data = gzinflate(substr($data, 10, -8));

ちなみにRuby側です。
Zlib::MAX_WBITSに+16を足すと、前後に圧縮データ情報も付加されたGZIP形式になるそうです。

z = Zlib::Deflate.new Zlib::BEST_COMPRESSION, Zlib::MAX_WBITS + 16
param = [z.deflate(data, Zlib::FINISH)].pack('m')
z.close

lighttpdで直リンク対策を簡単にする

前回に引き続き直リンク対策をlighttpdでおこなってみたいと思います。Apacheと同じようにrewriteで・・・と行きたい所ですが、lighttpdmod_rewriteは、Apacheほどの機能は持っていないようです。
設定ファイルにずらずらと書いて行ってもいいのですが、それでは簡単ではありません。色々調べた所、lighttpdでは、include_shellという便利な機能があります。これは、スクリプトなどで出力した結果をそのまま設定として読み取ってくれるという便利な機能です。
今回は、include_shellを使って、ドメイン許可リストから読み取ったものを設定として出力するといったことをしたいと思います。

lighttpdの設定は以下のようになります。

include_shell "/usr/local/etc/lighttpd/referer_list.rb"

referer_list.rbの中身は以下になります。

#!/usr/bin/env ruby
list_file = '/usr/local/etc/referer_list'
referer_list = []
File::open(list_file) do |f|
while line = f.gets
referer_list << line.chomp if line.chomp[0, 1] != "#"
end
end
str = referer_list.join("|").gsub(/\./, "\\.")
puts <<EOS
$HTTP["referer"] !~ "^($|http://(www\\.)?(#{str}))" {
url.access-deny = (".jpg", ".jpeg", ".gif", ".png", ".bmp", ".swf")
}
EOS

例の如くreferer_list.rbには実行権限が必要です。$HTTP[“referer”]を使って、正規表現で許可するドメイン(ホスト)を羅列していくシンプルなものです。後は、条件に一致しないドメインに対し、access-denyをかけます。

referer_list.rbに実行権限をかけるのを忘れ、ひたすら悪戦苦闘してしまった・・・。何もエラーが出ないようなので気をつけてください。

mod_rewriteで直リンク対策を簡単にする

外部から勝手にリンクされて困ってしまう場合、特に画像や動画ファイルなど比較的重たいファイルを勝手にリンクされてしまうと帯域を無駄に使ってしまい、困ってしまうことがあると思います。
そこでリファラを使って、直リンクされている場合に403を返すようにmod_rewriteを使ってみます。
今回はそれを簡単にするために、直リンクを許可するドメインをリスト化し、RewriteMapを使って処理したいと思います。

RewriteMapについては下記の記事でも取り上げています。
Apacheのmod_rewriteを使ってフェイルオーバー? – フタなしカンヅメ

Apacheの設定ファイルは以下のようにします。

<IfModule mod_rewrite.c>
RewriteEngine On
# 必ずロックファイルを指定
RewriteLock /tmp/map.lock
# RewriteMapで使う外部プログラムを指定
RewriteMap referercheck-map prg:/usr/local/bin/referer_check.rb
</IfModule>
<Directory />
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} \.(gif|png|jpg|jpeg|bmp)$ [NC]
RewriteCond ${referercheck-map:%{HTTP_REFERER}|NG} ^NG$
RewriteRule ^.*$ / [L,F]
</Directory>

許可ドメインリストを読み取って処理する「referer_check.rb」の中身です。

#!/usr/bin/env ruby
$stdin.sync = 1
$stdout.sync = 1
require 'uri'
CHECK_OK = 'OK'
CHECK_NG = 'NG'
list_file = '/usr/local/etc/referer_list'
referer_list = []
File::open(list_file) do |f|
while line = f.gets
referer_list << line.chomp if line.chomp[0, 1] != "#"
end
end
while true
flag = nil
buffer = $stdin.gets
referer = URI.encode(buffer.strip)
if referer.empty?
flag = true
else
begin
uri = URI.parse(referer)
flag = referer_list.any? {|v| uri.host.include?(v)} if uri.host
rescue Exception
#
end
end
$stdout.puts(flag ? CHECK_OK : CHECK_NG)
end

referer_check.rbには実行権限が必要ですので注意してください。リストファイルは、一行ずつ許可ドメイン(ホスト)を記述します。

example.com
example.co.jp

許可するドメインが増えたら、リストを更新するだけで対応できるので、ちょっと簡単?になった気がします。今回は許可ドメインということでしたが、同じように拒否ドメインリストで対応ってことも可能だと思います。

Railsでmixiアプリを作ってみました

mixiアプリってなんじゃろ・・・と思いつつ、調べてみたらOpenSocialというコンテナを使って、mixiの中で自分の作ったアプリが動くらしい。
何やら面白そうだったので、Railsの勉強がてらに作ってみました。

あしあとプラス
ソーシャル・ネットワーキング サービス [mixi(ミクシィ)]
あしあとの履歴を長期間とったり、ユーザが消したあしあとも見れちゃうアプリです。誰かが10人使ってくれればカテゴリに登録できるみたいです。
内容が内容なので審査を通るのかあやしいですが・・・。

携帯開発環境をMoxyからSSBに

今までの開発環境はMoxyを利用させて頂いていたのですが、OSを入れ直したついでにMoxyを新しくしようとしたところ、どうしてもうまく動かすことができませんでした。古いのでいいかと思ったのですが、SSBというのが目につきました。

どうやらこいつは、Rubyで出来ている携帯開発環境(ブラウザ?)らしい。インストールもあっさりできてたので、こっちを使って見ることにしました。どうもRailsで出来ているのかと思ったら違うみたい・・・。インタフェースも今風です。

SSBのダウンロード先

ssb –
CodeRepos::Share – Trac

早速、動かしてみたのですが一つだけハマりました。PHPのNet_UserAgent_Mobileを使っていたので、初期の設定で以下のようなエラーがでました。

DoCoMo/2.0 P905i(c100;TB;ser000000000000000;icc0000000000000000): might be new variants. Please contact the author of Net_UserAgent_Mobile!

ソースを確認したところ、マッチするUserAgentがないとエラーになるようです。ここで引っかかっています。

if (preg_match('/^icc(\w{20})?$/', $value, $matches)) {

どうやら、初期の設定だとiccが15桁なので20桁に変更してあげれば無事動きました。

Apache2でPassengerを使ってみる

Apache2のモジュールでお手軽にRailsを動かせるというPassengerをインストールしてみることにしました。そこでちょっとハマった。

Apacheは最初からインストール済みで、まずはRubyGemsを入れるところから始めました。yumで入るかと思ったのですが、初期の設定では無理なようでソースから入れました。(リポジトリを追加したりすればできそう)

次にgemでPassengerをインストール。

# gem install passenger

あとは、rootになってpassenger-install-apache2-moduleを実行するだけでOKだそうです。しかしApacheが見つからないというエラー。どうやら/usr/local/apache2にインストールしていたせいみたい。
ドキュメントには、export APXS2=/usr/local/apache2/bin/apxsを実行すれば大丈夫と書いてありました。やってみるとApacheのディレクトリをちゃんと見てくれるようになりました。
しかし、途中のビルドで以下のようなエラーが・・・

(in /usr/lib/ruby/gems/1.8/gems/passenger-2.2.4)
** Invoke apache2 (first_time)
** Invoke ext/apache2/mod_passenger.so (first_time)
** Invoke ext/apache2/libpassenger_common.a (first_time, not_needed)
** Invoke ext/apache2/libpassenger_common/Utils.o (first_time, not_needed)
** Invoke ext/common/Utils.cpp (first_time, not_needed)
** Invoke ext/common/Utils.h (first_time, not_needed)
** Invoke ext/apache2/libpassenger_common/Logging.o (first_time, not_needed)
** Invoke ext/common/Logging.cpp (first_time, not_needed)
** Invoke ext/common/Logging.h (first_time, not_needed)
** Invoke ext/apache2/libpassenger_common/SystemTime.o (first_time, not_needed)
** Invoke ext/common/SystemTime.cpp (first_time, not_needed)
** Invoke ext/common/SystemTime.h (first_time, not_needed)
** Invoke ext/apache2/libpassenger_common/CachedFileStat.o (first_time, not_needed)
** Invoke ext/common/CachedFileStat.cpp (first_time, not_needed)
** Invoke ext/common/CachedFileStat.h (first_time, not_needed)
** Invoke ext/apache2/libpassenger_common/Base64.o (first_time, not_needed)
** Invoke ext/common/Base64.cpp (first_time, not_needed)
** Invoke ext/common/Base64.h (first_time, not_needed)
** Invoke ext/apache2/ApplicationPoolServerExecutable (first_time, not_needed)
** Invoke ext/common/ApplicationPoolServerExecutable.cpp (first_time, not_needed)
** Invoke ext/common/ApplicationPool.h (first_time, not_needed)
** Invoke ext/common/Application.h (first_time, not_needed)
** Invoke ext/common/StandardApplicationPool.h (first_time, not_needed)
** Invoke ext/common/ApplicationPoolStatusReporter.h (first_time, not_needed)
** Invoke ext/common/MessageChannel.h (first_time, not_needed)
** Invoke ext/common/SpawnManager.h (first_time, not_needed)
** Invoke ext/common/PoolOptions.h (first_time, not_needed)
** Invoke ext/common/StringListCreator.h (first_time, not_needed)
** Invoke ext/common/FileChangeChecker.h (first_time, not_needed)
** Invoke ext/common/SystemTime.h (not_needed)
** Invoke ext/common/CachedFileStat.hpp (first_time, not_needed)
** Invoke ext/apache2/libboost_oxt.a (first_time, not_needed)
** Invoke ext/apache2/libboost_oxt/boost/once.o (first_time, not_needed)
** Invoke ext/boost/src/pthread/once.cpp (first_time, not_needed)
** Invoke ext/apache2/libboost_oxt/boost/exceptions.o (first_time, not_needed)
** Invoke ext/boost/src/pthread/exceptions.cpp (first_time, not_needed)
** Invoke ext/apache2/libboost_oxt/boost/thread.o (first_time, not_needed)
** Invoke ext/boost/src/pthread/thread.cpp (first_time, not_needed)
** Invoke ext/apache2/libboost_oxt/oxt/system_calls.o (first_time, not_needed)
** Invoke ext/oxt/system_calls.cpp (first_time, not_needed)
** Invoke ext/oxt/thread.hpp (first_time, not_needed)
** Invoke ext/oxt/spin_lock.hpp (first_time, not_needed)
** Invoke ext/oxt/backtrace.hpp (first_time, not_needed)
** Invoke ext/oxt/system_calls.hpp (first_time, not_needed)
** Invoke ext/oxt/macros.hpp (first_time, not_needed)
** Invoke ext/oxt/tracable_exception.hpp (first_time, not_needed)
** Invoke ext/oxt/detail/tracable_exception_disabled.hpp (first_time, not_needed)
** Invoke ext/oxt/detail/spin_lock_pthreads.hpp (first_time, not_needed)
** Invoke ext/oxt/detail/tracable_exception_enabled.hpp (first_time, not_needed)
** Invoke ext/oxt/detail/spin_lock_portable.hpp (first_time, not_needed)
** Invoke ext/oxt/detail/backtrace_disabled.hpp (first_time, not_needed)
** Invoke ext/oxt/detail/backtrace_enabled.hpp (first_time, not_needed)
** Invoke ext/oxt/detail/spin_lock_gcc_x86.hpp (first_time, not_needed)
** Invoke ext/apache2/libboost_oxt/oxt/tracable_exception.o (first_time, not_needed)
** Invoke ext/oxt/tracable_exception.cpp (first_time, not_needed)
** Invoke ext/oxt/thread.hpp (not_needed)
** Invoke ext/oxt/spin_lock.hpp (not_needed)
** Invoke ext/oxt/backtrace.hpp (not_needed)
** Invoke ext/oxt/system_calls.hpp (not_needed)
** Invoke ext/oxt/macros.hpp (not_needed)
** Invoke ext/oxt/tracable_exception.hpp (not_needed)
** Invoke ext/oxt/detail/tracable_exception_disabled.hpp (not_needed)
** Invoke ext/oxt/detail/spin_lock_pthreads.hpp (not_needed)
** Invoke ext/oxt/detail/tracable_exception_enabled.hpp (not_needed)
** Invoke ext/oxt/detail/spin_lock_portable.hpp (not_needed)
** Invoke ext/oxt/detail/backtrace_disabled.hpp (not_needed)
** Invoke ext/oxt/detail/backtrace_enabled.hpp (not_needed)
** Invoke ext/oxt/detail/spin_lock_gcc_x86.hpp (not_needed)
** Invoke ext/apache2/libboost_oxt/oxt/backtrace.o (first_time, not_needed)
** Invoke ext/oxt/backtrace.cpp (first_time, not_needed)
** Invoke ext/oxt/thread.hpp (not_needed)
** Invoke ext/oxt/spin_lock.hpp (not_needed)
** Invoke ext/oxt/backtrace.hpp (not_needed)
** Invoke ext/oxt/system_calls.hpp (not_needed)
** Invoke ext/oxt/macros.hpp (not_needed)
** Invoke ext/oxt/tracable_exception.hpp (not_needed)
** Invoke ext/oxt/detail/tracable_exception_disabled.hpp (not_needed)
** Invoke ext/oxt/detail/spin_lock_pthreads.hpp (not_needed)
** Invoke ext/oxt/detail/tracable_exception_enabled.hpp (not_needed)
** Invoke ext/oxt/detail/spin_lock_portable.hpp (not_needed)
** Invoke ext/oxt/detail/backtrace_disabled.hpp (not_needed)
** Invoke ext/oxt/detail/backtrace_enabled.hpp (not_needed)
** Invoke ext/oxt/detail/spin_lock_gcc_x86.hpp (not_needed)
** Invoke ext/apache2/libboost_oxt/oxt/thread.o (first_time, not_needed)
** Invoke ext/oxt/thread.cpp (first_time, not_needed)
** Invoke ext/oxt/thread.hpp (not_needed)
** Invoke ext/oxt/spin_lock.hpp (not_needed)
** Invoke ext/oxt/backtrace.hpp (not_needed)
** Invoke ext/oxt/system_calls.hpp (not_needed)
** Invoke ext/oxt/macros.hpp (not_needed)
** Invoke ext/oxt/tracable_exception.hpp (not_needed)
** Invoke ext/oxt/detail/tracable_exception_disabled.hpp (not_needed)
** Invoke ext/oxt/detail/spin_lock_pthreads.hpp (not_needed)
** Invoke ext/oxt/detail/tracable_exception_enabled.hpp (not_needed)
** Invoke ext/oxt/detail/spin_lock_portable.hpp (not_needed)
** Invoke ext/oxt/detail/backtrace_disabled.hpp (not_needed)
** Invoke ext/oxt/detail/backtrace_enabled.hpp (not_needed)
** Invoke ext/oxt/detail/spin_lock_gcc_x86.hpp (not_needed)
** Invoke ext/apache2/libpassenger_common.a (not_needed)
** Invoke ext/apache2/libboost_oxt.a (not_needed)
** Invoke ext/apache2/mod_passenger.o (first_time)
** Invoke ext/apache2/mod_passenger.c (first_time, not_needed)
** Execute ext/apache2/mod_passenger.o
gcc -Iext -Iext/common -fPIC -DLINUX=2 -D_REENTRANT -D_GNU_SOURCE -D_LARGEFILE64_SOURCE -I/usr/include/apr-1 -I/usr/include/apr-1 -I/usr/local/apache2/include -D_REENTRANT -I/usr/local/include -Wall -g -DPASSENGER_DEBUG -DBOOST_DISABLE_ASSERTS -o ext/apache2/mod_passenger.o -c ext/apache2/mod_passenger.c
** Invoke ext/apache2/Configuration.o (first_time)
** Invoke ext/apache2/Configuration.cpp (first_time, not_needed)
** Invoke ext/apache2/Configuration.h (first_time, not_needed)
** Execute ext/apache2/Configuration.o
g++ -Iext -Iext/common -fPIC -DLINUX=2 -D_REENTRANT -D_GNU_SOURCE -D_LARGEFILE64_SOURCE -I/usr/include/apr-1 -I/usr/include/apr-1 -I/usr/local/apache2/include -D_REENTRANT -I/usr/local/include -Wall -g -DPASSENGER_DEBUG -DBOOST_DISABLE_ASSERTS -o ext/apache2/Configuration.o -c ext/apache2/Configuration.cpp
/usr/local/apache2/include/apr_file_info.h:192: error: ‘apr_ino_t’ does not name a type
rake aborted!
Command failed with status (1): [g++ -Iext -Iext/common -fPIC -DLINUX=2 -D_...]
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:995:in `sh'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:1010:in `call'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:1010:in `sh'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:1094:in `sh'
/usr/lib/ruby/gems/1.8/gems/passenger-2.2.4/misc/rake/cplusplus.rb:31:in `compile_cxx'
/usr/lib/ruby/gems/1.8/gems/passenger-2.2.4/Rakefile:310
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:636:in `call'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:636:in `execute'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:631:in `each'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:631:in `execute'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:597:in `invoke_with_call_chain'
/usr/lib/ruby/1.8/monitor.rb:238:in `synchronize'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:590:in `invoke_with_call_chain'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:607:in `invoke_prerequisites'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:604:in `each'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:604:in `invoke_prerequisites'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:596:in `invoke_with_call_chain'
/usr/lib/ruby/1.8/monitor.rb:238:in `synchronize'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:590:in `invoke_with_call_chain'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:607:in `invoke_prerequisites'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:604:in `each'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:604:in `invoke_prerequisites'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:596:in `invoke_with_call_chain'
/usr/lib/ruby/1.8/monitor.rb:238:in `synchronize'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:590:in `invoke_with_call_chain'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:583:in `invoke'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:2051:in `invoke_task'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:2029:in `top_level'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:2029:in `each'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:2029:in `top_level'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:2068:in `standard_exception_handling'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:2023:in `top_level'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:2001:in `run'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:2068:in `standard_exception_handling'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake.rb:1998:in `run'
/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/bin/rake:31
/usr/bin/rake:19:in `load'
/usr/bin/rake:19

どうやらもう一つ環境変数として、APR_CONFIGも書き出しておかないといけないみたい。

# export APR_CONFIG=/usr/local/apache2/bin/apr-1-config

これで無事にインストール完了しました。

参考
404 Not Found

RubyのNet::HTTPでハマる

前回の記事でmod_rewriteのRewriteMapを使い、なんちゃってフェイルオーバーを作ってみましたが、どうもたまにヘルスチェックをおこなうrubyのスクリプトが落ちるわけです・・・。
エラーはこんな感じ。

/usr/lib/ruby/1.8/timeout.rb:54:in `open': execution expired (Timeout::Error)
from /usr/lib/ruby/1.8/net/http.rb:560:in `connect'
from /usr/lib/ruby/1.8/timeout.rb:56:in `timeout'
from /usr/lib/ruby/1.8/timeout.rb:76:in `timeout'
from /usr/lib/ruby/1.8/net/http.rb:560:in `connect'
from /usr/lib/ruby/1.8/net/http.rb:553:in `do_start'
from /usr/lib/ruby/1.8/net/http.rb:542:in `start'
from /usr/local/etc/shells/healthcheck.rb:30

rescueで捕捉しているのにどうしてだろう?と思っていたのですが、下記のような記事を発見。どうやら今のやり方では、Timeout::Errorの例外は捕捉してくれないようです。
Net::HTTPの例外補足方法 – OVERT MEMO

さっそく以下のように修正。

      begin
http = Net::HTTP.new(hostname, port ? port : 80)
http.open_timeout = HTTP_TIMEOUT
http.read_timeout = HTTP_TIMEOUT
http.start {|p| response = p.head(path ? path : '/') }
rescue Exception # ここを修正
#
end

Apacheのmod_rewriteを使ってフェイルオーバー?

Apachemod_rewriteを使って、フェイルオーバーみたいなことができないかと思い、ちょっとやってみることにしました。

フェイルオーバーといったら大げさな感じですが、もうちょっと本格的にやるならmod_proxy_balancerを使ったほうがいいと思うので。。。

現在下記のような構成で運用しているサーバがありますが、静的ファイルを配信するサーバBが落ちている場合やオフになっている場合などは、サーバBから配信したいという感じです。

サーバA -> CGIの処理や比較的軽いHTMLを処理
サーバB -> 主に画像などの重たい静的ファイルを処理

かなり限定的な環境だと思うのですが、サーバAで一度リクエストを受け、画像ファイルだけをリダイレクトでサーバBに転送しています。
なので、サーバBが落ちていたりした場合、サーバBに転送せずにサーバAで返却したいというわけです。サーバAでも同じデータを持っている必要があります。

調べたところRewriteMapを使うことで外部プログラムを呼び出すことができるようです。今回はこれを使ってみます。
サーバが稼働しているか定期的にヘルスチェックをする必要がありますが、今回はサーバAにリクエストされた際に、外部プログラムを通して5秒間隔でサーバBにHTTPリクエストをおこなってチェックしてみます。

外部プログラムはRubyを使ってみます。外部プログラムの呼び出しは起動時に一回のみおこなわれ、STDIN、STDOUTを通して処理されるとのことで、オーバーヘッドはあまり気にする必要はなさそうです。
こんな感じで作ってみました。

## /usr/local/bin/healthcheck.rb
#!/usr/bin/env ruby
# Buffered I/Oを使わない
$stdin.sync = 1
$stdout.sync = 1
require 'net/http'
STATUS_DOWN = 'DOWN'
STATUS_UP = 'UP'
STATUS_NULL = 'NULL'
CHECK_PERIOD = 5 # ヘルスチェックの間隔(秒)
HTTP_TIMEOUT = 2 # HTTPコネクションと読み込みのタイムアウト(秒)
lastcheck = 0
laststatus = STATUS_NULL
while true
buffer = $stdin.gets
hostname, port, path = buffer.chomp.split(/:/, 3)
nowcheck = Time.now.to_i
if (lastcheck + CHECK_PERIOD) > nowcheck
$stdout.puts laststatus
else
response = nil
if hostname
begin
http = Net::HTTP.new(hostname, port ? port : 80)
http.open_timeout = HTTP_TIMEOUT
http.read_timeout = HTTP_TIMEOUT
http.start {|p| response = p.head(path ? path : '/') }
rescue
#
end
end
if response and response.code.to_i == 200
$stdout.puts laststatus = STATUS_UP
else
$stdout.puts laststatus = STATUS_DOWN
end
lastcheck = nowcheck
end
end

注意:上記のスクリプトにはバグがあります。
RubyのNet::HTTPでハマる – フタなしカンヅメ

Apacheの設定ファイルは下記のように書きました。

<IfModule mod_rewrite.c>
RewriteEngine On
# 必ずロックファイルを指定
RewriteLock /tmp/map.lock
# RewriteMapで使う外部プログラムを指定
RewriteMap healthcheck-map prg:/usr/local/bin/healthcheck.rb
</IfModule>
<Directory />
RewriteEngine On
# RewriteMapを呼び出す時は下記のように指定
# RewriteRuleでも呼び出せます
RewriteCond %{REQUEST_FILENAME} \.(gif|png|jpg|jpeg|bmp)$ [NC]
RewriteCond ${healthcheck-map:192.168.1.3:80:/} ^UP$ [NC]
RewriteRule (.*) http://example.com/$2 [L,R]
</Directory>
RewriteCond ${healthcheck-map:192.168.1.3:80:/} ^UP$ [NC]
- healthcheck-map => 使用するRewriteMapの名前
- 192.168.1.3 => ヘルスチェックするホスト名またはIPアドレス
- 80 => ポート番号
- / => チェック先のリクエストパス
のように「:」で区切って呼び出します

一番ハマったのが、RewriteMapで外部プログラムを指定する部分。いろいろいじっても下記のようなエラーが・・・。

map lookup FAILED: map=healthcheck-map key=localhost:80:/

原因は、ディレクティブの中で、RewriteEngine Onを指定していなかったせいでした。そんなんあり?

参考サイト
mod_rewriteでサーバーの負荷が高いときだけリダイレクトする – inspfightmanの日記
Apache module mod_rewrite
mod_rewrite – RewriteMap – とみぞーノート
rubyスクリプトでRewriteMapを書く際の注意点 — Stack-Style
mod_rewrite:RewriteMapで柔軟なURL書き換え | ゆーすけぶろぐ