lib/mechanize/http/agent.rb in mechanize-2.3 vs lib/mechanize/http/agent.rb in mechanize-2.4

- old
+ new

@@ -41,15 +41,13 @@ # A list of hooks to call to handle the content-encoding of a request. attr_reader :content_encoding_hooks # :section: HTTP Authentication + attr_reader :auth_store # :nodoc: attr_reader :authenticate_methods # :nodoc: attr_reader :digest_challenges # :nodoc: - attr_accessor :user - attr_accessor :password - attr_accessor :domain # :section: Redirection # Follow HTML meta refresh and HTTP Refresh. If set to +:anywhere+ meta # refresh tags outside of the head element will be followed. @@ -137,21 +135,19 @@ @robots = false @user_agent = nil @webrobots = nil # HTTP Authentication + @auth_store = Mechanize::HTTP::AuthStore.new @authenticate_parser = Mechanize::HTTP::WWWAuthenticateParser.new @authenticate_methods = Hash.new do |methods, uri| methods[uri] = Hash.new do |realms, auth_scheme| realms[auth_scheme] = [] end end @digest_auth = Net::HTTP::DigestAuth.new @digest_challenges = {} - @password = nil # HTTP auth password - @user = nil # HTTP auth user - @domain = nil # NTLM HTTP domain # SSL @pass = nil @scheme_handlers = Hash.new { |h, scheme| @@ -168,10 +164,37 @@ @http = Net::HTTP::Persistent.new 'mechanize' @http.idle_timeout = 5 @http.keep_alive = 300 end + ## + # Adds credentials +user+, +pass+ for +uri+. If +realm+ is set the + # credentials are used only for that realm. If +realm+ is not set the + # credentials become the default for any realm on that URI. + # + # +domain+ and +realm+ are exclusive as NTLM does not follow RFC 2617. If + # +domain+ is given it is only used for NTLM authentication. + + def add_auth uri, user, password, realm = nil, domain = nil + @auth_store.add_auth uri, user, password, realm, domain + end + + ## + # USE OF add_default_auth IS NOT RECOMMENDED AS IT MAY EXPOSE PASSWORDS TO + # THIRD PARTIES + # + # Adds credentials +user+, +pass+ as the default authentication credentials. + # If no other credentials are available these will be returned from + # credentials_for. + # + # If +domain+ is given it is only used for NTLM authentication. + + def add_default_auth user, password, domain = nil # :nodoc: + @auth_store.add_default_auth user, password, domain + end + + ## # Retrieves +uri+ and parses it into a page or other object according to # PluggableParser. If the URI is an HTTP or HTTPS scheme URI the given HTTP # +method+ is used to retrieve it, along with the HTTP +headers+, request # +params+ and HTTP +referer+. # @@ -263,11 +286,11 @@ response_redirect response, method, page, redirects, referer when Net::HTTPUnauthorized response_authenticate(response, page, uri, request, headers, params, referer) else - raise Mechanize::ResponseCodeError.new(page), "Unhandled response" + raise Mechanize::ResponseCodeError.new(page, 'unhandled response') end end # URI for a proxy connection @@ -468,20 +491,22 @@ if realm = schemes[:digest].find { |r| r.uri == base_uri } then request_auth_digest request, uri, realm, base_uri, false elsif realm = schemes[:iis_digest].find { |r| r.uri == base_uri } then request_auth_digest request, uri, realm, base_uri, true - elsif schemes[:basic].find { |r| r.uri == base_uri } then - request.basic_auth @user, @password + elsif realm = schemes[:basic].find { |r| r.uri == base_uri } then + user, password, = @auth_store.credentials_for uri, realm.realm + request.basic_auth user, password end end def request_auth_digest request, uri, realm, base_uri, iis challenge = @digest_challenges[realm] - uri.user = @user - uri.password = @password + user, password, = @auth_store.credentials_for uri, realm.realm + uri.user = user + uri.password = password auth = @digest_auth.auth_header uri, challenge.to_s, request.method, iis request['Authorization'] = auth end @@ -521,12 +546,12 @@ # Sets a Referer header. Fragment part is removed as demanded by # RFC 2616 14.36, and user information part is removed just like # major browsers do. def request_referer request, uri, referer return unless referer - return if 'https' == referer.scheme.downcase and - 'https' != uri.scheme.downcase + return if 'https'.casecmp(referer.scheme) == 0 and + 'https'.casecmp(uri.scheme) != 0 if referer.fragment || referer.user || referer.password referer = referer.dup referer.fragment = referer.user = referer.password = nil end request['Referer'] = referer @@ -641,18 +666,24 @@ end end def response_authenticate(response, page, uri, request, headers, params, referer) - raise Mechanize::UnauthorizedError, page unless @user || @password - www_authenticate = response['www-authenticate'] - raise Mechanize::UnauthorizedError, page unless www_authenticate - + unless www_authenticate = response['www-authenticate'] then + message = 'WWW-Authenticate header missing in response' + raise Mechanize::UnauthorizedError.new(page, nil, message) + end + challenges = @authenticate_parser.parse www_authenticate + unless @auth_store.credentials? uri, challenges then + message = "no credentials found, provide some with #add_auth" + raise Mechanize::UnauthorizedError.new(page, challenges, message) + end + if challenge = challenges.find { |c| c.scheme =~ /^Digest$/i } then realm = challenge.realm uri auth_scheme = if response['server'] =~ /Microsoft-IIS/ then :iis_digest @@ -660,27 +691,34 @@ :digest end existing_realms = @authenticate_methods[realm.uri][auth_scheme] - raise Mechanize::UnauthorizedError, page if - existing_realms.include? realm + if existing_realms.include? realm + message = 'Digest authentication failed' + raise Mechanize::UnauthorizedError.new(page, challeges, message) + end existing_realms << realm @digest_challenges[realm] = challenge elsif challenge = challenges.find { |c| c.scheme == 'NTLM' } then existing_realms = @authenticate_methods[uri + '/'][:ntlm] - raise Mechanize::UnauthorizedError, page if - existing_realms.include?(realm) and not challenge.params + if existing_realms.include?(realm) and not challenge.params then + message = 'NTLM authentication failed' + raise Mechanize::UnauthorizedError.new(page, challenges, message) + end existing_realms << realm if challenge.params then type_2 = Net::NTLM::Message.decode64 challenge.params - type_3 = type_2.response({ :user => @user, :password => @password, :domain => @domain }, + user, password, domain = @auth_store.credentials_for uri, nil + + type_3 = type_2.response({ :user => user, :password => password, + :domain => domain }, { :ntlmv2 => true }).encode64 headers['Authorization'] = "NTLM #{type_3}" else type_1 = Net::NTLM::Message::Type1.new.encode64 @@ -689,16 +727,19 @@ elsif challenge = challenges.find { |c| c.scheme == 'Basic' } then realm = challenge.realm uri existing_realms = @authenticate_methods[realm.uri][:basic] - raise Mechanize::UnauthorizedError, page if - existing_realms.include? realm + if existing_realms.include? realm then + message = 'Basic authentication failed' + raise Mechanize::UnauthorizedError.new(page, challenges, message) + end existing_realms << realm else - raise Mechanize::UnauthorizedError, page + message = 'unsupported authentication scheme' + raise Mechanize::UnauthorizedError.new(page, challenges, message) end fetch uri, request.method.downcase.to_sym, headers, params, referer end @@ -838,11 +879,11 @@ end body_io.flush body_io.rewind - raise Mechanize::ResponseCodeError, response if + raise Mechanize::ResponseCodeError.new(response, uri) if Net::HTTPUnknownResponse === response content_length = response.content_length unless Net::HTTP::Head === request or Net::HTTPRedirection === response then @@ -1123,6 +1164,8 @@ size >= @max_file_buffer end end + +require 'mechanize/http/auth_store'