How can I get soap4r to work with digest authentication?

Discussion in 'Ruby' started by yonatan_avraham@hotmail.com, Dec 21, 2005.

  1. Guest

    Hello,

    I used wsdl2rb.rb to create a SOAP client and got it to work with basic
    authentication by adding one single line of code to the
    defaultDriver.rb file that was built by wsdl2rb.rb. (I also had to
    install http-access2 over my Ruby installation to avoid a runtime
    error.)

    options["protocol.http.basic_auth"]<<
    "https://www.example.com/somewhere/, "user", "passwd"]

    So far so good.

    Now: How do I add support for digest authentication to my client?

    I'm using Ruby 1.8.1 for Windows.
    I've installed soap4r 1.5.5.
    I've installed http-access2 2.0.6.

    Thanks,
    Yonatan
     
    , Dec 21, 2005
    #1
    1. Advertising

  2. Guest

    I was able to get digest authentication working by incorporating the
    patch I found here into http-access2:

    http://dev.ctor.org/http-access2/ticket/27

    For what it's worth, here is my version with the patch. I really
    hardcoded things quite a bit. Concept wise, it's totally not correct,
    because it relies on the basic_auth property. If the basic_auth
    property was set, it attempts to add both basic and digest. I did
    verify, though, that it works (on my setup) against both a server that
    requires basic and a server that requires digest:

    # HTTPAccess2 - HTTP accessing library.
    # Copyright (C) 2000-2005 NAKAMURA, Hiroshi <>.

    # This program is copyrighted free software by NAKAMURA, Hiroshi. You
    can
    # redistribute it and/or modify it under the same terms of Ruby's
    license;
    # either the dual license version in 2003, or any later version.

    # http-access2.rb is based on http-access.rb in http-access/0.0.4.
    Some part
    # of code in http-access.rb was recycled in http-access2.rb. Those
    part is
    # copyrighted by Maehashi-san.


    # Ruby standard library
    require 'timeout'
    require 'uri'
    require 'socket'
    require 'thread'

    require 'digest/md5'


    # Extra library
    require 'http-access2/http'
    require 'http-access2/cookie'


    module HTTPAccess2
    VERSION = '2.0.6'
    RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})
    [#{RUBY_PLATFORM}]"
    s = %w$Id: http-access2.rb 114 2005-09-13 03:20:38Z nahi $
    RCS_FILE, RCS_REVISION = s[1][/.*(?=,v$)/], s[2]

    SSLEnabled = begin
    require 'openssl'
    true
    rescue LoadError
    false
    end

    DEBUG_SSL = true


    # DESCRIPTION
    # HTTPAccess2::Client -- Client to retrieve web resources via HTTP.
    #
    # How to create your client.
    # 1. Create simple client.
    # clnt = HTTPAccess2::Client.new
    #
    # 2. Accessing resources through HTTP proxy.
    # clnt = HTTPAccess2::Client.new("http://myproxy:8080")
    #
    # 3. Set User-Agent and From in HTTP request header.(nil means "No
    proxy")
    # clnt = HTTPAccess2::Client.new(nil, "MyAgent",
    "")
    #
    # How to retrieve web resources.
    # 1. Get content of specified URL.
    # puts clnt.get_content("http://www.ruby-lang.org/en/")
    #
    # 2. Do HEAD request.
    # res = clnt.head(uri)
    #
    # 3. Do GET request with query.
    # res = clnt.get(uri)
    #
    # 4. Do POST request.
    # res = clnt.post(uri)
    # res = clnt.get|post|head(uri, proxy)
    #
    class Client
    attr_reader :agent_name
    attr_reader :from
    attr_reader :ssl_config
    attr_accessor :cookie_manager
    attr_reader :test_loopback_response

    class << self
    %w(get_content head get post put delete options trace).each do
    |name|
    eval <<-EOD
    def #{name}(*arg)
    new.#{name}(*arg)
    end
    EOD
    end
    end

    # SYNOPSIS
    # Client.new(proxy = nil, agent_name = nil, from = nil)
    #
    # ARGS
    # proxy A String of HTTP proxy URL. ex. "http://proxy:8080".
    # agent_name A String for "User-Agent" HTTP request header.
    # from A String for "From" HTTP request header.
    #
    # DESCRIPTION
    # Create an instance.
    # SSLConfig cannot be re-initialized. Create new client.
    #
    def initialize(proxy = nil, agent_name = nil, from = nil)
    @proxy = nil # assigned later.
    @no_proxy = nil
    @agent_name = agent_name
    @from = from
    @basic_auth = BasicAuth.new(self)
    @digest_auth = DigestAuth.new(self)
    @debug_dev = nil
    @ssl_config = SSLConfig.new(self)
    @redirect_uri_callback = method:)default_redirect_uri_callback)
    @test_loopback_response = []
    @session_manager = SessionManager.new
    @session_manager.agent_name = @agent_name
    @session_manager.from = @from
    @session_manager.ssl_config = @ssl_config
    @cookie_manager = WebAgent::CookieManager.new
    self.proxy = proxy
    end

    def debug_dev
    @debug_dev
    end

    def debug_dev=(dev)
    @debug_dev = dev
    reset_all
    @session_manager.debug_dev = dev
    end

    def protocol_version
    @session_manager.protocol_version
    end

    def protocol_version=(protocol_version)
    reset_all
    @session_manager.protocol_version = protocol_version
    end

    def connect_timeout
    @session_manager.connect_timeout
    end

    def connect_timeout=(connect_timeout)
    reset_all
    @session_manager.connect_timeout = connect_timeout
    end

    def send_timeout
    @session_manager.send_timeout
    end

    def send_timeout=(send_timeout)
    reset_all
    @session_manager.send_timeout = send_timeout
    end

    def receive_timeout
    @session_manager.receive_timeout
    end

    def receive_timeout=(receive_timeout)
    reset_all
    @session_manager.receive_timeout = receive_timeout
    end

    def proxy
    @proxy
    end

    def proxy=(proxy)
    if proxy.nil?
    @proxy = nil
    else
    if proxy.is_a?(URI)
    @proxy = proxy
    else
    @proxy = URI.parse(proxy)
    end
    if @proxy.scheme == nil or @proxy.scheme.downcase != 'http' or
    @proxy.host == nil or @proxy.port == nil
    raise ArgumentError.new("unsupported proxy `#{proxy}'")
    end
    end
    reset_all
    @proxy
    end

    def no_proxy
    @no_proxy
    end

    def no_proxy=(no_proxy)
    @no_proxy = no_proxy
    reset_all
    end

    # if your ruby is older than 2005-09-06, do not set socket_sync =
    false to
    # avoid an SSL socket blocking bug in openssl/buffering.rb.
    def socket_sync=(socket_sync)
    @session_manager.socket_sync = socket_sync
    end

    def set_basic_auth(uri, user_id, passwd)
    unless uri.is_a?(URI)
    uri = URI.parse(uri)
    end
    @basic_auth.set(uri, user_id, passwd)
    @digest_auth.set(uri, user_id, passwd)
    end

    =begin
    def set_digest_auth(uri, user_id, passwd)
    unless uri.is_a?(URI)
    uri = URI.parse(uri)
    end
    @digest_auth.set(uri, user_id, passwd)
    end
    =end

    def set_cookie_store(filename)
    if @cookie_manager.cookies_file
    raise RuntimeError.new("overriding cookie file location")
    end
    @cookie_manager.cookies_file = filename
    @cookie_manager.load_cookies if filename
    end

    def save_cookie_store
    @cookie_manager.save_cookies
    end

    def redirect_uri_callback=(redirect_uri_callback)
    @redirect_uri_callback = redirect_uri_callback
    end

    # SYNOPSIS
    # Client#get_content(uri, query = nil, extheader = {}, &block =
    nil)
    #
    # ARGS
    # uri an_URI or a_string of uri to connect.
    # query a_hash or an_array of query part. e.g. { "a" => "b" }.
    # Give an array to pass multiple value like
    # [["a" => "b"], ["a" => "c"]].
    # extheader
    # a_hash of extra headers like { "SOAPAction" => "urn:foo" }.
    # &block Give a block to get chunked message-body of response like
    # get_content(uri) { |chunked_body| ... }
    # Size of each chunk may not be the same.
    #
    # DESCRIPTION
    # Get a_sring of message-body of response.
    #
    def get_content(uri, query = nil, extheader = {}, &block)
    retry_connect(uri, query) do |uri, query|
    get(uri, query, extheader, &block)
    end
    end

    def post_content(uri, body = nil, extheader = {}, &block)
    retry_connect(uri, nil) do |uri, query|
    post(uri, body, extheader, &block)
    end
    end

    def default_redirect_uri_callback(res)
    uri = res.header['location'][0]
    puts "Redirect to: #{uri}" if $DEBUG
    uri
    end

    def head(uri, query = nil, extheader = {})
    request('HEAD', uri, query, nil, extheader)
    end

    def get(uri, query = nil, extheader = {}, &block)
    request('GET', uri, query, nil, extheader, &block)
    end

    def post(uri, body = nil, extheader = {}, &block)
    request('POST', uri, nil, body, extheader, &block)
    end

    def put(uri, body = nil, extheader = {}, &block)
    request('PUT', uri, nil, body, extheader, &block)
    end

    def delete(uri, extheader = {}, &block)
    request('DELETE', uri, nil, nil, extheader, &block)
    end

    def options(uri, extheader = {}, &block)
    request('OPTIONS', uri, nil, nil, extheader, &block)
    end

    def trace(uri, query = nil, body = nil, extheader = {}, &block)
    request('TRACE', uri, query, body, extheader, &block)
    end

    def request(method, uri, query = nil, body = nil, extheader = {},
    &block)
    conn = Connection.new
    conn_request(conn, method, uri, query, body, extheader, &block)
    conn.pop
    end

    # Async interface.

    def head_async(uri, query = nil, extheader = {})
    request_async('HEAD', uri, query, nil, extheader)
    end

    def get_async(uri, query = nil, extheader = {})
    request_async('GET', uri, query, nil, extheader)
    end

    def post_async(uri, body = nil, extheader = {})
    request_async('POST', uri, nil, body, extheader)
    end

    def put_async(uri, body = nil, extheader = {})
    request_async('PUT', uri, nil, body, extheader)
    end

    def delete_async(uri, extheader = {})
    request_async('DELETE', uri, nil, nil, extheader)
    end

    def options_async(uri, extheader = {})
    request_async('OPTIONS', uri, nil, nil, extheader)
    end

    def trace_async(uri, query = nil, body = nil, extheader = {})
    request_async('TRACE', uri, query, body, extheader)
    end

    def request_async(method, uri, query = nil, body = nil, extheader =
    {})
    conn = Connection.new
    t = Thread.new(conn) { |tconn|
    conn_request(tconn, method, uri, query, body, extheader)
    }
    conn.async_thread = t
    conn
    end

    ##
    # Multiple call interface.

    # ???

    ##
    # Management interface.

    def reset(uri)
    @session_manager.reset(uri)
    end

    def reset_all
    @session_manager.reset_all
    end

    private

    def retry_connect(uri, query = nil)
    retry_number = 0
    while retry_number < 10
    res = yield(uri, query)
    if res.status == HTTP::Status::OK
    return res.content
    elsif HTTP::Status.redirect?(res.status)
    uri = @redirect_uri_callback.call(res)
    query = nil
    retry_number += 1
    else
    raise RuntimeError.new("Unexpected response: #{res.header.inspect}")
    end
    end
    raise RuntimeError.new("Retry count exceeded.")
    end

    def conn_request(conn, method, uri, query, body, extheader, &block)
    unless uri.is_a?(URI)
    uri = URI.parse(uri)
    end
    proxy = no_proxy?(uri) ? nil : @proxy
    begin
    req = create_request(method, uri, query, body, extheader,
    !proxy.nil?)
    do_get_block(req, proxy, conn, &block)
    rescue Session::KeepAliveDisconnected
    req = create_request(method, uri, query, body, extheader,
    !proxy.nil?)
    do_get_block(req, proxy, conn, &block)
    end
    end

    def create_request(method, uri, query, body, extheader, proxy)
    if extheader.is_a?(Hash)
    extheader = extheader.to_a
    end



    if cred = @digest_auth.get(uri) then
    extheader << ['Authorization', "Digest " << cred]
    end
    if cred = @basic_auth.get(uri) then
    extheader << ['Authorization', "Basic " << cred]
    end


    =begin
    if cred = @digest_auth.get(uri) then
    extheader << ['Authorization', "Digest" << cred]
    elsif cred = @basic_auth.get(uri)
    extheader << ['Authorization', "Basic" << cred]
    end
    =end


    if cookies = @cookie_manager.find(uri)
    extheader << ['Cookie', cookies]
    end
    boundary = nil
    content_type = extheader.find { |key, value|
    key.downcase == 'content-type'
    }
    if content_type && content_type[1] =~ /boundary=(.+)\z/
    boundary = $1
    end
    req = HTTP::Message.new_request(method, uri, query, body, proxy,
    boundary)
    extheader.each do |key, value|
    req.header.set(key, value)
    end
    if content_type.nil? and !body.nil?
    req.header.set('content-type',
    'application/x-www-form-urlencoded')
    end
    req
    end

    NO_PROXY_HOSTS = ['localhost']

    def no_proxy?(uri)
    if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
    return true
    end
    unless @no_proxy
    return false
    end
    @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
    if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
    (!port || uri.port == port.to_i)
    return true
    end
    end
    false
    end

    # !! CAUTION !!
    # Method 'do_get*' runs under MT conditon. Be careful to change.
    def do_get_block(req, proxy, conn, &block)
    if str = @test_loopback_response.shift
    dump_dummy_request_response(req.body.dump, str) if @debug_dev
    conn.push(HTTP::Message.new_response(str))
    return
    end
    content = ''
    res = HTTP::Message.new_response(content)
    @debug_dev << "= Request\n\n" if @debug_dev
    sess = @session_manager.query(req, proxy)
    @debug_dev << "\n\n= Response\n\n" if @debug_dev
    do_get_header(req, res, sess)
    conn.push(res)
    sess.get_data() do |str|
    block.call(str) if block
    content << str
    end
    @session_manager.keep(sess) unless sess.closed?
    end

    def do_get_stream(req, proxy, conn)
    if str = @test_loopback_response.shift
    dump_dummy_request_response(req.body.dump, str) if @debug_dev
    conn.push(HTTP::Message.new_response(str))
    return
    end
    piper, pipew = IO.pipe
    res = HTTP::Message.new_response(piper)
    @debug_dev << "= Request\n\n" if @debug_dev
    sess = @session_manager.query(req, proxy)
    @debug_dev << "\n\n= Response\n\n" if @debug_dev
    do_get_header(req, res, sess)
    conn.push(res)
    sess.get_data() do |str|
    pipew.syswrite(str)
    end
    pipew.close
    @session_manager.keep(sess) unless sess.closed?
    end

    def do_get_header(req, res, sess)
    res.version, res.status, res.reason = sess.get_status
    sess.get_header().each do |line|
    unless /^([^:]+)\s*:\s*(.*)$/ =~ line
    raise RuntimeError.new("Unparsable header: '#{line}'.") if $DEBUG
    end
    res.header.set($1, $2)
    end
    if res.header['set-cookie']
    res.header['set-cookie'].each do |cookie|
    @cookie_manager.parse(cookie, req.header.request_uri)
    end
    end
    end

    def dump_dummy_request_response(req, res)
    @debug_dev << "= Dummy Request\n\n"
    @debug_dev << req
    @debug_dev << "\n\n= Dummy Response\n\n"
    @debug_dev << res
    end
    end


    # HTTPAccess2::SSLConfig -- SSL configuration of a client.
    #
    class SSLConfig # :nodoc:
    attr_reader :client_cert
    attr_reader :client_key
    attr_reader :client_ca

    attr_reader :verify_mode
    attr_reader :verify_depth
    attr_reader :verify_callback

    attr_reader :timeout
    attr_reader :eek:ptions
    attr_reader :ciphers

    attr_reader :cert_store # don't use if you don't know what it is.

    def initialize(client)
    return unless SSLEnabled
    @client = client
    @cert_store = OpenSSL::X509::Store.new
    @client_cert = @client_key = @client_ca = nil
    @verify_mode = OpenSSL::SSL::VERIFY_PEER |
    OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
    @verify_depth = nil
    @verify_callback = nil
    @dest = nil
    @timeout = nil
    @options = defined?(OpenSSL::SSL::OP_ALL) ?
    OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_SSLv2 : nil
    @ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:mad:STRENGTH"
    end

    def set_client_cert_file(cert_file, key_file)
    @client_cert =
    OpenSSL::X509::Certificate.new(File.open(cert_file).read)
    @client_key = OpenSSL::pKey::RSA.new(File.open(key_file).read)
    change_notify
    end

    def set_trust_ca(trust_ca_file_or_hashed_dir)
    if FileTest.directory?(trust_ca_file_or_hashed_dir)
    @cert_store.add_path(trust_ca_file_or_hashed_dir)
    else
    @cert_store.add_file(trust_ca_file_or_hashed_dir)
    end
    change_notify
    end

    def set_crl(crl_file)
    crl = OpenSSL::X509::CRL.new(File.open(crl_file).read)
    @cert_store.add_crl(crl)
    @cert_store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK |
    OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
    change_notify
    end

    def client_cert=(client_cert)
    @client_cert = client_cert
    change_notify
    end

    def client_key=(client_key)
    @client_key = client_key
    change_notify
    end

    def client_ca=(client_ca)
    @client_ca = client_ca
    change_notify
    end

    def verify_mode=(verify_mode)
    @verify_mode = verify_mode
    change_notify
    end

    def verify_depth=(verify_depth)
    @verify_depth = verify_depth
    change_notify
    end

    def verify_callback=(verify_callback)
    @verify_callback = verify_callback
    change_notify
    end

    def timeout=(timeout)
    @timeout = timeout
    change_notify
    end

    def options=(options)
    @options = options
    change_notify
    end

    def ciphers=(ciphers)
    @ciphers = ciphers
    change_notify
    end

    # don't use if you don't know what it is.
    def cert_store=(cert_store)
    @cert_store = cert_store
    change_notify
    end

    # interfaces for SSLSocketWrap.

    def set_context(ctx)
    # Verification: Use Store#verify_callback instead of
    SSLContext#verify*?
    ctx.cert_store = @cert_store
    ctx.verify_mode = @verify_mode
    ctx.verify_depth = @verify_depth if @verify_depth
    ctx.verify_callback = @verify_callback ||
    method:)default_verify_callback)
    # SSL config
    ctx.cert = @client_cert
    ctx.key = @client_key
    ctx.client_ca = @client_ca
    ctx.timeout = @timeout
    ctx.options = @options
    ctx.ciphers = @ciphers
    end

    # this definition must match with the one in
    ext/openssl/lib/openssl/ssl.rb
    def post_connection_check(peer_cert, hostname)
    check_common_name = true
    cert = peer_cert
    cert.extensions.each{|ext|
    next if ext.oid != "subjectAltName"
    ext.value.split(/,\s+/).each{|general_name|
    if /\ADNS:(.*)/ =~ general_name
    check_common_name = false
    reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+")
    return true if /\A#{reg}\z/i =~ hostname
    elsif /\AIP Address:(.*)/ =~ general_name
    check_common_name = false
    return true if $1 == hostname
    end
    }
    }
    if check_common_name
    cert.subject.to_a.each{|oid, value|
    if oid == "CN" && value.casecmp(hostname) == 0
    return true
    end
    }
    end
    raise OpenSSL::SSL::SSLError, "hostname not match"
    end

    # Default callback for verification: only dumps error.
    def default_verify_callback(is_ok, ctx)
    if $DEBUG
    puts "#{ is_ok ? 'ok' : 'ng' }: #{ctx.current_cert.subject}"
    end
    if !is_ok
    depth = ctx.error_depth
    code = ctx.error
    msg = ctx.error_string
    STDERR.puts "at depth #{depth} - #{code}: #{msg}"
    end
    is_ok
    end

    # Sample callback method: CAUTION: does not check CRL/ARL.
    def sample_verify_callback(is_ok, ctx)
    unless is_ok
    depth = ctx.error_depth
    code = ctx.error
    msg = ctx.error_string
    STDERR.puts "at depth #{depth} - #{code}: #{msg}" if $DEBUG
    return false
    end

    cert = ctx.current_cert
    self_signed = false
    ca = false
    pathlen = nil
    server_auth = true
    self_signed = (cert.subject.cmp(cert.issuer) == 0)

    # Check extensions whatever its criticality is. (sample)
    cert.extensions.each do |ex|
    case ex.oid
    when 'basicConstraints'
    /CA:(TRUE|FALSE), pathlen:(\d+)/ =~ ex.value
    ca = ($1 == 'TRUE')
    pathlen = $2.to_i
    when 'keyUsage'
    usage = ex.value.split(/\s*,\s*/)
    ca = usage.include?('Certificate Sign')
    server_auth = usage.include?('Key Encipherment')
    when 'extendedKeyUsage'
    usage = ex.value.split(/\s*,\s*/)
    server_auth = usage.include?('Netscape Server Gated Crypto')
    when 'nsCertType'
    usage = ex.value.split(/\s*,\s*/)
    ca = usage.include?('SSL CA')
    server_auth = usage.include?('SSL Server')
    end
    end

    if self_signed
    STDERR.puts 'self signing CA' if $DEBUG
    return true
    elsif ca
    STDERR.puts 'middle level CA' if $DEBUG
    return true
    elsif server_auth
    STDERR.puts 'for server authentication' if $DEBUG
    return true
    end

    return false
    end

    private

    def change_notify
    @client.reset_all
    end
    end


    # HTTPAccess2::BasicAuth -- BasicAuth repository.
    #
    class BasicAuth # :nodoc:
    def initialize(client)
    @client = client
    @auth = {}
    end

    def set(uri, user_id, passwd)
    uri = uri.clone
    uri.path = uri.path.sub(/\/[^\/]*$/, '/')
    @auth[uri] = ["#{user_id}:#{passwd}"].pack('m').strip
    @client.reset_all
    end

    def get(uri)
    @auth.each do |realm_uri, cred|
    if ((realm_uri.host == uri.host) and
    (realm_uri.scheme == uri.scheme) and
    (realm_uri.port == uri.port) and
    uri.path.upcase.index(realm_uri.path.upcase) == 0)
    return cred
    end
    end
    nil
    end
    end



    # HTTPAccess2::BasicAuth -- BasicAuth repository.
    #
    class DigestAuth # :nodoc:
    def initialize(client)
    @client = client
    @auth = {}
    @params = {}
    @nonce_count = 0
    end

    def set(uri, user_id, passwd)

    uri = uri.clone


    @client.head(uri).header['WWW-Authenticate'].to_s.gsub(/(\w+)="(.*?)"/)
    { @params[$1] = $2 }
    @a_1 = "#{user_id}:#{@params['realm']}:#{passwd}"

    # XXX: need to obtain proper method (e.g. GET/POST/HEAD)
    #@a_2 = "GET:#{uri.path}"
    @a_2 = "POST:#{uri.path}"

    @cnonce = Digest::MD5.new(Time.now.to_s).hexdigest
    @nonce_count += 1
    @message_digest = ''
    @message_digest << Digest::MD5.new(@a_1).hexdigest
    return if nil == @params['nonce']
    @message_digest << ':' << @params['nonce']
    @message_digest << ':' << ('%08x' % @nonce_count)
    @message_digest << ':' << @cnonce
    @message_digest << ':' << @params['qop']
    @message_digest << ':' << Digest::MD5.new(@a_2).hexdigest

    @header = ''
    @header << "username=\"#{user_id}\", "
    @header << "realm=\"#{@params['realm']}\", "
    @header << "qop=\"#{@params['qop']}\", "
    @header << "algorithm=\"MD5\", "
    @header << "uri=\"#{uri.path}\", "
    @header << "nonce=\"#{@params['nonce']}\", "
    @header << "nc=#{'%08x' % @nonce_count}, "
    @header << "cnonce=\"#{@cnonce}\", "
    @header <<
    "response=\"#{Digest::MD5.new(@message_digest).hexdigest}\", "
    @header << "opaque=\"#{@params['opaque']}\" "

    puts "header->#{@header}"
    puts "message_digest->#{@message_digest}"

    uri.path = uri.path.sub(/\/[^\/]*$/, '/')
    # @auth[uri] = ["#{user_id}:#{passwd}"].pack('m').strip
    @auth[uri] = @header
    @client.reset_all
    end

    def get(uri)
    @auth.each do |realm_uri, cred|
    if ((realm_uri.host == uri.host) and
    (realm_uri.scheme == uri.scheme) and
    (realm_uri.port == uri.port) and
    uri.path.upcase.index(realm_uri.path.upcase) == 0)
    return cred
    end
    end
    nil
    end
    end



    # HTTPAccess2::Site -- manage a site(host and port)
    #
    class Site # :nodoc:
    attr_accessor :scheme
    attr_accessor :host
    attr_reader :port

    def initialize(uri = nil)
    if uri
    @scheme = uri.scheme
    @host = uri.host
    @port = uri.port.to_i
    else
    @scheme = 'tcp'
    @host = '0.0.0.0'
    @port = 0
    end
    end

    def addr
    "#{@scheme}://#{@host}:#{@port.to_s}"
    end

    def port=(port)
    @port = port.to_i
    end

    def ==(rhs)
    if rhs.is_a?(Site)
    ((@scheme == rhs.scheme) and (@host == rhs.host) and (@port ==
    rhs.port))
    else
    false
    end
    end

    def to_s
    addr
    end

    def inspect
    sprintf("#<%s:0x%x %s>", self.class.name, __id__, addr)
    end
    end


    # HTTPAccess2::Connection -- magage a connection(one request and
    response to it).
    #
    class Connection # :nodoc:
    attr_accessor :async_thread

    def initialize(header_queue = [], body_queue = [])
    @headers = header_queue
    @body = body_queue
    @async_thread = nil
    @queue = Queue.new
    end

    def finished?
    if !@async_thread
    # Not in async mode.
    true
    elsif @async_thread.alive?
    # Working...
    false
    else
    # Async thread have been finished.
    @async_thread.join
    true
    end
    end

    def pop
    @queue.pop
    end

    def push(result)
    @queue.push(result)
    end

    def join
    unless @async_thread
    false
    else
    @async_thread.join
    end
    end
    end


    # HTTPAccess2::SessionManager -- manage several sessions.
    #
    class SessionManager # :nodoc:
    attr_accessor :agent_name # Name of this client.
    attr_accessor :from # Owner of this client.

    attr_accessor :protocol_version # Requested protocol version
    attr_accessor :chunk_size # Chunk size for chunked request
    attr_accessor :debug_dev # Device for dumping log for debugging
    attr_accessor :socket_sync # Boolean value for Socket#sync

    # These parameters are not used now...
    attr_accessor :connect_timeout
    attr_accessor :connect_retry # Maximum retry count. 0 for infinite.
    attr_accessor :send_timeout
    attr_accessor :receive_timeout
    attr_accessor :read_block_size

    attr_accessor :ssl_config

    def initialize
    @proxy = nil

    @agent_name = nil
    @from = nil

    @protocol_version = nil
    @debug_dev = nil
    @socket_sync = true
    @chunk_size = 4096

    @connect_timeout = 60
    @connect_retry = 1
    @send_timeout = 120
    @receive_timeout = 60 # For each read_block_size bytes
    @read_block_size = 8192

    @ssl_config = nil

    @sess_pool = []
    @sess_pool_mutex = Mutex.new
    end

    def proxy=(proxy)
    if proxy.nil?
    @proxy = nil
    else
    @proxy = Site.new(proxy)
    end
    end

    def query(req, proxy)
    req.body.chunk_size = @chunk_size
    dest_site = Site.new(req.header.request_uri)
    proxy_site = if proxy
    Site.new(proxy)
    else
    @proxy
    end
    sess = open(dest_site, proxy_site)
    begin
    sess.query(req)
    rescue
    sess.close
    raise
    end
    sess
    end

    def reset(uri)
    unless uri.is_a?(URI)
    uri = URI.parse(uri.to_s)
    end
    site = Site.new(uri)
    close(site)
    end

    def reset_all
    close_all
    end

    def keep(sess)
    add_cached_session(sess)
    end

    private

    def open(dest, proxy = nil)
    sess = nil
    if cached = get_cached_session(dest)
    sess = cached
    else
    sess = Session.new(dest, @agent_name, @from)
    sess.proxy = proxy
    sess.socket_sync = @socket_sync
    sess.requested_version = @protocol_version if @protocol_version
    sess.connect_timeout = @connect_timeout
    sess.connect_retry = @connect_retry
    sess.send_timeout = @send_timeout
    sess.receive_timeout = @receive_timeout
    sess.read_block_size = @read_block_size
    sess.ssl_config = @ssl_config
    sess.debug_dev = @debug_dev
    end
    sess
    end

    def close_all
    each_sess do |sess|
    sess.close
    end
    @sess_pool.clear
    end

    def close(dest)
    if cached = get_cached_session(dest)
    cached.close
    true
    else
    false
    end
    end

    def get_cached_session(dest)
    cached = nil
    @sess_pool_mutex.synchronize do
    new_pool = []
    @sess_pool.each do |s|
    if s.dest == dest
    cached = s
    else
    new_pool << s
    end
    end
    @sess_pool = new_pool
    end
    cached
    end

    def add_cached_session(sess)
    @sess_pool_mutex.synchronize do
    @sess_pool << sess
    end
    end

    def each_sess
    @sess_pool_mutex.synchronize do
    @sess_pool.each do |sess|
    yield(sess)
    end
    end
    end
    end


    # HTTPAccess2::SSLSocketWrap
    #
    class SSLSocketWrap
    def initialize(socket, context, debug_dev = nil)
    unless SSLEnabled
    raise RuntimeError.new(
    "Ruby/OpenSSL module is required for https access.")
    end
    @context = context
    @socket = socket
    @ssl_socket = create_ssl_socket(@socket)
    @debug_dev = debug_dev
    end

    def ssl_connect
    @ssl_socket.connect
    end

    def post_connection_check(host)
    verify_mode = @context.verify_mode || OpenSSL::SSL::VERIFY_NONE
    if verify_mode == OpenSSL::SSL::VERIFY_NONE
    return
    elsif @ssl_socket.peer_cert.nil? and
    check_mask(verify_mode,
    OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT)
    raise OpenSSL::SSL::SSLError, "no peer cert"
    end
    hostname = host.host
    if @ssl_socket.respond_to?:)post_connection_check)
    @ssl_socket.post_connection_check(hostname)
    end
    @context.post_connection_check(@ssl_socket.peer_cert, hostname)
    end

    def peer_cert
    @ssl_socket.peer_cert
    end

    def addr
    @socket.addr
    end

    def close
    @ssl_socket.close
    @socket.close
    end

    def closed?
    @socket.closed?
    end

    def eof?
    @ssl_socket.eof?
    end

    def gets(*args)
    str = @ssl_socket.gets(*args)
    @debug_dev << str if @debug_dev
    str
    end

    def read(*args)
    str = @ssl_socket.read(*args)
    @debug_dev << str if @debug_dev
    str
    end

    def <<(str)
    rv = @ssl_socket.write(str)
    @debug_dev << str if @debug_dev
    rv
    end

    def flush
    @ssl_socket.flush
    end

    def sync
    @ssl_socket.sync
    end

    def sync=(sync)
    @ssl_socket.sync = sync
    end

    private

    def check_mask(value, mask)
    value & mask == mask
    end

    def create_ssl_socket(socket)
    ssl_socket = nil
    if OpenSSL::SSL.const_defined?("SSLContext")
    ctx = OpenSSL::SSL::SSLContext.new
    @context.set_context(ctx)
    ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
    else
    ssl_socket = OpenSSL::SSL::SSLSocket.new(socket)
    @context.set_context(ssl_socket)
    end
    ssl_socket
    end
    end


    # HTTPAccess2::DebugSocket -- debugging support
    #
    class DebugSocket < TCPSocket
    attr_accessor :debug_dev # Device for logging.

    class << self
    def create_socket(host, port, debug_dev)
    debug_dev << "! CONNECT TO #{host}:#{port}\n"
    socket = new(host, port)
    socket.debug_dev = debug_dev
    socket.log_connect
    socket
    end

    private :new
    end

    def initialize(*args)
    super
    @debug_dev = nil
    end

    def log_connect
    @debug_dev << '! CONNECTION ESTABLISHED' << "\n"
    end

    def close
    super
    @debug_dev << '! CONNECTION CLOSED' << "\n"
    end

    def gets(*args)
    str = super
    @debug_dev << str if str
    str
    end

    def read(*args)
    str = super
    @debug_dev << str if str
    str
    end

    def <<(str)
    super
    @debug_dev << str
    end
    end


    # HTTPAccess2::Session -- manage http session with one site.
    # One or more TCP sessions with the site may be created.
    # Only 1 TCP session is live at the same time.
    #
    class Session # :nodoc:

    class Error < StandardError # :nodoc:
    end

    class InvalidState < Error # :nodoc:
    end

    class BadResponse < Error # :nodoc:
    end

    class KeepAliveDisconnected < Error # :nodoc:
    end

    attr_reader :dest # Destination site
    attr_reader :src # Source site
    attr_accessor :proxy # Proxy site
    attr_accessor :socket_sync # Boolean value for Socket#sync

    attr_accessor :requested_version # Requested protocol version

    attr_accessor :debug_dev # Device for dumping log for debugging

    # These session parameters are not used now...
    attr_accessor :connect_timeout
    attr_accessor :connect_retry
    attr_accessor :send_timeout
    attr_accessor :receive_timeout
    attr_accessor :read_block_size

    attr_accessor :ssl_config

    def initialize(dest, user_agent, from)
    @dest = dest
    @src = Site.new
    @proxy = nil
    @socket_sync = true
    @requested_version = nil

    @debug_dev = nil

    @connect_timeout = nil
    @connect_retry = 1
    @send_timeout = nil
    @receive_timeout = nil
    @read_block_size = nil

    @ssl_config = nil

    @user_agent = user_agent
    @from = from
    @state = :INIT

    @requests = []

    @status = nil
    @reason = nil
    @headers = []

    @socket = nil
    end

    # Send a request to the server
    def query(req)
    connect() if @state == :INIT
    begin
    timeout(@send_timeout) do
    set_header(req)
    req.dump(@socket)
    # flush the IO stream as IO::sync mode is false
    @socket.flush unless @socket_sync
    end
    rescue Errno::ECONNABORTED
    close
    raise KeepAliveDisconnected.new
    rescue
    if SSLEnabled and $!.is_a?(OpenSSL::SSL::SSLError)
    raise KeepAliveDisconnected.new
    elsif $!.is_a?(TimeoutError)
    close
    raise
    else
    raise
    end
    end

    @state = :META if @state == :WAIT
    @next_connection = nil
    @requests.push(req)
    end

    def close
    unless @socket.nil?
    @socket.flush
    @socket.close unless @socket.closed?
    end
    @state = :INIT
    end

    def closed?
    @state == :INIT
    end

    def get_status
    version = status = reason = nil
    begin
    if @state != :META
    raise RuntimeError.new("get_status must be called at the beginning of
    a session.")
    end
    version, status, reason = read_header()
    rescue
    close
    raise
    end
    return version, status, reason
    end

    def get_header(&block)
    begin
    read_header() if @state == :META
    rescue
    close
    raise
    end
    if block
    @headers.each do |line|
    block.call(line)
    end
    else
    @headers
    end
    end

    def eof?
    if @content_length == 0
    true
    elsif @readbuf.length > 0
    false
    else
    @socket.closed? or @socket.eof?
    end
    end

    def get_data(&block)
    begin
    read_header() if @state == :META
    return nil if @state != :DATA
    unless @state == :DATA
    raise InvalidState.new('state != DATA')
    end
    data = nil
    if block
    until eof?
    begin
    timeout(@receive_timeout) do
    data = read_body()
    end
    rescue TimeoutError
    raise
    end
    block.call(data) if data
    end
    data = nil # Calling with block returns nil.
    else
    begin
    timeout(@receive_timeout) do
    data = read_body()
    end
    rescue TimeoutError
    raise
    end
    end
    rescue
    close
    raise
    end
    if eof?
    if @next_connection
    @state = :WAIT
    else
    close
    end
    end
    data
    end

    private

    LibNames = "(#{RCS_FILE}/#{RCS_REVISION}, #{RUBY_VERSION_STRING})"

    def set_header(req)
    req.version = @requested_version if @requested_version
    if @user_agent
    req.header.set('User-Agent', "#{@user_agent} #{LibNames}")
    end
    if @from
    req.header.set('From', @from)
    end
    req.header.set('Date', Time.now)
    end

    # Connect to the server
    def connect
    site = @proxy || @dest
    begin
    retry_number = 0
    timeout(@connect_timeout) do
    @socket = create_socket(site)
    begin
    @src.host = @socket.addr[3]
    @src.port = @socket.addr[1]
    rescue SocketError
    # to avoid IPSocket#addr problem on Mac OS X 10.3 +
    ruby-1.8.1.
    # cf. [ruby-talk:84909], [ruby-talk:95827]
    end
    if @dest.scheme == 'https'
    @socket = create_ssl_socket(@socket)
    connect_ssl_proxy(@socket) if @proxy
    @socket.ssl_connect
    @socket.post_connection_check(@dest)
    end
    # Use Ruby internal buffering instead of passing data
    immediatly
    # to the underlying layer
    # => we need to to call explicitely flush on the socket
    @socket.sync = @socket_sync
    end
    rescue TimeoutError
    if @connect_retry == 0
    retry
    else
    retry_number += 1
    retry if retry_number < @connect_retry
    end
    close
    raise
    end

    @state = :WAIT
    @readbuf = ''
    end

    def create_socket(site)
    begin
    if @debug_dev
    DebugSocket.create_socket(site.host, site.port, @debug_dev)
    else
    TCPSocket.new(site.host, site.port)
    end
    rescue SystemCallError => e
    e.message << " (#{site.host}, ##{site.port})"
    raise
    end
    end

    # wrap socket with OpenSSL.
    def create_ssl_socket(raw_socket)
    SSLSocketWrap.new(raw_socket, @ssl_config, (DEBUG_SSL ? @debug_dev
    : nil))
    end

    def connect_ssl_proxy(socket)
    socket << sprintf("CONNECT %s:%s HTTP/1.1\r\n\r\n", @dest.host,
    @dest.port)
    parse_header(socket)
    unless @status == 200
    raise BadResponse.new(
    "connect to ssl proxy failed with status #{@status} #{@reason}")
    end
    end

    # Read status block.
    def read_header
    if @state == :DATA
    get_data {}
    check_state()
    end
    unless @state == :META
    raise InvalidState, 'state != :META'
    end
    parse_header(@socket)
    @content_length = nil
    @chunked = false
    @headers.each do |line|
    case line
    when /^Content-Length:\s+(\d+)/i
    @content_length = $1.to_i
    when /^Transfer-Encoding:\s+chunked/i
    @chunked = true
    @content_length = true # how?
    @chunk_length = 0
    when /^Connection:\s+([\-\w]+)/i,
    /^Proxy-Connection:\s+([\-\w]+)/i
    case $1
    when /^Keep-Alive$/i
    @next_connection = true
    when /^close$/i
    @next_connection = false
    end
    else
    # Nothing to parse.
    end
    end

    # Head of the request has been parsed.
    @state = :DATA
    req = @requests.shift

    if req.header.request_method == 'HEAD'
    @content_length = 0
    if @next_connection
    @state = :WAIT
    else
    close
    end
    end
    @next_connection = false unless @content_length
    return [@version, @status, @reason]
    end

    StatusParseRegexp = %r(\AHTTP/(\d+\.\d+)\s+(\d+)(?:\s+(.*))?\r?\n\z)
    def parse_header(socket)
    begin
    timeout(@receive_timeout) do
    begin
    initial_line = socket.gets("\n")
    if initial_line.nil?
    raise KeepAliveDisconnected.new
    end
    if StatusParseRegexp =~ initial_line
    @version, @status, @reason = $1, $2.to_i, $3
    @next_connection = HTTP.keep_alive_enabled?(@version)
    else
    @version = '0.9'
    @status = nil
    @reason = nil
    @next_connection = false
    @readbuf = initial_line
    break
    end
    @headers = []
    while true
    line = socket.gets("\n")
    unless line
    raise BadResponse.new('Unexpected EOF.')
    end
    line.sub!(/\r?\n\z/, '')
    break if line.empty?
    if line.sub!(/^\t/, '')
    @headers[-1] << line
    else
    @headers.push(line)
    end
    end
    end while (@version == '1.1' && @status == 100)
    end
    rescue TimeoutError
    raise
    end
    end

    def read_body
    if @chunked
    return read_body_chunked()
    elsif @content_length == 0
    return nil
    elsif @content_length
    return read_body_length()
    else
    if @readbuf.length > 0
    data = @readbuf
    @readbuf = ''
    return data
    else
    data = @socket.read(@read_block_size)
    data = nil if data.empty? # Absorbing interface mismatch.
    return data
    end
    end
    end

    def read_body_length
    maxbytes = @read_block_size
    if @readbuf.length > 0
    data = @readbuf[0, @content_length]
    @readbuf[0, @content_length] = ''
    @content_length -= data.length
    return data
    end
    maxbytes = @content_length if maxbytes > @content_length
    data = @socket.read(maxbytes)
    if data
    @content_length -= data.length
    else
    @content_length = 0
    end
    return data
    end

    RS = "\r\n"
    ChunkDelimiter = "0#{RS}"
    ChunkTrailer = "0#{RS}#{RS}"
    def read_body_chunked
    if @chunk_length == 0
    until (i = @readbuf.index(RS))
    @readbuf << @socket.gets(RS)
    end
    i += 2
    if @readbuf[0, i] == ChunkDelimiter
    @content_length = 0
    unless @readbuf[0, 5] == ChunkTrailer
    @readbuf << @socket.gets(RS)
    end
    @readbuf[0, 5] = ''
    return nil
    end
    @chunk_length = @readbuf[0, i].hex
    @readbuf[0, i] = ''
    end
    while @readbuf.length < @chunk_length + 2
    @readbuf << @socket.read(@chunk_length + 2 - @readbuf.length)
    end
    data = @readbuf[0, @chunk_length]
    @readbuf[0, @chunk_length + 2] = ''
    @chunk_length = 0
    return data
    end

    def check_state
    if @state == :DATA
    if eof?
    if @next_connection
    if @requests.empty?
    @state = :WAIT
    else
    @state = :META
    end
    end
    end
    end
    end
    end


    end


    HTTPClient = HTTPAccess2::Client
     
    , Dec 22, 2005
    #2
    1. Advertising

  3. Guest

    Security alert: Don't use my code as-is! I checked with a sniffer and
    it looks like I'm attempting to send the password in the clear before
    sending the digest response.

    It looks like the flow is wrong . . . (Sorry for not testing this
    earlier!)
     
    , Dec 25, 2005
    #3
  4. Guest

    I fixed the problem of always sending the password in the clear by
    checking the HTTP and making sure that the server is indeed requesting
    basic realm. In class BasicAuth I added lines with +:

    def set(uri, user_id, passwd)
    uri = uri.clone

    + # Make sure that the server is really requesting Basic
    Authentication!
    + serverRealm = (@client.head(uri).header['WWW-Authenticate']).join
    + return nil if ("Basic realm".downcase !=
    serverRealm[0,11].downcase)

    uri.path = uri.path.sub(/\/[^\/]*$/, '/')
    @auth[uri] = ["#{user_id}:#{passwd}"].pack('m').strip
    @client.reset_all
    end

    I forgot to mention that the digest authentication in the code I posted
    earlier is hardcoded to build the digest response with "POST" which
    always works for me, as I'm sending SOAP. I'm not sure how to get the
    correct HTTP method. (http-access2 is about 1700 lines of code and I
    haven't had a chance to understand the flow . . . )
     
    , Dec 26, 2005
    #4
  5. -----BEGIN PGP SIGNED MESSAGE-----
    Hash: SHA1

    Hi,

    Sorry I couldn't reply sooner.

    wrote:
    > I fixed the problem of always sending the password in the clear by
    > checking the HTTP and making sure that the server is indeed requesting
    > basic realm. In class BasicAuth I added lines with +:
    >
    > def set(uri, user_id, passwd)
    > uri = uri.clone
    >
    > + # Make sure that the server is really requesting Basic
    > Authentication!
    > + serverRealm = (@client.head(uri).header['WWW-Authenticate']).join
    > + return nil if ("Basic realm".downcase !=
    > serverRealm[0,11].downcase)
    >
    > uri.path = uri.path.sub(/\/[^\/]*$/, '/')
    > @auth[uri] = ["#{user_id}:#{passwd}"].pack('m').strip
    > @client.reset_all
    > end


    I think I understood the problem but the problem is in BasicAuth#get,
    not in BasicAuth#set, right? http-access2 now sends password to a
    defined realm even if WWW-Authenticate is missing.

    I'll fix this. Thanks.

    Regards,
    // NaHi
    -----BEGIN PGP SIGNATURE-----
    Version: GnuPG v1.4.1 (Cygwin)

    iD8DBQFD5curf6b33ts2dPkRAhqxAJ9D5uOyVOpDuLSj3h2csm4n+RKXWQCgjUsE
    ynbn9wvGnjpZ4+jVC9GucmY=
    =k6RG
    -----END PGP SIGNATURE-----
     
    NAKAMURA, Hiroshi, Feb 5, 2006
    #5
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Wayne Carlaw
    Replies:
    0
    Views:
    441
    Wayne Carlaw
    Mar 29, 2005
  2. Tammy Mc
    Replies:
    3
    Views:
    231
    Tammy Mc
    Oct 1, 2006
  3. Ryan Q.
    Replies:
    8
    Views:
    150
    Ryan Q.
    Sep 7, 2008
  4. myalo
    Replies:
    4
    Views:
    1,366
    A. Sinan Unur
    Nov 28, 2007
  5. Replies:
    2
    Views:
    354
    Julian Cromarty
    Jun 26, 2013
Loading...

Share This Page