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 で同様の問題にぶち当たったことがある。あのときも、contribEasySSLProtocolSocketFactory を使う必要があったっけ。

もう、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_exitTest::Unit::AutoRunnerrun メソッドが呼び出される。この定義は 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_ で始まる名前のデータベースへの権限

つまり、最初から用意されているアカウントには、いずれもパスワードが設定されていない。これはセキュリティ的によろしくないので、次のふたつを実行する。

  1. 匿名アカウントの削除
  2. 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 に標準にバンドルされている rubyirb でヒストリー機能が利用できない、など使い勝手が悪い上、脆弱性も修正されていないバージョンなので、最新版をソースからインストールする。

なお、古いバージョンの 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

Want fries with that?

Open Source Projects