SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|05|06|07|08|09|10|11|

2024-09-10(Tue) RubyでDigest認証クライアントを作る

  ちょっとした理由でウェブアクセスするクライアントを書き始めたら、相手にDigest認証を求められた。

  あれ。Rubyの標準ライブラリにはBasic認証の機能しかない。TLSの普及につれ、Digest認証が廃れて、Basic認証が復活してくる傾向とは思っていたが、相手サーバがDigest認証で固定なら、それに対応せざるをえない。

  標準ライブラリになくたって、誰か書いているだろう、と思ったが、検索してもRailsはよくわからんし、WEBrickはサーバ側の話だろうし……まぁ、自分で書くかと。車輪の再発明だとしても楽しいしな。

  Digest認証が面倒なのは、認証に先立って相手からnonceというチャレンジコードをもらう必要があるので、最初に失敗アクセス(401:Unauthorized)を行うことが必須であることだ。Basic認証ならば、最初から認証情報を送ることができるので、失敗アクセスは必須ではない。

  RFCを眺めつつ、既存のRubyのライブラリに近い使い勝手で実装する。できた。

require './http_digest_auth'
res = nil; auth_res = nil
uri = URI.parse('http://localhost:8080/digest/')
req = Net::HTTP::Get.new(uri.path)
3.times {
    auth_res = req.digest_auth('username', 'password', res, auth_res)
    res = Net::HTTP.start(uri.host, uri.port) {|http|
        http.request(req)
    }
    puts(res.code)
    res.code.to_i < 400 and break
}
puts(res.body)

  既存のRubyのライブラリでBasic認証する場合のサンプルコードが以下なので、できるだけ近づけた。ただし、最初に失敗アクセスを行うことが必須なので、ループしたり、認証情報の引き渡したりする処理が入るのは仕方ない。逆に、上記のスタイルでもBasic認証は動く。

require 'net/http'
uri = URI.parse('http://localhost:8080/basic/')
req = Net::HTTP::Get.new(uri.path)
req.basic_auth('username', 'password')
res = Net::HTTP.start(uri.host, uri.port) {|http|
    http.request(req)
}
puts(res.body)

  ライブラリのコードは以下。RFCのまんまといえば、RFCのまんま。

require 'net/http'
require 'digest/md5'
 
class Net::HTTPGenericRequest
 
    attr_accessor :secret_data
 
    def digest_auth(username, passwd, res, auth_res = nil)
 
        @nc ||= 0
        @secret_data ||= 'secret-data'
 
        @auth_res = auth_res || {}
        if(res and res.header['www-authenticate'] =~ /Digest\s+(.+)/i)
            $1.split(/,\s*/).each {|kv0|
                kv = kv0.split(/\s*=\s*/, 2)
                kv[1].strip =~ /^\"(.*)\"$/ and kv[1] = $1
                @auth_res[kv[0]] = kv[1]
            }
        end
 
        if(@auth_res['realm'])
            ha1 = Digest::MD5.hexdigest('%s:%s:%s' % [username, @auth_res['realm'], passwd])
            ha2 = Digest::MD5.hexdigest('%s:%s' % [method, path])
            nc = '%08X' % [@nc += 1]
            cnonce = Digest::MD5.hexdigest('%s:%s' % [Time.now.to_f, @secret_data])
            qop = 'auth'                                                                        # TODO
            response = Digest::MD5.hexdigest('%s:%s:%s:%s:%s:%s' % [ha1, @auth_res['nonce'], nc, cnonce, qop, ha2])
 
            auth_req = []
            auth_req << 'username="%s"' % username
            auth_req << 'realm="%s"' % @auth_res['realm']
            auth_req << 'uri="%s"' % path
            auth_req << 'algorithm=%s' % 'MD5'                                                  # TODO
            auth_req << 'nonce="%s"' % @auth_res['nonce']
            auth_req << 'nc=%s' % nc
            auth_req << 'cnonce="%s"' % cnonce
            auth_req << 'qop=%s' % qop
            auth_req << 'response="%s"' % response
            auth_req << 'opaque="%s"' % @auth_res['opaque'] if(@auth_res['opaque'])
 
            @header['authorization'] = ['Digest ' + auth_req.join(', ')]
        end
 
        @auth_res
    end
end

  なかなか美しく書けた気がする。ふぅ。