mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-10-06 00:02:08 -04:00
added authentication plugin with tests (basic is working, digest is not)
This commit is contained in:
parent
eccea7c443
commit
68faf1c4a1
182
lib/httpx/plugins/authentication.rb
Normal file
182
lib/httpx/plugins/authentication.rb
Normal file
@ -0,0 +1,182 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
module Plugins
|
||||
module Authentication
|
||||
DigestError = Class.new(Error)
|
||||
|
||||
def self.load_dependencies(*)
|
||||
require "base64"
|
||||
require "securerandom"
|
||||
require "digest"
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def authentication(token)
|
||||
headers("authorization" => token)
|
||||
end
|
||||
|
||||
def basic_authentication(user, password)
|
||||
authentication("Basic #{Base64.strict_encode64("#{user}:#{password}")}")
|
||||
end
|
||||
alias :basic_auth :basic_authentication
|
||||
|
||||
def digest_authentication(user, password)
|
||||
@_digest_auth_user = user
|
||||
@_digest_auth_pass = password
|
||||
@_digest = Digest.new
|
||||
self
|
||||
end
|
||||
alias :digest_auth :digest_authentication
|
||||
|
||||
def request(*args, **options)
|
||||
return super unless @_digest
|
||||
begin
|
||||
#keep_open = @keep_open
|
||||
#@keep_open = true
|
||||
|
||||
requests = __build_reqs(*args, **options)
|
||||
responses = __send_reqs(*requests)
|
||||
|
||||
failed_requests = []
|
||||
failed_responses_ids = responses.each_with_index.map do |response, index|
|
||||
next unless response.status == 401
|
||||
request = requests[index]
|
||||
|
||||
token = @_digest.generate_header(@_digest_auth_user,
|
||||
@_digest_auth_pass,
|
||||
request,
|
||||
response)
|
||||
|
||||
request.headers["authorization"] = "Digest #{token}"
|
||||
request.transition(:idle)
|
||||
|
||||
failed_requests << request
|
||||
|
||||
index
|
||||
end.compact
|
||||
|
||||
return responses if failed_requests.empty?
|
||||
|
||||
repeated_responses = __send_reqs(*failed_requests)
|
||||
repeated_responses.each_with_index do |rep, index|
|
||||
responses[index] = rep
|
||||
end
|
||||
return responses.first if responses.size == 1
|
||||
responses
|
||||
ensure
|
||||
#@keep_open = keep_open
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Digest
|
||||
def initialize
|
||||
@nonce = 0
|
||||
end
|
||||
|
||||
def generate_header(user, password, request, response, iis = false)
|
||||
method = request.verb.to_s.upcase
|
||||
www = response.headers["www-authenticate"]
|
||||
|
||||
# TODO: assert if auth-type is Digest
|
||||
auth_info = www[/^(\w+) (.*)/, 2]
|
||||
|
||||
|
||||
params = Hash[ auth_info.scan(/(\w+)="(.*?)"/) ]
|
||||
|
||||
nonce = params["nonce"]
|
||||
nc = next_nonce
|
||||
|
||||
# verify qop
|
||||
qop = params["qop"]
|
||||
|
||||
if params["algorithm"] =~ /(.*?)(-sess)?$/
|
||||
algorithm = case $1
|
||||
when "MD5" then ::Digest::MD5
|
||||
when "SHA1" then ::Digest::SHA1
|
||||
when "SHA2" then ::Digest::SHA2
|
||||
when "SHA256" then ::Digest::SHA256
|
||||
when "SHA384" then ::Digest::SHA384
|
||||
when "SHA512" then ::Digest::SHA512
|
||||
when "RMD160" then ::Digest::RMD160
|
||||
else raise DigestError, "unknown algorithm \"#{$1}\""
|
||||
end
|
||||
sess = $2
|
||||
else
|
||||
algorithm = ::Digest::MD5
|
||||
end
|
||||
|
||||
if qop or sess
|
||||
cnonce = make_cnonce
|
||||
nc = "%08x" % nc
|
||||
end
|
||||
|
||||
a1 = if sess then
|
||||
[ algorithm.hexdigest("#{user}:#{params["realm"]}:#{password}"),
|
||||
nonce,
|
||||
cnonce,
|
||||
].join ":"
|
||||
else
|
||||
"#{user}:#{params["realm"]}:#{password}"
|
||||
end
|
||||
|
||||
ha1 = algorithm.hexdigest(a1)
|
||||
ha2 = algorithm.hexdigest("#{method}:#{request.path}")
|
||||
|
||||
request_digest = [ha1, nonce]
|
||||
request_digest.push(nc, cnonce, qop) if qop
|
||||
request_digest << ha2
|
||||
request_digest = request_digest.join(":")
|
||||
|
||||
header = [
|
||||
"username=\"#{user}\"",
|
||||
"response=\"#{algorithm.hexdigest(request_digest)}\"",
|
||||
"uri=\"#{request.path}\"",
|
||||
"nonce=\"#{nonce}\""
|
||||
]
|
||||
header << "realm=\"#{params["realm"]}\"" if params.key?("realm")
|
||||
header << "opaque=\"#{params["opaque"]}\"" if params.key?("opaque")
|
||||
header << "algorithm=#{params["algorithm"]}" if params.key?("algorithm")
|
||||
header << "cnonce=#{cnonce}" if cnonce
|
||||
header << "nc=#{nc}"
|
||||
header << "qop=#{qop}" if qop
|
||||
#
|
||||
# if qop.nil? then
|
||||
# elsif iis then
|
||||
# "qop=\"#{qop}\""
|
||||
# else
|
||||
# "qop=#{qop}"
|
||||
# end,
|
||||
# if qop then
|
||||
# [
|
||||
# "nc=#{"%08x" % nonce}",
|
||||
# "cnonce=\"#{cnonce}\"",
|
||||
# ]
|
||||
# end,
|
||||
# if params.key?("opaque") then
|
||||
# "opaque=\"#{params["opaque"]}\""
|
||||
# end
|
||||
# ].compact
|
||||
|
||||
header.join ", "
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def make_cnonce
|
||||
::Digest::MD5.hexdigest [
|
||||
Time.now.to_i,
|
||||
Process.pid,
|
||||
SecureRandom.random_number(2**32),
|
||||
].join ":"
|
||||
end
|
||||
|
||||
def next_nonce
|
||||
@nonce += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
register_plugin :authentication, Authentication
|
||||
end
|
||||
end
|
@ -12,7 +12,9 @@ class HTTP1Test < HTTPTest
|
||||
include Headers
|
||||
include ResponseBody
|
||||
include IO
|
||||
|
||||
|
||||
include Plugins::Authentication
|
||||
|
||||
private
|
||||
|
||||
def origin
|
||||
|
@ -12,7 +12,11 @@ module ResponseHelpers
|
||||
def verify_#{meth}(#{meth}s, key, expect)
|
||||
assert #{meth}s.key?(key), "#{meth}s don't contain the given key (" + key + ")"
|
||||
value = #{meth}s[key]
|
||||
assert value.start_with?(expect), "#{meth} assertion failed: " + key + "=" + value + " (expected: " + expect + ")"
|
||||
if value.respond_to?(:start_with?)
|
||||
assert value.start_with?(expect), "#{meth} assertion failed: " + key + "=" + value + " (expected: " + expect + ")"
|
||||
else
|
||||
assert value == expect, "#{meth} assertion failed: " + key + "=" + value.to_s + " (expected: " + expect.to_s + ")"
|
||||
end
|
||||
end
|
||||
DEFINE
|
||||
end
|
||||
|
68
test/support/requests/plugins/authentication.rb
Normal file
68
test/support/requests/plugins/authentication.rb
Normal file
@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "base64"
|
||||
|
||||
module Requests
|
||||
module Plugins
|
||||
module Authentication
|
||||
|
||||
def test_plugin_authentication_no_auth
|
||||
|
||||
end
|
||||
|
||||
def test_plugin_authentication_auth
|
||||
|
||||
end
|
||||
|
||||
def test_plugin_authentication_no_basic_auth
|
||||
response = HTTPX.get(basic_auth_uri)
|
||||
verify_status(response.status, 401)
|
||||
verify_header(response.headers, "www-authenticate", "Basic realm=\"Fake Realm\"")
|
||||
end
|
||||
|
||||
def test_plugin_authentication_basic_auth
|
||||
client = HTTPX.plugin(:authentication)
|
||||
response = client.basic_authentication(user, pass).get(basic_auth_uri)
|
||||
verify_status(response.status, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body, "authenticated", true)
|
||||
verify_header(body, "user", user)
|
||||
|
||||
invalid_response = client.basic_authentication(user, "fake").get(basic_auth_uri)
|
||||
verify_status(invalid_response.status, 401)
|
||||
end
|
||||
|
||||
def test_plugin_authentication_digest_auth
|
||||
client = HTTPX.plugin(:authentication)
|
||||
response = client.digest_authentication(user, pass).get(digest_auth_uri)
|
||||
verify_status(response.status, 200)
|
||||
body = json_body(response)
|
||||
verify_header(body, "authenticated", true)
|
||||
verify_header(body, "user", user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def basic_auth_uri
|
||||
build_uri("/basic-auth/#{user}/#{pass}")
|
||||
end
|
||||
|
||||
def digest_auth_uri(qop="auth")
|
||||
build_uri("/digest-auth/#{qop}/#{user}/#{pass}")
|
||||
end
|
||||
|
||||
def user
|
||||
"user"
|
||||
end
|
||||
|
||||
def pass
|
||||
"pass"
|
||||
end
|
||||
|
||||
def basic_auth_token
|
||||
Base64.strict_encode64("#{user}:#{pass}")
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user