lib/httparty/net_digest_auth.rb



# frozen_string_literal: true

require 'digest/md5'
require 'net/http'

module Net
  module HTTPHeader
    def digest_auth(username, password, response)
      authenticator = DigestAuthenticator.new(
        username,
        password,
        @method,
        @path,
        response
      )

      authenticator.authorization_header.each do |v|
        add_field('Authorization', v)
      end

      authenticator.cookie_header.each do |v|
        add_field('Cookie', v)
      end
    end

    class DigestAuthenticator
      def initialize(username, password, method, path, response_header)
        @username = username
        @password = password
        @method   = method
        @path     = path
        @response = parse(response_header)
        @cookies  = parse_cookies(response_header)
      end

      def authorization_header
        @cnonce = md5(random)
        header = [
          %(Digest username="#{@username}"),
          %(realm="#{@response['realm']}"),
          %(nonce="#{@response['nonce']}"),
          %(uri="#{@path}"),
          %(response="#{request_digest}")
        ]

        header << %(algorithm="#{@response['algorithm']}") if algorithm_present?

        if qop_present?
          header << %(cnonce="#{@cnonce}")
          header << %(qop="#{@response['qop']}")
          header << 'nc=00000001'
        end

        header << %(opaque="#{@response['opaque']}") if opaque_present?
        header
      end

      def cookie_header
        @cookies
      end

      private

      def parse(response_header)
        header = response_header['www-authenticate']

        header = header.gsub(/qop=(auth(?:-int)?)/, 'qop="\\1"')

        header =~ /Digest (.*)/
        params = {}
        if $1
          non_quoted = $1.gsub(/(\w+)="(.*?)"/) { params[$1] = $2 }
          non_quoted.gsub(/(\w+)=([^,]*)/) { params[$1] = $2 }
        end
        params
      end

      def parse_cookies(response_header)
        return [] unless response_header['Set-Cookie']

        cookies = response_header['Set-Cookie'].split('; ')

        cookies.reduce([]) do |ret, cookie|
          ret << cookie
          ret
        end

        cookies
      end

      def opaque_present?
        @response.key?('opaque') && !@response['opaque'].empty?
      end

      def qop_present?
        @response.key?('qop') && !@response['qop'].empty?
      end

      def random
        format '%x', (Time.now.to_i + rand(65535))
      end

      def request_digest
        a = [md5(a1), @response['nonce'], md5(a2)]
        a.insert(2, '00000001', @cnonce, @response['qop']) if qop_present?
        md5(a.join(':'))
      end

      def md5(str)
        Digest::MD5.hexdigest(str)
      end

      def algorithm_present?
        @response.key?('algorithm') && !@response['algorithm'].empty?
      end

      def use_md5_sess?
        algorithm_present? && @response['algorithm'] == 'MD5-sess'
      end

      def a1
        a1_user_realm_pwd =  [@username, @response['realm'], @password].join(':')
        if use_md5_sess?
          [ md5(a1_user_realm_pwd), @response['nonce'], @cnonce ].join(':')
        else
          a1_user_realm_pwd
        end
      end

      def a2
        [@method, @path].join(':')
      end
    end
  end
end