// ----------------------------------------------------------------------------
//
//  Copyright (c) Microsoft Corporation.  All rights reserved.
//
// ----------------------------------------------------------------------------

using System;
using System.Diagnostics;
using System.Text;
using Microsoft.Singularity.Crypto;

public class HttpAuthenticator
{
    private HttpRequest fRequest;
    private string fUsername, fPassword;
    private MD5 fMD5;

    public HttpAuthenticator(HttpRequest request, string username, string password)
    {
        fRequest = request;
        fUsername = username;
        fPassword = password;
        fMD5 = new MD5();
    }

    public HttpResponse GetResponse()
    {
        // First try the original request
        HttpResponse response = fRequest.GetResponse();

        if (response == null) {
            throw new Exception("Initial HTTP request failed");
        }

        int status = response.StatusCode;

        if (status != 401) {
            // No authentication was actually required
            return response;
        }
        else {
            // Authentication is required. Go spelunking for the crypto challenge.
            string challenge = response.GetHeader("WWW-Authenticate");

            if (challenge == null) {
                throw new Exception("401 without a WWW-Authenticate header");
            }

            if (!challenge.StartsWith("Digest")) {
                throw new Exception("Invalid WWW-Authenticate header");
            }

            string realm = GetTaggedQuotedString(challenge, "realm");
            string nonce = GetTaggedQuotedString(challenge, "nonce");
            string opaque = GetTaggedQuotedString(challenge, "opaque");
            string algorithm = GetTaggedQuotedString(challenge, "algorithm");

            if ((algorithm != null) && (!algorithm.ToLower().Equals("md5"))) {
                throw new Exception("Unknown authentication scheme");
            }

            if ((realm == null) || (nonce == null)) {
                throw new Exception("Missing essential elements of WWW-Challenge");
            }

            // Compute the crypto response
            string authResponse = "Digest username=\"" + fUsername +
                "\",realm=\"" + realm +
                "\",nonce=\"" + nonce +
                "\",uri=\"" + fRequest.Resource +
                "\",response=\"";

            //
            // The crypto response is specified (RFC 2069) as:
            //
            // MD5( concat(A1, ":", B))
            // where A1 is:  MD5( concat( username, ":", realm, ":", password) )
            // and B is:     concat( nonce, ":", A2)
            // and A2 is:    MD5( concat (method, ":", digest-uri) )
            // Where MD5(X) is a string of hex digits representing the MD5 digest of X.
            // Got all that?
            //

            // Form A1 = MD5(username : realm : password)
            string a1 = MD5(fUsername + ":" + realm + ":" + fPassword);

            // Form A2 = md5(method : digest-uri)
            string a2 = MD5(fRequest.Method + ":" + fRequest.Resource);

            // Form B = nonce : A2
            string b = nonce + ":" + a2;

            // Form the unhashed final answer as MD5(A1 : B)
            authResponse += MD5(a1 + ":" + b) + "\"";

            // Echo the server's opaque string if present.
            if (opaque != null) {
                authResponse += ",opaque=\"" + opaque + "\"";
            }

            fRequest.AddHeader("Authorization", authResponse);

            // Rerun the request
            response = fRequest.GetResponse();

            if ((response != null) && (response.StatusCode == 401)) {
                // Authentication failed; credentials must be bad
                throw new Exception("Bad credentials");
            }

            // Looks like it worked after authentication
            return response;
        }
    }

    private string GetTaggedQuotedString(string! baseString, string tagName)
    {
        string tagPreamble = tagName + "=\"";
        int tagStart = baseString.IndexOf(tagPreamble);

        if (tagStart == -1) {
            return null;
        }

        int tagEnd = tagStart + tagPreamble.Length;
        int quoteEnd = baseString.IndexOf('"', tagEnd);

        if (quoteEnd == -1) {
            throw new Exception("Malformed tagged+quoted string");
        }

        return baseString.Substring(tagEnd, quoteEnd - tagEnd);
    }

    private string MD5(string! hashValue)
    {
        byte[] vals = Encoding.ASCII.GetBytes(hashValue);
        byte[] result = (!)fMD5.Hash(vals);
        return ToHexString(result);
    }

    private string ToHexString(byte[]! vals)
    {
        string retval = String.Empty;

        for (int i = 0; i < vals.Length; ++i) {
            retval += vals[i].ToString("x2");
        }

        return retval;
    }
}
