Digest.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. using System;
  2. using System.Collections.Generic;
  3. namespace BestHTTP.Authentication
  4. {
  5. using BestHTTP.Extensions;
  6. using BestHTTP.PlatformSupport.Memory;
  7. using System.Text;
  8. /// <summary>
  9. /// Internal class that stores all information that received from a server in a WWW-Authenticate and need to construct a valid Authorization header. Based on rfc 2617 (http://tools.ietf.org/html/rfc2617).
  10. /// Used only internally by the plugin.
  11. /// </summary>
  12. public sealed class Digest
  13. {
  14. #region Public Properties
  15. /// <summary>
  16. /// The Uri that this Digest is bound to.
  17. /// </summary>
  18. public Uri Uri { get; private set; }
  19. public AuthenticationTypes Type { get; private set; }
  20. /// <summary>
  21. /// A string to be displayed to users so they know which username and password to use.
  22. /// This string should contain at least the name of the host performing the authentication and might additionally indicate the collection of users who might have access.
  23. /// </summary>
  24. public string Realm { get; private set; }
  25. /// <summary>
  26. /// A flag, indicating that the previous request from the client was rejected because the nonce value was stale.
  27. /// If stale is TRUE (case-insensitive), the client may wish to simply retry the request with a new encrypted response, without the user for a new username and password.
  28. /// The server should only set stale to TRUE if it receives a request for which the nonce is invalid but with a valid digest for that nonce
  29. /// (indicating that the client knows the correct username/password).
  30. /// If stale is FALSE, or anything other than TRUE, or the stale directive is not present, the username and/or password are invalid, and new values must be obtained.
  31. /// </summary>
  32. public bool Stale { get; private set; }
  33. #endregion
  34. #region Private Properties
  35. /// <summary>
  36. /// A server-specified data string which should be uniquely generated each time a 401 response is made.
  37. /// Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
  38. /// </summary>
  39. private string Nonce { get; set; }
  40. /// <summary>
  41. /// A string of data, specified by the server, which should be returned by the client unchanged in the Authorization header of subsequent requests with URIs in the same protection space.
  42. /// It is recommended that this string be base64 or data.
  43. /// </summary>
  44. private string Opaque { get; set; }
  45. /// <summary>
  46. /// A string indicating a pair of algorithms used to produce the digest and a checksum. If this is not present it is assumed to be "MD5".
  47. /// If the algorithm is not understood, the challenge should be ignored (and a different one used, if there is more than one).
  48. /// </summary>
  49. private string Algorithm { get; set; }
  50. /// <summary>
  51. /// List of URIs, as specified in RFC XURI, that define the protection space.
  52. /// If a URI is an abs_path, it is relative to the canonical root URL (see section 1.2 above) of the server being accessed.
  53. /// An absoluteURI in this list may refer to a different server than the one being accessed.
  54. /// The client can use this list to determine the set of URIs for which the same authentication information may be sent:
  55. /// any URI that has a URI in this list as a prefix (after both have been made absolute) may be assumed to be in the same protection space.
  56. /// If this directive is omitted or its value is empty, the client should assume that the protection space consists of all URIs on the responding server.
  57. /// </summary>
  58. public List<string> ProtectedUris { get; private set; }
  59. /// <summary>
  60. /// If present, it is a quoted string of one or more tokens indicating the "quality of protection" values supported by the server.
  61. /// The value "auth" indicates authentication. The value "auth-int" indicates authentication with integrity protection.
  62. /// </summary>
  63. private string QualityOfProtections { get; set; }
  64. /// <summary>
  65. /// his MUST be specified if a qop directive is sent (see above), and MUST NOT be specified if the server did not send a qop directive in the WWW-Authenticate header field.
  66. /// The nc-value is the hexadecimal count of the number of requests (including the current request) that the client has sent with the nonce value in this request.
  67. /// </summary>
  68. private int NonceCount { get; set; }
  69. /// <summary>
  70. /// Used to store the last HA1 that can be used in the next header generation when Algorithm is set to "md5-sess".
  71. /// </summary>
  72. private string HA1Sess { get; set; }
  73. #endregion
  74. internal Digest(Uri uri)
  75. {
  76. this.Uri = uri;
  77. this.Algorithm = "md5";
  78. }
  79. /// <summary>
  80. /// Parses a WWW-Authenticate header's value to retrive all information.
  81. /// </summary>
  82. public void ParseChallange(string header)
  83. {
  84. // Reset some values to its defaults.
  85. this.Type = AuthenticationTypes.Unknown;
  86. this.Stale = false;
  87. this.Opaque = null;
  88. this.HA1Sess = null;
  89. this.NonceCount = 0;
  90. this.QualityOfProtections = null;
  91. if (this.ProtectedUris != null)
  92. this.ProtectedUris.Clear();
  93. // Parse the header
  94. WWWAuthenticateHeaderParser qpl = new WWWAuthenticateHeaderParser(header);
  95. // Then process
  96. foreach (var qp in qpl.Values)
  97. switch (qp.Key)
  98. {
  99. case "basic": this.Type = AuthenticationTypes.Basic; break;
  100. case "digest": this.Type = AuthenticationTypes.Digest; break;
  101. case "realm": this.Realm = qp.Value; break;
  102. case "domain":
  103. {
  104. if (string.IsNullOrEmpty(qp.Value) || qp.Value.Length == 0)
  105. break;
  106. if (this.ProtectedUris == null)
  107. this.ProtectedUris = new List<string>();
  108. int idx = 0;
  109. string val = qp.Value.Read(ref idx, ' ');
  110. do
  111. {
  112. this.ProtectedUris.Add(val);
  113. val = qp.Value.Read(ref idx, ' ');
  114. } while (idx < qp.Value.Length);
  115. break;
  116. }
  117. case "nonce": this.Nonce = qp.Value; break;
  118. case "qop": this.QualityOfProtections = qp.Value; break;
  119. case "stale": this.Stale = bool.Parse(qp.Value); break;
  120. case "opaque": this.Opaque = qp.Value; break;
  121. case "algorithm": this.Algorithm = qp.Value; break;
  122. }
  123. }
  124. /// <summary>
  125. /// Generates a string that can be set to an Authorization header.
  126. /// </summary>
  127. public string GenerateResponseHeader(HTTPRequest request, Credentials credentials, bool isProxy = false)
  128. {
  129. try
  130. {
  131. switch (Type)
  132. {
  133. case AuthenticationTypes.Basic:
  134. return string.Concat("Basic ", Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format("{0}:{1}", credentials.UserName, credentials.Password))));
  135. case AuthenticationTypes.Digest:
  136. {
  137. NonceCount++;
  138. string HA1 = string.Empty;
  139. // The cnonce-value is an opaque quoted string value provided by the client and used by both client and server to avoid chosen plaintext attacks, to provide mutual
  140. // authentication, and to provide some message integrity protection.
  141. string cnonce = new System.Random(request.GetHashCode()).Next(int.MinValue, int.MaxValue).ToString("X8");
  142. string ncvalue = NonceCount.ToString("X8");
  143. switch (Algorithm.TrimAndLower())
  144. {
  145. case "md5":
  146. HA1 = string.Format("{0}:{1}:{2}", credentials.UserName, Realm, credentials.Password).CalculateMD5Hash();
  147. break;
  148. case "md5-sess":
  149. if (string.IsNullOrEmpty(this.HA1Sess))
  150. this.HA1Sess = string.Format("{0}:{1}:{2}:{3}:{4}", credentials.UserName, Realm, credentials.Password, Nonce, ncvalue).CalculateMD5Hash();
  151. HA1 = this.HA1Sess;
  152. break;
  153. default: //throw new NotSupportedException("Not supported hash algorithm found in Web Authentication: " + Algorithm);
  154. return string.Empty;
  155. }
  156. // A string of 32 hex digits, which proves that the user knows a password. Set according to the qop value.
  157. string response = string.Empty;
  158. // The server sent QoP-value can be a list of supported methodes(if sent at all - in this case it's null).
  159. // The rfc is not specify that this is a space or comma separeted list. So it can be "auth, auth-int" or "auth auth-int".
  160. // We will first check the longer value("auth-int") then the short one ("auth"). If one matches we will reset the qop to the exact value.
  161. string qop = this.QualityOfProtections != null ? this.QualityOfProtections.TrimAndLower() : null;
  162. // When we authenticate with a proxy and we want to tunnel the request, we have to use the CONNECT method instead of the
  163. // request's, as the proxy will not know about the request itself.
  164. string method = isProxy ? "CONNECT" : request.MethodType.ToString().ToUpper();
  165. // When we authenticate with a proxy and we want to tunnel the request, the uri must match what we are sending in the CONNECT request's
  166. // Host header.
  167. string uri = isProxy ? request.CurrentUri.Host + ":" + request.CurrentUri.Port : request.CurrentUri.GetRequestPathAndQueryURL();
  168. if (qop == null)
  169. {
  170. string HA2 = string.Concat(request.MethodType.ToString().ToUpper(), ":", request.CurrentUri.GetRequestPathAndQueryURL()).CalculateMD5Hash();
  171. response = string.Format("{0}:{1}:{2}", HA1, Nonce, HA2).CalculateMD5Hash();
  172. }
  173. else if (qop.Contains("auth-int"))
  174. {
  175. qop = "auth-int";
  176. byte[] entityBody = request.GetEntityBody();
  177. if (entityBody == null)
  178. entityBody = BufferPool.NoData; //string.Empty.GetASCIIBytes();
  179. string HA2 = string.Format("{0}:{1}:{2}", method, uri, entityBody.CalculateMD5Hash()).CalculateMD5Hash();
  180. response = string.Format("{0}:{1}:{2}:{3}:{4}:{5}", HA1, Nonce, ncvalue, cnonce, qop, HA2).CalculateMD5Hash();
  181. }
  182. else if (qop.Contains("auth"))
  183. {
  184. qop = "auth";
  185. string HA2 = string.Concat(method, ":", uri).CalculateMD5Hash();
  186. response = string.Format("{0}:{1}:{2}:{3}:{4}:{5}", HA1, Nonce, ncvalue, cnonce, qop, HA2).CalculateMD5Hash();
  187. }
  188. else //throw new NotSupportedException("Unrecognized Quality of Protection value found: " + this.QualityOfProtections);
  189. return string.Empty;
  190. string result = string.Format("Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\", cnonce=\"{4}\", response=\"{5}\"",
  191. credentials.UserName, Realm, Nonce, uri, cnonce, response);
  192. if (qop != null)
  193. result += String.Concat(", qop=\"", qop, "\", nc=", ncvalue);
  194. if (!string.IsNullOrEmpty(Opaque))
  195. result = String.Concat(result, ", opaque=\"", Opaque, "\"");
  196. return result;
  197. }// end of case "digest":
  198. default:
  199. break;
  200. }
  201. }
  202. catch
  203. {
  204. }
  205. return string.Empty;
  206. }
  207. public bool IsUriProtected(Uri uri)
  208. {
  209. // http://tools.ietf.org/html/rfc2617#section-3.2.1
  210. // An absoluteURI in this list may refer to
  211. // a different server than the one being accessed. The client can use
  212. // this list to determine the set of URIs for which the same
  213. // authentication information may be sent: any URI that has a URI in
  214. // this list as a prefix (after both have been made absolute) may be
  215. // assumed to be in the same protection space. If this directive is
  216. // omitted or its value is empty, the client should assume that the
  217. // protection space consists of all URIs on the responding server.
  218. if (string.CompareOrdinal(uri.Host, this.Uri.Host) != 0)
  219. return false;
  220. string uriStr = uri.ToString();
  221. if (ProtectedUris != null && ProtectedUris.Count > 0)
  222. for (int i = 0; i < ProtectedUris.Count; ++i)
  223. if (uriStr.Contains(ProtectedUris[i]))
  224. return true;
  225. return true;
  226. }
  227. }
  228. }