#region License
/*
 * AuthenticationResponse.cs
 *
 * The ParseBasicCredentials method is derived from HttpListenerContext.cs
 * (System.Net) of Mono (http://www.mono-project.com).
 *
 * The MIT License
 *
 * Copyright (c) 2005 Novell, Inc. (http://www.novell.com)
 * Copyright (c) 2013-2024 sta.blockhead
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
#endregion

using System;
using System.Collections.Specialized;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;

namespace WebSocketSharp.Net
{
  internal class AuthenticationResponse
  {
    #region Private Fields

    private uint                  _nonceCount;
    private NameValueCollection   _parameters;
    private AuthenticationSchemes _scheme;

    #endregion

    #region Private Constructors

    private AuthenticationResponse (
      AuthenticationSchemes scheme,
      NameValueCollection parameters
    )
    {
      _scheme = scheme;
      _parameters = parameters;
    }

    #endregion

    #region Internal Constructors

    internal AuthenticationResponse (NetworkCredential credentials)
      : this (
          AuthenticationSchemes.Basic,
          new NameValueCollection (),
          credentials,
          0
        )
    {
    }

    internal AuthenticationResponse (
      AuthenticationChallenge challenge,
      NetworkCredential credentials,
      uint nonceCount
    )
      : this (challenge.Scheme, challenge.Parameters, credentials, nonceCount)
    {
    }

    internal AuthenticationResponse (
      AuthenticationSchemes scheme,
      NameValueCollection parameters,
      NetworkCredential credentials,
      uint nonceCount
    )
      : this (scheme, parameters)
    {
      _parameters["username"] = credentials.Username;
      _parameters["password"] = credentials.Password;
      _parameters["uri"] = credentials.Domain;
      _nonceCount = nonceCount;

      if (scheme == AuthenticationSchemes.Digest)
        initAsDigest ();
    }

    #endregion

    #region Internal Properties

    internal uint NonceCount {
      get {
        return _nonceCount < UInt32.MaxValue ? _nonceCount : 0;
      }
    }

    internal NameValueCollection Parameters {
      get {
        return _parameters;
      }
    }

    #endregion

    #region Public Properties

    public string Algorithm {
      get {
        return _parameters["algorithm"];
      }
    }

    public string Cnonce {
      get {
        return _parameters["cnonce"];
      }
    }

    public string Nc {
      get {
        return _parameters["nc"];
      }
    }

    public string Nonce {
      get {
        return _parameters["nonce"];
      }
    }

    public string Opaque {
      get {
        return _parameters["opaque"];
      }
    }

    public string Password {
      get {
        return _parameters["password"];
      }
    }

    public string Qop {
      get {
        return _parameters["qop"];
      }
    }

    public string Realm {
      get {
        return _parameters["realm"];
      }
    }

    public string Response {
      get {
        return _parameters["response"];
      }
    }

    public AuthenticationSchemes Scheme {
      get {
        return _scheme;
      }
    }

    public string Uri {
      get {
        return _parameters["uri"];
      }
    }

    public string UserName {
      get {
        return _parameters["username"];
      }
    }

    #endregion

    #region Private Methods

    private static string createA1 (
      string username,
      string password,
      string realm
    )
    {
      return String.Format ("{0}:{1}:{2}", username, realm, password);
    }

    private static string createA1 (
      string username,
      string password,
      string realm,
      string nonce,
      string cnonce
    )
    {
      var a1 = createA1 (username, password, realm);

      return String.Format ("{0}:{1}:{2}", hash (a1), nonce, cnonce);
    }

    private static string createA2 (string method, string uri)
    {
      return String.Format ("{0}:{1}", method, uri);
    }

    private static string createA2 (string method, string uri, string entity)
    {
      return String.Format ("{0}:{1}:{2}", method, uri, hash (entity));
    }

    private static string hash (string value)
    {
      var buff = new StringBuilder (64);

      var md5 = MD5.Create ();
      var bytes = Encoding.UTF8.GetBytes (value);
      var res = md5.ComputeHash (bytes);

      foreach (var b in res)
        buff.Append (b.ToString ("x2"));

      return buff.ToString ();
    }

    private void initAsDigest ()
    {
      var qops = _parameters["qop"];

      if (qops != null) {
        var hasAuth = qops.Split (',').Contains (
                        qop => qop.Trim ().ToLower () == "auth"
                      );

        if (hasAuth) {
          _parameters["qop"] = "auth";
          _parameters["cnonce"] = AuthenticationChallenge.CreateNonceValue ();
          _parameters["nc"] = String.Format ("{0:x8}", ++_nonceCount);
        }
        else {
          _parameters["qop"] = null;
        }
      }

      _parameters["method"] = "GET";
      _parameters["response"] = CreateRequestDigest (_parameters);
    }

    #endregion

    #region Internal Methods

    internal static string CreateRequestDigest (NameValueCollection parameters)
    {
      var uname = parameters["username"];
      var passwd = parameters["password"];
      var realm = parameters["realm"];
      var nonce = parameters["nonce"];
      var uri = parameters["uri"];
      var algo = parameters["algorithm"];
      var qop = parameters["qop"];
      var cnonce = parameters["cnonce"];
      var nc = parameters["nc"];
      var method = parameters["method"];

      var a1 = algo != null && algo.ToLower () == "md5-sess"
               ? createA1 (uname, passwd, realm, nonce, cnonce)
               : createA1 (uname, passwd, realm);

      var a2 = qop != null && qop.ToLower () == "auth-int"
               ? createA2 (method, uri, parameters["entity"])
               : createA2 (method, uri);

      var secret = hash (a1);
      var data = qop != null
                 ? String.Format (
                     "{0}:{1}:{2}:{3}:{4}",
                     nonce,
                     nc,
                     cnonce,
                     qop,
                     hash (a2)
                   )
                 : String.Format ("{0}:{1}", nonce, hash (a2));

      var keyed = String.Format ("{0}:{1}", secret, data);

      return hash (keyed);
    }

    internal static AuthenticationResponse Parse (string value)
    {
      try {
        var cred = value.Split (new[] { ' ' }, 2);

        if (cred.Length != 2)
          return null;

        var schm = cred[0].ToLower ();

        if (schm == "basic") {
          var parameters = ParseBasicCredentials (cred[1]);

          return new AuthenticationResponse (
                   AuthenticationSchemes.Basic,
                   parameters
                 );
        }

        if (schm == "digest") {
          var parameters = AuthenticationChallenge.ParseParameters (cred[1]);

          return new AuthenticationResponse (
                   AuthenticationSchemes.Digest,
                   parameters
                 );
        }

        return null;
      }
      catch {
        return null;
      }
    }

    internal static NameValueCollection ParseBasicCredentials (string value)
    {
      var ret = new NameValueCollection ();

      // Decode the basic-credentials (a Base64 encoded string).

      var bytes = Convert.FromBase64String (value);
      var userPass = Encoding.UTF8.GetString (bytes);

      // The format is [<domain>\]<username>:<password>.

      var idx = userPass.IndexOf (':');
      var uname = userPass.Substring (0, idx);
      var passwd = idx < userPass.Length - 1
                   ? userPass.Substring (idx + 1)
                   : String.Empty;

      // Check if <domain> exists.

      idx = uname.IndexOf ('\\');

      if (idx > -1)
        uname = uname.Substring (idx + 1);

      ret["username"] = uname;
      ret["password"] = passwd;

      return ret;
    }

    internal string ToBasicString ()
    {
      var uname = _parameters["username"];
      var passwd = _parameters["password"];
      var userPass = String.Format ("{0}:{1}", uname, passwd);

      var bytes = Encoding.UTF8.GetBytes (userPass);
      var cred = Convert.ToBase64String (bytes);

      return "Basic " + cred;
    }

    internal string ToDigestString ()
    {
      var buff = new StringBuilder (256);

      var uname = _parameters["username"];
      var realm = _parameters["realm"];
      var nonce = _parameters["nonce"];
      var uri = _parameters["uri"];
      var res = _parameters["response"];

      buff.AppendFormat (
        "Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\", response=\"{4}\"",
        uname,
        realm,
        nonce,
        uri,
        res
      );

      var opaque = _parameters["opaque"];

      if (opaque != null)
        buff.AppendFormat (", opaque=\"{0}\"", opaque);

      var algo = _parameters["algorithm"];

      if (algo != null)
        buff.AppendFormat (", algorithm={0}", algo);

      var qop = _parameters["qop"];

      if (qop != null) {
        var cnonce = _parameters["cnonce"];
        var nc = _parameters["nc"];

        buff.AppendFormat (
          ", qop={0}, cnonce=\"{1}\", nc={2}",
          qop,
          cnonce,
          nc
        );
      }

      return buff.ToString ();
    }

    #endregion

    #region Public Methods

    public IIdentity ToIdentity ()
    {
      if (_scheme == AuthenticationSchemes.Basic) {
        var uname = _parameters["username"];
        var passwd = _parameters["password"];

        return new HttpBasicIdentity (uname, passwd);
      }

      if (_scheme == AuthenticationSchemes.Digest)
        return new HttpDigestIdentity (_parameters);

      return null;
    }

    public override string ToString ()
    {
      if (_scheme == AuthenticationSchemes.Basic)
        return ToBasicString ();

      if (_scheme == AuthenticationSchemes.Digest)
        return ToDigestString ();

      return String.Empty;
    }

    #endregion
  }
}
