CouchDBのGetting startedには Ruby用のCouchモジュールが掲載されています。
Basic認証に対応させたり、SSLクライアント認証対応にしたりしてきましたが、今回はDigest認証に対応させる事にしました。
これはCouchDBの標準機能ではありません。フロントエンドにApacheなどでProxy Serverを配置し、そのProxy ServerがDigest認証を行なう事を想定しています。
なおDigest認証の仕様は RFC2069 - An Extension to HTTP : Digest Access Authenticationで定義されています。
使い方のサンプル
Couch::Server#newメソッドの第三引数のハッシュに'digest_auth'プロパティを設定します。 他はBasic認証と同じです。
couch = Couch::Server.new("couch.example.org","443", {
'digest_auth' => 'true',
'user' => 'admin',
'password' => 'xxxxxxxxxxxx',
...})
他は同じように使えますが、ループの中で繰り返し実行するような場合には#get, #post, #put, #deleteなどの各メソッドで再認証を求められる可能性を考慮する必要があります。
open(file).each_line do |line| row = json = Hash.new ... res = "" while not res.kind_of?(Net::HTTPSuccess) begin res = couch.post(uri, json.to_json) rescue p $! end end end
ライブラリを少し変更すると自動的に再認証することもできますが、最大回数を設定するなどの配慮が必要になるでしょう。
Digest情報のキャッシュ
今回利用したライブラリのサンプルでは再利用の方法について、サンプルはないようでした。
RFC2069では、再利用できる情報としてusername, password, nonce, nonce count and opaque valuesが挙げられています。
これらの情報を使いまわすようにしていますが、手元のApacheを使った環境では5分毎に再認証(rc=401)を求められます。
この挙動への対応として、ライブラリ側でのコントロールをしない方法を選択しました。 オリジナルのライブラリで行なっていたレスポンスコードに応じた例外の送出は行なわれません。
レスポンスオブジェクトはそのままクライアントに返すので、クライアント側では再認証が発生した場合に再度メソッドを呼び出すなど判断をしてください。
net-http-digest_authモジュールの導入
標準ライブラリではDigest認証に対応しません。今回は net-http-digest_authを利用しました。
このライブラリのロードはdigest_authプロパティを設定した場合に発生するので、クライアント側でrubygemsを呼び出すなどの準備が必要です。
couchdb.rbと同じディレクトリにnetディレクトリを配置した場合の設定例
main.rbからcouchdb.rbを呼び出す、次のようなディレクトリ構造を想定しています。
bin/main.rb lib/ lib/couchdb.rb lib/net/http/digest_auth.rb
main.rbでは次のようにライブラリをロードします。
$:.unshift File.join([File.dirname($0),"..","lib"])
require 'couchdb'
gemsを使う場合の設定例
今回もmain.rbからcouchdb.rbを呼び出す、次のようなディレクトリ構造を想定しています。
$ cd lib $ gem install -i gems net-http-digest_auth
bin/main.rb lib/couchdb.rb lib/gems/gems/net-http-digest_auth-1.0/lib/net/http/digest_auth.rb
標準以外の場所にgemsディレクトリがある場合の対応は以下のようになります。
(-iオプションを使わずに)標準的な場所にインストールした場合には、当然、ENV['GEM_HOME']
の行は不要です。
ENV['GEM_HOME'] = File.join([File.dirname($0),"..","lib","gems"])
require 'rubygems'
$:.unshift File.join([File.dirname($0),"..","lib"])
require 'couchdb'
変更を加えたCouchモジュール
モジュールの全体は次の通りです。
# -*- coding: utf-8 -*-
require 'net/https'
#
# This module comes from the couchdb wiki;
# http://wiki.apache.org/couchdb/Getting_started_with_Ruby
#
# Modifyed by Yasuhiro ABE - yasu@yasundial.org
#
module Couch
class Server
def initialize(host, port, options = nil)
@host = host
@port = port
@options = options
@options = Hash.new if options.nil? or not options.kind_of?(Hash)
@www_auth = nil
@auth = nil
if options.has_key?('digest_auth')
require 'net/http/digest_auth'
@digest_auth = Net::HTTP::DigestAuth.new
end
end
def delete(uri)
setup_digest_auth(uri,'DELETE')
request(Net::HTTP::Delete.new(uri))
end
def get(uri)
setup_digest_auth(uri,'GET')
request(Net::HTTP::Get.new(uri))
end
def put(uri, json)
setup_digest_auth(uri,'PUT')
req = Net::HTTP::Put.new(uri)
req["content-type"] = "application/json"
req.body = json
request(req)
end
def post(uri, json)
setup_digest_auth(uri,'POST')
req = Net::HTTP::Post.new(uri)
req["content-type"] = "application/json"
req.body = json
request(req)
end
def check_ssl(client)
if @options.has_key?('cacert')
client.use_ssl = true
client.ca_file = @options['cacert']
client.verify_mode = @options['ssl_verify_mode'] if @options.has_key?('ssl_verify_mode')
client.verify_mode = OpenSSL::SSL::VERIFY_PEER if not @options.has_key?('ssl_verify_mode')
client.verify_depth = @options['ssl_verify_depth'] if @options.has_key?('ssl_verify_depth')
client.verify_depth = 5 if not @options.has_key?('ssl_verify_depth')
client.cert = @options['ssl_client_cert'] if @options.has_key?('ssl_client_cert')
client.key = @options['ssl_client_key'] if @options.has_key?('ssl_client_key')
end
end
def request(req)
req.basic_auth @options['user'], @options['password'] if @options.has_key?('user') and @options.has_key?('password') and not @options.has_key?('digest_auth')
req["X-Auth-CouchDB-UserName"] = @options['proxy_auth_user'] if @options.has_key?('proxy_auth_user')
req["X-Auth-CouchDB-Roles"] = @options['proxy_auth_roles'] if @options.has_key?('proxy_auth_roles')
req["X-Auth-CouchDB-Token"] = @options['proxy_auth_token'] if @options.has_key?('proxy_auth_token')
client = Net::HTTP.new(@host, @port)
check_ssl(client)
if @options.has_key?('digest_auth')
req["Authorization"] = @auth
end
res = client.start { |http| http.request(req) }
@www_auth = nil if res.kind_of?(Net::HTTPUnauthorized) and @options.has_key?('digest_auth')
res
end
private
def setup_digest_auth(uri, method)
return if not @options.has_key?('digest_auth')
if @www_auth == nil
req = Net::HTTP::Get.new(uri)
client = Net::HTTP.new(@host, @port)
check_ssl(client)
res = client.start { |http| http.request(req) }
## res must be the Net::HTTPUnauthorized
raise res if not res.kind_of?(Net::HTTPUnauthorized)
@www_auth = res['www-authenticate']
end
url = TinyURI.new(@options['user'], @options['password'], uri)
@auth = @digest_auth.auth_header(url, @www_auth, method)
end
end
private
# net/http/digest_auth using this class to pass information.
class TinyURI # :nodoc:all
attr_accessor :request_uri, :user, :password
def initialize(user, pass, path)
@user = user
@password = pass
@request_uri = path
end
end
end
2010/11/30 22:00追記
認証に失敗した場合に@www_authを初期化するコードが抜けていたので追記しています。
モジュールの使い方: README.rd
手元で書いたREADME.rbの内容をそのまま転載します。
ruby用Couchモジュールについて
http://wiki.apache.org/couchdb/Getting_started_with_Ruby に掲載されているモジュールをベースにしています。
追加した機能は次の通りです。
- Proxy認証 (couch_httpd_auth:proxy_authentification_handler用)
- SSLクライアント認証 (stunnelを想定したセキュア接続用)
- Basic認証 (couch_httpd_auth:default_authentication_handler, apache等proxy serverでの認証用)
- Digest認証 (apache等proxy serverでの認証用)
基本的な使い方はCouch::Server.new(host,port,options)でインスタンス生成時のoptions引数にハッシュを与えます。
プロパティ認証用プロパティ
optionsにプロパティが設定されていない場合には何もしません。 値がセットされている場合に、各ヘッダにその値を指定します。
- proxy_auth_user (X-Auth-CouchDB-UserNameヘッダの値にセット)
- proxy_auth_roles (X-Auth-CouchDB-Rolesヘッダの値にセット)
- proxy_auth_token (X-Auth-CouchDB-Tokenヘッダの値にセット)
現状ではこれらの値の設定をサポートするメソッドは提供されません。
Basic and Digest認証用プロパティ
認証を有効にするためには、user, password両方とも設定する必要があります。
- user (任意の文字列)
- password (任意の文字列)
Digest認証用プロパティ
次の"digest_auth"が設定され、@options.has_key?('digest_auth')がtrueを返す場合に有効です。
- digest_auth (設定されている場合に有効)
この機能のテストでは{ "digest_auth" => "true" } のようにダミーの文字列を設定しています。
Digest認証を使うクライアントで必要なライブラリのロード
Digest認証を有効にした場合には、net/http/digest_auth モジュールをロードしようとします。
デフォルトでは有効になっていないので、モジュールを適切な方法で配置してください。
方法の1つは次のようにcouchdb.rbと同じディレクトリにnetディレクトリを配置して、相対パスでこのlibディレクトリを$:変数に含める方法です。
bin/main.rb lib/ lib/couchdb.rb lib/net/http/digest_auth.rb
main.rbには次のように記述します。
$:.unshift File.join([File.dirname($0),"..","lib"]) require 'couchdb'
方法の2つめはgemsを使い、require 'rubygems'を加える方法です。 デフォルトの場所以外にgemsでインストールした場合には次のように、その場所をポイントする必要があります。
$ gem install -i gems net-http-digest_auth
次のようなディレクトリ構造だとします。
bin/main.rb lib/couchdb.rb lib/gems/gems/net-http-digest_auth-1.0/lib/net/http/digest_auth.rb
この場合は、main.rbの先頭は次のようになるでしょう。
ENV['GEM_HOME'] = File.join([File.dirname($0),"..","lib","gems"]) require 'rubygems' $:.unshift File.join([File.dirname($0),"..","lib"]) require 'couchdb'
前者の方法はシンプルですがライブラリのメンテナンスを考えるならgemsの利用も検討するべきです。
それにドメイン毎の事情を考慮して、他の方法を検討することはとても素晴しいアプローチです。
SSLクライアント認証用プロパティ
stunnelを使って検証しています。
cacertが設定されている場合に、自動的にNet::HTTP#use_sslを有効にします。 それぞれに設定する値の詳細は、net/httpsのNet::HTTPクラスライブラリを参照してください。
- cacert (デフォルト値なし。pemファイルへのパス文字列)
- ssl_verify_mode (default: OpenSSL::SSL::VERIFY_PEER。another value: OpenSSL::SSL::VERIFY_NONE)
- ssl_verify_depth (default: 5)
- ssl_client_cert (default値なし。OpenSSL::X509::Certificate オブジェクト)
- ssl_client_key (default値なし。OpenSSL::PKey::RSA オブジェクト、もしくは、OpenSSL::PKey::DSA オブジェクト)
SSLクライアント認証とBasic認証を組み合せる場合のコーディング例
n少なくとも次のような方法で各オブジェクトを準備し、Couch::Serverクラスのインスタンスを生成する事が必要です。 Basic認証が不要な場合は、user, passwordプロパティを設定しないでください。
詳細はnet/httpsのNet::HTTPクラスライブラリの各メソッドから、OpenSSL:SSLクラスライブラリと併せて参照してください。
ssl_client_cert = OpenSSL::X509::Certificate.new(File.new(File.expand_path('stunnel.client.cert.pem', File.dirname($0)))) ssl_client_key = OpenSSL::PKey::RSA.new(File.new(File.expand_path('stunnel.client.key.pem', File.dirname($0))), 'xxxxxxx') couch = Couch::Server.new("couch.example.org","5984", { 'user' => 'admin', 'password' => 'xxxxxxxxxx', 'cacert' => File.expand_path('cacert.pem', File.dirname($0)), 'ssl_client_cert' => ssl_client_cert, 'ssl_client_key' => ssl_client_key })
SSLサーバ認証について
SSLサーバ認証は、SSLクライアント認証のサブセットで、少なくともcacertプロパティにはサーバ側の証明書が検証できるPEMファイルを設定することが必要です。
その他のプロパティは任意です。