SSL で自己証明書を使用している URL には open-uri でアクセスできない
Ruby の open-uri って、SSL で自己証明書を使用している URL には使えないのね。あっさり、certificate verify failed (OpenSSL::SSL::SSLError) とか OpenSSL で弾かれる。
「なんかオプションとかないのかね?」とリファレンスマニュアルを探してみるが、どうにもそれらしいものが見つからない。
ソースコードを読んでみると、やはり、検証方法は OpenSSL::SSL::VERIFY_PEER 固定のようだ。 open-uri.rb の 228 行目。
def OpenURI.open_http(buf, target, proxy, options) # :nodoc:
...
http = klass.new(target_host, target_port)
if target.class == URI::HTTPS
require 'net/https'
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
store = OpenSSL::X509::Store.new
store.set_default_paths
http.cert_store = store
end
...
end
そういえば、Java でも、Commons の HttpClient で同様の問題にぶち当たったことがある。あのときも、contrib の EasySSLProtocolSocketFactory を使う必要があったっけ。
もう、net/https を使うしかないのかな ...
MySQL/Ruby の test.rb が失敗する理由と対策
前回書いた、MySQL/Ruby の test.rb が失敗する原因を調査する。
まずは、実行結果の出力を調べてみよう。 すべてのテストが失敗しているため長いので、最初のテスト結果のみ抜粋する。
% ruby ./test.rb localhost root newpass
...
1) Failure:
test_connect(TC_Mysql) [./test.rb:39]:
Exception raised:
Class: <Mysql::Error>
Message: <"Access denied for user 'ishikawa'@'localhost' (using password: NO)">
---Backtrace---
./test.rb:39:in `connect'
./test.rb:39:in `test_connect'
./test.rb:39:in `test_connect'
...
MySQL のエラーで <tt>"Access denied for user 'ishikawa'@'localhost' (using password: NO)"</tt> と出力されているので、これは単なるアクセス制御の問題だ。
そして、test.rb のコマンドライン引数で root ユーザを指定しているにも関わらず、実際には ishikawa ユーザでアクセスしようとしている。
うまくいくわけがない。 コマンドライン引数による指定が無視されているわけだ。
何故、コマンドライン引数が無視されるのか
コマンドライン引数が無視される原因を調べてみると、どうやら test.rb が使っている Test::Unit が悪さをしているらしい(Test::Unit は Ruby の標準添付ライブラリで、いわゆる xUnit ツールの Ruby 版である)。
簡単な検証スクリプトを書いてみる。
require "test/unit"
puts "before test: ARGV = #{ARGV.inspect}"
class SimpleTestCase < Test::Unit::TestCase
def test_argv()
puts "in test: ARGV = #{ARGV.inspect}"
end
end
ユニットテストが実行される前と、実行されるときに ARGV(コマンドライン引数の配列)をダンプするだけのスクリプトだ。これを適当な引数つきで実行して、その結果を確認する。
% ruby ./simple_test.rb 1 2 3
before test: ARGV = ["1", "2", "3"]
Loaded suite ./simple_test
Started
in test: ARGV =
.
Finished in 0.000316 seconds.
1 tests, 0 assertions, 0 failures, 0 errors
ユニットテストを実行する前では ARGV に引数の 1, 2, 3 が格納されているが、テストの内部では ARGV が空になっている。となると、Test::Unit が渡ってきた引数を食べてしまっているのに違いない。
Test::Unit のソースを調べる
実際のとこ、どうなっているのか。ソースコードに聞いてみよう。場所は /usr/local/lib/ruby/1.8/test/。まずは動作を確認するために、unit.rb の 276 行目だ。
at_exit do
unless $! || Test::Unit.run?
exit Test::Unit::AutoRunner.run
end
end
このコードは require されたときに実行され、at_exit で Test::Unit::AutoRunner の run メソッドが呼び出される。この定義は autorunner.rb だ。
module Test
module Unit
class AutoRunner
def self.run(force_standalone=false, default_dir=nil, argv=ARGV, &block)
r = new(force_standalone || standalone?, &block)
if((!r.process_args(argv)) && default_dir)
r.to_run << default_dir
end
r.run
end
怪しげなコードを発見できた。 インスタンスの process_args メソッドに ARGV をそのまま渡している。
def process_args(args = ARGV)
begin
options.order!(args) {|arg| @to_run << arg}
rescue OptionParser::ParseError => e
puts e
puts options
$! = nil
abort
else
@filters << proc{false} unless(@filters.empty?)
end
not @to_run.empty?
end
肝になるのは options.order!(args) の部分だ。options では、OptionParser インスタンスが返り、そして、OptionParser#order! はマニュアルによると、
与えられた argv を順番にパースします。 オプションではないコマンドの引数(下の例で言うと somefile)に出会うと、パースを中断します。 ブロックが与えられている場合は、パースを中断せずに 引数をブロックに渡してブロックを評価し、パースを継続します。argv を返します。
<strong>order! は与えられた argv を破壊的にパースします。 argv からオプションがすべて取り除かれます。</strong>
とのことなので、ここで ARGV の中身が破壊されているのは明白だ。
test.rb を変更して対策
では、どうするか?
結局、Test::Unit::AutoRunner.run で直接 ARGV を渡してしまうのが不味いわけで、自前で Test::Unit::AutoRunner.run を呼び出し、そのさいに ARGV のコピーを渡すようにしてみた。
--- test.rb.orig 2006-12-28 01:20:19.000000000 +0900
+++ test.rb 2006-12-28 01:21:04.000000000 +0900
@@ -1429,3 +1429,4 @@
end
end if Mysql.client_version >= 40100
+Test::Unit::AutoRunner.run(false, nil, ARGV.dup)
これでテストも実行できるはず ...
% ruby ./test.rb localhost root
Loaded suite ./test
Started
....................................................................FF...........................................
Finished in 0.32177 seconds.
通った!...
1) Failure:
test_fetch_double(TC_MysqlStmt2) [./test.rb:920]:
<-1.79769313486232e+308> and
<-1.79769313486232e+308> expected to be within
<2.22044604925031e-16> of each other.
2) Failure:
test_fetch_double_unsigned(TC_MysqlStmt2) [./test.rb:937]:
<1.79769313486232e+308> and
<1.79769313486232e+308> expected to be within
<2.22044604925031e-16> of each other.
... と思ったら駄目でした。
うーん、高速化パッチを適用せずにテストしても同じ結果なので、これはまた別の原因だな。 まあ、浮動小数点関係っぽいので環境依存かもしれん、、てことでスルーしとこう。
Mac OS X に MySQL/Ruby をインストール + 高速化パッチ
長かった Ruby on Rails 環境構築シリーズもやっと終盤です。
Ruby から MySQL につなぐためのバインディングである MySQL/Ruby をインストールすれば、Ruby on Rails の開発環境構築は一段落。
なお、同様のバインディングである Ruby/MySQL もあり、こちらは Ruby で書かれているためコンパイルが不要。MySQL/Ruby とも、ほぼ互換性がある。ただ、今回はパフォーマンス優先でいきたいと思う。
さて、ダウンロードページから最新版をダウンロード。現時点では mysql-ruby-2.7.3.tar.gz が最新版のようだ。
% curl --location -O http://tmtm.org/downloads/mysql/ruby/mysql-ruby-2.7.3.tar.gz % tar xvzf mysql-ruby-2.7.3.tar.gz % cd mysql-ruby-2.7.3
さきほど「パフォーマンス優先」と書いたけど、RailsExpress.blog の人が MySQL/Ruby を 30% 高速にするパッチを公開している。記事を読んだときから試してみたかったので、これを機会に使ってみよう。
パッチをダウンロードして、MySQL/Ruby の展開先に置いたら、patch コマンドでパッチを適用する。
% curl -O http://railsexpress.de/downloads/mysql-ruby-2.7-less-string-copies-in-each-hash.diff % patch < mysql-ruby-2.7-less-string-copies-in-each-hash.diff patching file mysql.c.in Hunk #1 succeeded at 1009 (offset -1 lines). Hunk #2 succeeded at 1027 (offset -1 lines). Hunk #3 succeeded at 1059 (offset -1 lines). Hunk #4 succeeded at 1136 (offset -1 lines). Hunk #5 succeeded at 2079 (offset 3 lines).
無事、パッチを適用できたようなので、そのままインストールに進む。extconf.rb を実行するときに、--with-mysql-dir オプションで MySQL のインストール先ディレクトリを指定する必要があった。
% ruby extconf.rb --with-mysql-dir=/usr/local/mysql checking for mysql_query() in -lmysqlclient... no checking for main() in -lm... yes checking for mysql_query() in -lmysqlclient... no checking for main() in -lz... yes checking for mysql_query() in -lmysqlclient... yes checking for mysql_ssl_set()... yes checking for mysql.h... no checking for mysql/mysql.h... yes creating Makefile
さて、コンパイル ...
% make ... mysql.c: In function 'Init_mysql': mysql.c:2018: error: 'ulong' undeclared (first use in this function)
怒られてしまった。ulong 型が定義されていないようだ。 検索してみて、ここに書かれている解決策を採用。ただし unsigned long ではなく u_long にしておく。
mysql.c の最初(16 行目くらい)に
#define ulong u_long
を追加してリトライ。
% make gcc -I. -I. -I/usr/local/lib/ruby/1.8/i686-darwin8.8.3 -I. -DHAVE_MYSQL_SSL_SET -DHAVE_MYSQL_MYSQL_H -I/usr/local/mysql/include -fno-common -g -O2 -pipe -fno-common -c mysql.c cc -dynamic -bundle -undefined suppress -flat_namespace -L"/usr/local/mysql/lib" -L"/usr/local/lib" -L"/usr/local/mysql/lib/mysql" -o mysql.bundle mysql.o -lmysqlclient -lz -lm -ldl -lobjc
無事コンパイルできた。 しかし、テストが通らない ...
% ruby ./test.rb localhost ishikawa password ... 113 tests, 44 assertions, 3 failures, 177 errors
うーむ。つづきは今度。
MySQL をインストールしたあとの作業
初期化
MySQL のインストールが完了したら、マニュアル通り、インストール後の作業を進めていく。
mysql_install_db でデータ格納ディレクトリとアカウントを初期化する。
% cd /usr/local/mysql/ % sudo chown -R mysql:mysql /usr/local/mysql % sudo bin/mysql_install_db --user=mysql
データベースを起動。
% sudo bin/mysqld_safe --user=mysql &
いくつかのコマンドを実行して、動作を確認。
% bin/mysqladmin version % bin/mysqladmin variables % bin/mysqlshow
停止。
% sudo bin/mysqladmin -u root shutdown
アカウントの設定
インストール直後の MySQL には 2 つのアカウントが用意されている。
- root - パスワードなし - local host からの接続のみ許可
- 匿名アカウント - パスワードなし - local host からの接続のみ許可 - test または test_ で始まる名前のデータベースへの権限
つまり、最初から用意されているアカウントには、いずれもパスワードが設定されていない。これはセキュリティ的によろしくないので、次のふたつを実行する。
- 匿名アカウントの削除
- root にパスワードを設定
MySQL を起動し、mysql -u root で接続。
まずは、匿名アカウントの削除を削除する。
mysql> DELETE FROM mysql.user WHERE User = ''; mysql> FLUSH PRIVILEGES;
root にパスワードを設定する。
mysql> SET PASSWORD FOR 'root'\@'localhost' = PASSWORD('newpwd');
mysql> SET PASSWORD FOR 'root'\@'%' = PASSWORD('newpwd');
アカウントの追加
実際に、アプリケーションを開発する場合は、自分のアカウントを作っておいた方がいいだろう。
アカウントの追加は GRANT コマンドで行う。
mysql> GRANT ALL PRIVILEGES ON *.* TO 'ishikawa'\@'localhost'
-> IDENTIFIED BY 'newpwd' WITH GRANT OPTION;
ここでは ishikawa というアカウントを作成した。開発用のため、外部から接続する必要はないので、local host からのみのアクセスを許可した。
Mac OS X Tiger に Ruby をインストール
Mac OS X Tiger に標準にバンドルされている ruby は irb でヒストリー機能が利用できない、など使い勝手が悪い上、脆弱性も修正されていないバージョンなので、最新版をソースからインストールする。
なお、古いバージョンの Ruby に存在する脆弱性については以下を参照(最新版では修正済み)。
まずは、脆弱性を修正するパッチを適用した最新版のソースコードをダウンロードして展開。
% curl --location -O ftp://ftp.ruby-lang.org/pub/ruby/ruby-1.8.5-p12.tar.gz % tar xvzf ruby-1.8.5-p12.tar.gz
あとは普通に configure, make。
% cd ruby-1.8.5-p12 % ./configure % make % sudo make install