diff --git a/features/digest_authentication.feature b/features/digest_authentication.feature new file mode 100644 index 0000000..019cada --- /dev/null +++ b/features/digest_authentication.feature @@ -0,0 +1,20 @@ +Feature: Digest Authentication + + As a developer + I want to be able to use a service that requires Digest Authentication + Because that is not an uncommon requirement + + Scenario: Passing no credentials to a page requiring Digest Authentication + Given a restricted page at '/protected.html' + When I call HTTParty#get with '/protected.html' + Then it should return a response with a 401 response code + + Scenario: Passing proper credentials to a page requiring Digest Authentication + Given a remote service that returns 'Digest Authenticated Page' + And that service is accessed at the path '/protected.html' + And that service is protected by Digest Authentication + And that service requires the username 'jcash' with the password 'maninblack' + When I call HTTParty#get with '/protected.html' and a digest_auth hash: + | username | password | + | jcash | maninblack | + Then the return value should match 'Digest Authenticated Page' diff --git a/features/steps/httparty_steps.rb b/features/steps/httparty_steps.rb index 3a82c78..b9bd268 100644 --- a/features/steps/httparty_steps.rb +++ b/features/steps/httparty_steps.rb @@ -17,3 +17,11 @@ When /I call HTTParty#get with '(.*)' and a basic_auth hash:/ do |url, auth_tabl :basic_auth => { :username => h["username"], :password => h["password"] } ) end + +When /I call HTTParty#get with '(.*)' and a digest_auth hash:/ do |url, auth_table| + h = auth_table.hashes.first + @response_from_httparty = HTTParty.get( + "http://#{@host_and_port}#{url}", + :digest_auth => { :username => h["username"], :password => h["password"] } + ) +end \ No newline at end of file diff --git a/features/steps/mongrel_helper.rb b/features/steps/mongrel_helper.rb index 80b8d8c..4fc3508 100644 --- a/features/steps/mongrel_helper.rb +++ b/features/steps/mongrel_helper.rb @@ -50,6 +50,35 @@ def add_basic_authentication_to(handler) handler.extend(m) end +def add_digest_authentication_to(handler) + m = Module.new do + attr_writer :username, :password + + def self.extended(base) + base.instance_eval { @custom_headers["WWW-Authenticate"] = 'Digest realm="testrealm@host.com",qop="auth,auth-int",nonce="nonce",opaque="opaque"' } + base.class_eval { alias_method_chain :process, :digest_authentication } + end + + def process_with_digest_authentication(request, response) + if authorized?(request) + process_without_digest_authentication(request, response) + #does not work. At this point response.body_sent is nil and + #response.body.string is set to the correct value + # -> it's not a stream issue + #The else close is never called after this point, yet the result is whatever I put in the else statement + # -> don't get it + else + reply_with(response, 401, "Incorrect. You have 20 seconds to comply.") + end + end + + def authorized?(request) + request.params["HTTP_AUTHORIZATION"] =~ /Digest.*uri=/ + end + end + handler.extend(m) +end + def new_mongrel_redirector(target_url, relative_path = false) target_url = "http://#{@host_and_port}#{target_url}" unless relative_path Mongrel::RedirectHandler.new(target_url) diff --git a/features/steps/remote_service_steps.rb b/features/steps/remote_service_steps.rb index df69e7e..999616c 100644 --- a/features/steps/remote_service_steps.rb +++ b/features/steps/remote_service_steps.rb @@ -33,6 +33,10 @@ Given /that service is protected by Basic Authentication/ do add_basic_authentication_to @handler end +Given /that service is protected by Digest Authentication/ do + add_digest_authentication_to @handler +end + Given /that service requires the username '(.*)' with the password '(.*)'/ do |username, password| @handler.username = username @handler.password = password @@ -54,4 +58,4 @@ end Then /I wait for the server to recover/ do timeout = @request_options[:timeout] || 0 sleep @server_response_time - timeout -end \ No newline at end of file +end diff --git a/lib/httparty.rb b/lib/httparty.rb index 4b0fc83..a5f9443 100644 --- a/lib/httparty.rb +++ b/lib/httparty.rb @@ -84,7 +84,6 @@ module HTTParty default_options[:digest_auth] = {:username => u, :password => p} end - # Allows setting default parameters to be appended to each request. # Great for api keys and such. # diff --git a/lib/httparty/request.rb b/lib/httparty/request.rb index 10bee28..c6cdb79 100644 --- a/lib/httparty/request.rb +++ b/lib/httparty/request.rb @@ -52,7 +52,6 @@ module HTTParty options[:parser] end - def perform validate setup_raw_request @@ -62,97 +61,97 @@ module HTTParty private - def http - http = Net::HTTP.new(uri.host, uri.port, options[:http_proxyaddr], options[:http_proxyport]) - http.use_ssl = ssl_implied? + def http + http = Net::HTTP.new(uri.host, uri.port, options[:http_proxyaddr], options[:http_proxyport]) + http.use_ssl = ssl_implied? - if options[:timeout] && options[:timeout].is_a?(Integer) - http.open_timeout = options[:timeout] - http.read_timeout = options[:timeout] - end - - if options[:pem] && http.use_ssl? - http.cert = OpenSSL::X509::Certificate.new(options[:pem]) - http.key = OpenSSL::PKey::RSA.new(options[:pem]) - http.verify_mode = OpenSSL::SSL::VERIFY_PEER - else - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - end - - if options[:debug_output] - http.set_debug_output(options[:debug_output]) - end - - http + if options[:timeout] && options[:timeout].is_a?(Integer) + http.open_timeout = options[:timeout] + http.read_timeout = options[:timeout] end - def ssl_implied? - uri.port == 443 || uri.instance_of?(URI::HTTPS) + if options[:pem] && http.use_ssl? + http.cert = OpenSSL::X509::Certificate.new(options[:pem]) + http.key = OpenSSL::PKey::RSA.new(options[:pem]) + http.verify_mode = OpenSSL::SSL::VERIFY_PEER + else + http.verify_mode = OpenSSL::SSL::VERIFY_NONE end - def body - options[:body].is_a?(Hash) ? options[:body].to_params : options[:body] + if options[:debug_output] + http.set_debug_output(options[:debug_output]) end - def credentials - options[:basic_auth] || options[:digest_auth] + http + end + + def ssl_implied? + uri.port == 443 || uri.instance_of?(URI::HTTPS) + end + + def body + options[:body].is_a?(Hash) ? options[:body].to_params : options[:body] + end + + def credentials + options[:basic_auth] || options[:digest_auth] + end + + def username + credentials[:username] + end + + def password + credentials[:password] + end + + def setup_raw_request + @raw_request = http_method.new(uri.request_uri) + @raw_request.body = body if body + @raw_request.initialize_http_header(options[:headers]) + @raw_request.basic_auth(username, password) if options[:basic_auth] + setup_digest_auth if options[:digest_auth] + end + + def setup_digest_auth + res = http.head(uri.request_uri) + if res['www-authenticate'] != nil && res['www-authenticate'].length > 0 + @raw_request.digest_auth(username, password, res) + end + end + + def perform_actual_request + http.request(@raw_request) + end + + def get_response + self.last_response = perform_actual_request + options[:format] ||= format_from_mimetype(last_response['content-type']) + end + + def query_string(uri) + query_string_parts = [] + query_string_parts << uri.query unless uri.query.nil? + + if options[:query].is_a?(Hash) + query_string_parts << options[:default_params].merge(options[:query]).to_params + else + query_string_parts << options[:default_params].to_params unless options[:default_params].empty? + query_string_parts << options[:query] unless options[:query].nil? end - def username - credentials[:username] - end + query_string_parts.size > 0 ? query_string_parts.join('&') : nil + end - def password - credentials[:password] - end - - def setup_raw_request - @raw_request = http_method.new(uri.request_uri) - @raw_request.body = body if body - @raw_request.initialize_http_header(options[:headers]) - @raw_request.basic_auth(username, password) if options[:basic_auth] - setup_digest_auth if options[:digest_auth] - end - - def setup_digest_auth - res = http.head(uri.request_uri) - if res['www-authenticate'] != nil && res['www-authenticate'].length > 0 - @raw_request.digest_auth(username, password, res) - end - end - - def perform_actual_request - http.request(@raw_request) - end - - def get_response - self.last_response = perform_actual_request - options[:format] ||= format_from_mimetype(last_response['content-type']) - end - - def query_string(uri) - query_string_parts = [] - query_string_parts << uri.query unless uri.query.nil? - - if options[:query].is_a?(Hash) - query_string_parts << options[:default_params].merge(options[:query]).to_params - else - query_string_parts << options[:default_params].to_params unless options[:default_params].empty? - query_string_parts << options[:query] unless options[:query].nil? - end - - query_string_parts.size > 0 ? query_string_parts.join('&') : nil - end - - # Raises exception Net::XXX (http error code) if an http error occured - def handle_response - case last_response - when Net::HTTPMultipleChoice, # 300 - Net::HTTPMovedPermanently, # 301 - Net::HTTPFound, # 302 - Net::HTTPSeeOther, # 303 - Net::HTTPUseProxy, # 305 - Net::HTTPTemporaryRedirect + # Raises exception Net::XXX (http error code) if an http error occured + def handle_response + case last_response + when Net::HTTPMultipleChoice, # 300 + Net::HTTPMovedPermanently, # 301 + Net::HTTPFound, # 302 + Net::HTTPSeeOther, # 303 + Net::HTTPUseProxy, # 305 + Net::HTTPTemporaryRedirect if last_response.key?('location') options[:limit] -= 1 self.path = last_response['location'] @@ -165,30 +164,30 @@ module HTTParty end else Response.new(parse_response(last_response.body), last_response.body, last_response.code, last_response.message, last_response.to_hash) - end end + end - def parse_response(body) - parser.call(body, format) - end + def parse_response(body) + parser.call(body, format) + end - def capture_cookies(response) - return unless response['Set-Cookie'] - cookies_hash = HTTParty::CookieHash.new() - cookies_hash.add_cookies(options[:headers]['Cookie']) if options[:headers] && options[:headers]['Cookie'] - cookies_hash.add_cookies(response['Set-Cookie']) - options[:headers] ||= {} - options[:headers]['Cookie'] = cookies_hash.to_cookie_string - end + def capture_cookies(response) + return unless response['Set-Cookie'] + cookies_hash = HTTParty::CookieHash.new() + cookies_hash.add_cookies(options[:headers]['Cookie']) if options[:headers] && options[:headers]['Cookie'] + cookies_hash.add_cookies(response['Set-Cookie']) + options[:headers] ||= {} + options[:headers]['Cookie'] = cookies_hash.to_cookie_string + end - # Uses the HTTP Content-Type header to determine the format of the - # response It compares the MIME type returned to the types stored in the - # SupportedFormats hash - def format_from_mimetype(mimetype) - if mimetype && parser.respond_to?(:format_from_mimetype) - parser.format_from_mimetype(mimetype) - end + # Uses the HTTP Content-Type header to determine the format of the + # response It compares the MIME type returned to the types stored in the + # SupportedFormats hash + def format_from_mimetype(mimetype) + if mimetype && parser.respond_to?(:format_from_mimetype) + parser.format_from_mimetype(mimetype) end + end def validate raise HTTParty::RedirectionTooDeep.new(last_response), 'HTTP redirects too deep' if options[:limit].to_i <= 0 @@ -200,9 +199,8 @@ module HTTParty raise ArgumentError, ':query must be hash if using HTTP Post' if post? && !options[:query].nil? && !options[:query].is_a?(Hash) end - def post? - Net::HTTP::Post == http_method - end + def post? + Net::HTTP::Post == http_method + end end end -