PageRenderTime 44ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/src/ServiceStack.Client/WebRequestUtils.cs

http://github.com/ServiceStack/ServiceStack
C# | 322 lines | 245 code | 51 blank | 26 comment | 44 complexity | 2bbaf98646b4b04eb453b3167a1dfbbd MD5 | raw file
Possible License(s): BSD-3-Clause
  1. using System;
  2. using System.Net;
  3. using System.Text;
  4. using ServiceStack.Text;
  5. using ServiceStack.Logging;
  6. #if NETFX_CORE
  7. using System.Net.Http.Headers;
  8. using Windows.Security.Cryptography;
  9. using Windows.Security.Cryptography.Core;
  10. using Windows.Storage.Streams;
  11. #endif
  12. namespace ServiceStack
  13. {
  14. public class TokenException : AuthenticationException
  15. {
  16. public TokenException(string message) : base(message) {}
  17. }
  18. public class AuthenticationException : Exception, IHasStatusCode
  19. {
  20. public AuthenticationException() {}
  21. public AuthenticationException(string message)
  22. : base(message) {}
  23. public AuthenticationException(string message, Exception innerException)
  24. : base(message, innerException) {}
  25. public int StatusCode => (int) HttpStatusCode.Unauthorized;
  26. }
  27. // by adamfowleruk
  28. public class AuthenticationInfo
  29. {
  30. private static readonly ILog Log = LogManager.GetLogger(typeof(AuthenticationInfo));
  31. public string method { get; set; }
  32. public string realm { get; set; }
  33. public string qop { get; set; }
  34. public string nonce { get; set; }
  35. public string opaque { get; set; }
  36. // these values used between requests, and not taken from WWW-Authenticate header of response
  37. public string cnonce { get; set; }
  38. public int nc { get; set; }
  39. public AuthenticationInfo(string authHeader)
  40. {
  41. cnonce = "0a4f113b";
  42. nc = 1;
  43. // Example Digest header: WWW-Authenticate: Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"
  44. // get method from first word
  45. int pos = authHeader.IndexOf(" ", StringComparison.Ordinal);
  46. if (pos < 0)
  47. throw new AuthenticationException($"Authentication header not supported: {authHeader}");
  48. method = authHeader.Substring(0, pos).ToLowerInvariant();
  49. string remainder = authHeader.Substring(pos + 1);
  50. // split the rest by comma, then =
  51. string[] pars = remainder.Split(',');
  52. string[] newpars = new string[pars.Length];
  53. int maxnewpars = 0;
  54. // test possibility that a comma is mid value for a split (as in above example)
  55. for (int i = 0; i < pars.Length; i++)
  56. {
  57. if (pars[i].EndsWith("\""))
  58. {
  59. newpars[maxnewpars] = pars[i];
  60. maxnewpars++;
  61. }
  62. else
  63. {
  64. // merge with next one
  65. newpars[maxnewpars] = pars[i] + "," + pars[i + 1];
  66. maxnewpars++;
  67. i++; // skips next value
  68. }
  69. }
  70. // now go through each part, splitting on first = character, and removing leading and trailing spaces and " quotes
  71. for (int i = 0; i < maxnewpars; i++)
  72. {
  73. int pos2 = newpars[i].IndexOf("=", StringComparison.Ordinal);
  74. string name = newpars[i].Substring(0, pos2).Trim();
  75. string value = newpars[i].Substring(pos2 + 1).Trim();
  76. if (value.StartsWith("\""))
  77. {
  78. value = value.Substring(1);
  79. }
  80. if (value.EndsWith("\""))
  81. {
  82. value = value.Substring(0, value.Length - 1);
  83. }
  84. if ("qop".Equals(name))
  85. {
  86. qop = value;
  87. }
  88. else if ("realm".Equals(name))
  89. {
  90. realm = value;
  91. }
  92. else if ("nonce".Equals(name))
  93. {
  94. nonce = value;
  95. }
  96. else if ("opaque".Equals(name))
  97. {
  98. opaque = value;
  99. }
  100. }
  101. }
  102. public override string ToString()
  103. {
  104. return $"[AuthenticationInfo: method={method}, realm={realm}, qop={qop}, nonce={nonce}, opaque={opaque}, cnonce={cnonce}, nc={nc}]";
  105. }
  106. }
  107. public static class WebRequestUtils
  108. {
  109. internal static AuthenticationException CreateCustomException(string uri, AuthenticationException ex)
  110. {
  111. if (uri.StartsWith("https"))
  112. {
  113. return new AuthenticationException(
  114. "Invalid remote SSL certificate, overide with: \nServicePointManager.ServerCertificateValidationCallback += ((sender, certificate, chain, sslPolicyErrors) => isValidPolicy);", ex);
  115. }
  116. return null;
  117. }
  118. internal static bool ShouldAuthenticate(WebException webEx, bool hasAuthInfo)
  119. {
  120. return webEx?.Response != null
  121. && ((HttpWebResponse)webEx.Response).StatusCode == HttpStatusCode.Unauthorized
  122. && hasAuthInfo;
  123. }
  124. public static void AddBasicAuth(this WebRequest client, string userName, string password)
  125. {
  126. if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
  127. return;
  128. client.Headers[HttpHeaders.Authorization]
  129. = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(userName + ":" + password));
  130. }
  131. public static void AddApiKeyAuth(this WebRequest client, string apiKey)
  132. {
  133. if (string.IsNullOrEmpty(apiKey))
  134. return;
  135. client.Headers[HttpHeaders.Authorization]
  136. = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(apiKey + ":"));
  137. }
  138. public static void AddBearerToken(this WebRequest client, string bearerToken)
  139. {
  140. if (string.IsNullOrEmpty(bearerToken))
  141. return;
  142. client.Headers[HttpHeaders.Authorization] = "Bearer " + bearerToken;
  143. }
  144. public static string CalculateMD5Hash(string input)
  145. {
  146. // copied/pasted by adamfowleruk
  147. // step 1, calculate MD5 hash from input
  148. System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();
  149. byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
  150. byte[] hash = md5.ComputeHash(inputBytes);
  151. // step 2, convert byte array to hex string
  152. var sb = StringBuilderCache.Allocate();
  153. foreach (byte b in hash)
  154. {
  155. sb.Append(b.ToString("X2"));
  156. }
  157. return StringBuilderCache.ReturnAndFree(sb).ToLower(); // The RFC requires the hex values are lowercase
  158. }
  159. internal static string padNC(int num)
  160. {
  161. // by adamfowleruk
  162. var pad = "";
  163. for (var i = 0; i < (8 - ("" + num).Length); i++)
  164. {
  165. pad += "0";
  166. }
  167. var ret = pad + num;
  168. return ret;
  169. }
  170. internal static void AddAuthInfo(this WebRequest client, string userName, string password, AuthenticationInfo authInfo)
  171. {
  172. if ("basic".Equals(authInfo.method))
  173. {
  174. client.AddBasicAuth(userName, password); // FIXME AddBasicAuth ignores the server provided Realm property. Potential Bug.
  175. }
  176. else if ("digest".Equals(authInfo.method))
  177. {
  178. // do digest auth header using auth info
  179. // auth info saved in ServiceClientBase for subsequent requests
  180. client.AddDigestAuth(userName, password, authInfo);
  181. }
  182. }
  183. internal static void AddDigestAuth(this WebRequest client, string userName, string password, AuthenticationInfo authInfo)
  184. {
  185. // by adamfowleruk
  186. // See Client Request at http://en.wikipedia.org/wiki/Digest_access_authentication
  187. string ncUse = padNC(authInfo.nc);
  188. authInfo.nc++; // incrememnt for subsequent requests
  189. string ha1raw = userName + ":" + authInfo.realm + ":" + password;
  190. string ha1 = CalculateMD5Hash(ha1raw);
  191. string ha2raw = client.Method + ":" + client.RequestUri.PathAndQuery;
  192. string ha2 = CalculateMD5Hash(ha2raw);
  193. string md5rraw = ha1 + ":" + authInfo.nonce + ":" + ncUse + ":" + authInfo.cnonce + ":" + authInfo.qop + ":" + ha2;
  194. string response = CalculateMD5Hash(md5rraw);
  195. string header =
  196. "Digest username=\"" + userName + "\", realm=\"" + authInfo.realm + "\", nonce=\"" + authInfo.nonce + "\", uri=\"" +
  197. client.RequestUri.PathAndQuery + "\", cnonce=\"" + authInfo.cnonce + "\", nc=" + ncUse + ", qop=\"" + authInfo.qop + "\", response=\"" + response +
  198. "\", opaque=\"" + authInfo.opaque + "\"";
  199. client.Headers[HttpHeaders.Authorization] = header;
  200. }
  201. /// <summary>
  202. /// Naming convention for the request's Response DTO
  203. /// </summary>
  204. public const string ResponseDtoSuffix = "Response";
  205. public static string GetResponseDtoName(Type requestType)
  206. {
  207. #if NETSTANDARD2_0
  208. return requestType.FullName + ResponseDtoSuffix + "," + requestType.Assembly.GetName().Name;
  209. #else
  210. return requestType.FullName + ResponseDtoSuffix;
  211. #endif
  212. }
  213. public static Type GetErrorResponseDtoType<TResponse>(object request)
  214. {
  215. if (request is object[] batchRequest && batchRequest.Length > 0)
  216. request = batchRequest[0];
  217. var hasResponseStatus = typeof(TResponse).HasInterface(typeof(IHasResponseStatus))
  218. || typeof(TResponse).GetProperty("ResponseStatus") != null;
  219. return hasResponseStatus ? typeof(TResponse) : GetErrorResponseDtoType(request);
  220. }
  221. public static Type GetErrorResponseDtoType(object request)
  222. {
  223. return request == null
  224. ? typeof(ErrorResponse)
  225. : GetErrorResponseDtoType(request.GetType());
  226. }
  227. public static Type GetErrorResponseDtoType(Type requestType)
  228. {
  229. if (requestType == null)
  230. return typeof (ErrorResponse);
  231. //If a conventionally-named Response type exists use that regardless if it has ResponseStatus or not
  232. #if NETSTANDARD2_0
  233. var responseDtoType = Type.GetType(GetResponseDtoName(requestType));
  234. #else
  235. var responseDtoType = AssemblyUtils.FindType(GetResponseDtoName(requestType));
  236. #endif
  237. if (responseDtoType == null)
  238. {
  239. var genericDef = requestType.GetTypeWithGenericTypeDefinitionOf(typeof(IReturn<>));
  240. if (genericDef != null)
  241. {
  242. var returnDtoType = genericDef.GetGenericArguments()[0];
  243. var hasResponseStatus = returnDtoType.HasInterface(typeof(IHasResponseStatus))
  244. || returnDtoType.GetProperty("ResponseStatus") != null;
  245. //Only use the specified Return type if it has a ResponseStatus property
  246. if (hasResponseStatus)
  247. {
  248. responseDtoType = returnDtoType;
  249. }
  250. }
  251. }
  252. return responseDtoType ?? typeof(ErrorResponse);
  253. }
  254. /// <summary>
  255. /// Shortcut to get the ResponseStatus whether it's bare or inside a IHttpResult
  256. /// </summary>
  257. /// <param name="response"></param>
  258. /// <returns></returns>
  259. public static ResponseStatus GetResponseStatus(this object response)
  260. {
  261. if (response == null)
  262. return null;
  263. if (response is ResponseStatus status)
  264. return status;
  265. if (response is IHasResponseStatus hasResponseStatus)
  266. return hasResponseStatus.ResponseStatus;
  267. var statusGetter = TypeProperties.Get(response.GetType()).GetPublicGetter(nameof(ResponseStatus));
  268. return statusGetter?.Invoke(response) as ResponseStatus;
  269. }
  270. }
  271. }