added authentication plugin with tests (basic is working, digest is not)

This commit is contained in:
HoneyryderChuck 2017-12-15 18:15:04 +02:00
parent eccea7c443
commit 68faf1c4a1
4 changed files with 258 additions and 2 deletions

View 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

View File

@ -12,7 +12,9 @@ class HTTP1Test < HTTPTest
include Headers
include ResponseBody
include IO
include Plugins::Authentication
private
def origin

View File

@ -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

View 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