ConnectionHelper.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. using System;
  2. using System.Collections.Generic;
  3. using BestHTTP.Authentication;
  4. using BestHTTP.Core;
  5. using BestHTTP.Extensions;
  6. #if !BESTHTTP_DISABLE_CACHING
  7. using BestHTTP.Caching;
  8. #endif
  9. #if !BESTHTTP_DISABLE_COOKIES
  10. using BestHTTP.Cookies;
  11. #endif
  12. using BestHTTP.Logger;
  13. using BestHTTP.Timings;
  14. namespace BestHTTP.Connections
  15. {
  16. /// <summary>
  17. /// https://tools.ietf.org/html/draft-thomson-hybi-http-timeout-03
  18. /// Test servers: http://tools.ietf.org/ http://nginx.org/
  19. /// </summary>
  20. public sealed class KeepAliveHeader
  21. {
  22. /// <summary>
  23. /// A host sets the value of the "timeout" parameter to the time that the host will allow an idle connection to remain open before it is closed. A connection is idle if no data is sent or received by a host.
  24. /// </summary>
  25. public TimeSpan TimeOut { get; private set; }
  26. /// <summary>
  27. /// The "max" parameter has been used to indicate the maximum number of requests that would be made on the connection.This parameter is deprecated.Any limit on requests can be enforced by sending "Connection: close" and closing the connection.
  28. /// </summary>
  29. public int MaxRequests { get; private set; }
  30. public void Parse(List<string> headerValues)
  31. {
  32. HeaderParser parser = new HeaderParser(headerValues[0]);
  33. HeaderValue value;
  34. if (parser.TryGet("timeout", out value) && value.HasValue)
  35. {
  36. int intValue = 0;
  37. if (int.TryParse(value.Value, out intValue) && intValue > 1)
  38. this.TimeOut = TimeSpan.FromSeconds(intValue - 1);
  39. else
  40. this.TimeOut = TimeSpan.MaxValue;
  41. }
  42. if (parser.TryGet("max", out value) && value.HasValue)
  43. {
  44. int intValue = 0;
  45. if (int.TryParse("max", out intValue))
  46. this.MaxRequests = intValue;
  47. else
  48. this.MaxRequests = int.MaxValue;
  49. }
  50. }
  51. }
  52. public static class ConnectionHelper
  53. {
  54. public static void HandleResponse(string context, HTTPRequest request, out bool resendRequest, out HTTPConnectionStates proposedConnectionState, ref KeepAliveHeader keepAlive, LoggingContext loggingContext1 = null, LoggingContext loggingContext2 = null, LoggingContext loggingContext3 = null)
  55. {
  56. resendRequest = false;
  57. proposedConnectionState = HTTPConnectionStates.Processing;
  58. if (request.Response != null)
  59. {
  60. #if !BESTHTTP_DISABLE_COOKIES
  61. // Try to store cookies before we do anything else, as we may remove the response deleting the cookies as well.
  62. if (request.IsCookiesEnabled)
  63. CookieJar.Set(request.Response);
  64. #endif
  65. switch (request.Response.StatusCode)
  66. {
  67. // Not authorized
  68. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
  69. case 401:
  70. {
  71. string authHeader = DigestStore.FindBest(request.Response.GetHeaderValues("www-authenticate"));
  72. if (!string.IsNullOrEmpty(authHeader))
  73. {
  74. var digest = DigestStore.GetOrCreate(request.CurrentUri);
  75. digest.ParseChallange(authHeader);
  76. if (request.Credentials != null && digest.IsUriProtected(request.CurrentUri) && (!request.HasHeader("Authorization") || digest.Stale))
  77. resendRequest = true;
  78. }
  79. goto default;
  80. }
  81. #if !BESTHTTP_DISABLE_PROXY && (!UNITY_WEBGL || UNITY_EDITOR)
  82. case 407:
  83. {
  84. if (request.Proxy == null)
  85. goto default;
  86. resendRequest = request.Proxy.SetupRequest(request);
  87. goto default;
  88. }
  89. #endif
  90. // Redirected
  91. case 301: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.2
  92. case 302: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.3
  93. case 303: // "See Other"
  94. case 307: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.8
  95. case 308: // http://tools.ietf.org/html/rfc7238
  96. {
  97. if (request.RedirectCount >= request.MaxRedirects)
  98. goto default;
  99. request.RedirectCount++;
  100. string location = request.Response.GetFirstHeaderValue("location");
  101. if (!string.IsNullOrEmpty(location))
  102. {
  103. Uri redirectUri = ConnectionHelper.GetRedirectUri(request, location);
  104. if (HTTPManager.Logger.Level == Logger.Loglevels.All)
  105. HTTPManager.Logger.Verbose("HTTPConnection", string.Format("[{0}] - Redirected to Location: '{1}' redirectUri: '{1}'", context, location, redirectUri), loggingContext1, loggingContext2, loggingContext3);
  106. if (redirectUri == request.CurrentUri)
  107. {
  108. HTTPManager.Logger.Information("HTTPConnection", string.Format("[{0}] - Redirected to the same location!", context), loggingContext1, loggingContext2, loggingContext3);
  109. goto default;
  110. }
  111. // Let the user to take some control over the redirection
  112. if (!request.CallOnBeforeRedirection(redirectUri))
  113. {
  114. HTTPManager.Logger.Information("HTTPConnection", string.Format("[{0}] OnBeforeRedirection returned False", context), loggingContext1, loggingContext2, loggingContext3);
  115. goto default;
  116. }
  117. // Remove the previously set Host header.
  118. request.RemoveHeader("Host");
  119. // Set the Referer header to the last Uri.
  120. request.SetHeader("Referer", request.CurrentUri.ToString());
  121. // Set the new Uri, the CurrentUri will return this while the IsRedirected property is true
  122. request.RedirectUri = redirectUri;
  123. request.IsRedirected = true;
  124. resendRequest = true;
  125. }
  126. else
  127. throw new Exception(string.Format("[{0}] Got redirect status({1}) without 'location' header!", context, request.Response.StatusCode.ToString()));
  128. goto default;
  129. }
  130. #if !BESTHTTP_DISABLE_CACHING
  131. case 304:
  132. if (request.DisableCache)
  133. break;
  134. if (ConnectionHelper.LoadFromCache(context, request, loggingContext1, loggingContext2, loggingContext3))
  135. {
  136. request.Timing.Add(TimingEventNames.Loading_From_Cache);
  137. HTTPManager.Logger.Verbose("HTTPConnection", string.Format("[{0}] - HandleResponse - Loaded from cache successfully!", context), loggingContext1, loggingContext2, loggingContext3);
  138. // Update any caching value
  139. HTTPCacheService.SetUpCachingValues(request.CurrentUri, request.Response);
  140. }
  141. else
  142. {
  143. HTTPManager.Logger.Verbose("HTTPConnection", string.Format("[{0}] - HandleResponse - Loaded from cache failed!", context), loggingContext1, loggingContext2, loggingContext3);
  144. resendRequest = true;
  145. }
  146. break;
  147. #endif
  148. default:
  149. #if !BESTHTTP_DISABLE_CACHING
  150. ConnectionHelper.TryStoreInCache(request);
  151. #endif
  152. break;
  153. }
  154. // Closing the stream is done manually?
  155. if (request.Response != null && !request.Response.IsClosedManually)
  156. {
  157. // If we have a response and the server telling us that it closed the connection after the message sent to us, then
  158. // we will close the connection too.
  159. bool closeByServer = request.Response.HasHeaderWithValue("connection", "close");
  160. bool closeByClient = !request.IsKeepAlive;
  161. if (closeByServer || closeByClient)
  162. {
  163. proposedConnectionState = HTTPConnectionStates.Closed;
  164. }
  165. else if (request.Response != null)
  166. {
  167. var keepAliveheaderValues = request.Response.GetHeaderValues("keep-alive");
  168. if (keepAliveheaderValues != null && keepAliveheaderValues.Count > 0)
  169. {
  170. if (keepAlive == null)
  171. keepAlive = new KeepAliveHeader();
  172. keepAlive.Parse(keepAliveheaderValues);
  173. }
  174. }
  175. }
  176. // Null out the response here instead of the redirected cases (301, 302, 307, 308)
  177. // because response might have a Connection: Close header that we would miss to process.
  178. // If Connection: Close is present, the server is closing the connection and we would
  179. // reuse that closed connection.
  180. if (resendRequest)
  181. {
  182. // Discard the redirect response, we don't need it any more
  183. request.Response = null;
  184. if (proposedConnectionState == HTTPConnectionStates.Closed)
  185. proposedConnectionState = HTTPConnectionStates.ClosedResendRequest;
  186. }
  187. }
  188. }
  189. #if !BESTHTTP_DISABLE_CACHING
  190. public static bool LoadFromCache(string context, HTTPRequest request, LoggingContext loggingContext1 = null, LoggingContext loggingContext2 = null, LoggingContext loggingContext3 = null)
  191. {
  192. if (request.IsRedirected)
  193. {
  194. if (LoadFromCache(context, request, request.RedirectUri, loggingContext1, loggingContext2, loggingContext3))
  195. return true;
  196. else
  197. {
  198. Caching.HTTPCacheService.DeleteEntity(request.RedirectUri);
  199. }
  200. }
  201. bool loaded = LoadFromCache(context, request, request.Uri, loggingContext1, loggingContext2, loggingContext3);
  202. if (!loaded)
  203. Caching.HTTPCacheService.DeleteEntity(request.Uri);
  204. return loaded;
  205. }
  206. private static bool LoadFromCache(string context, HTTPRequest request, Uri uri, LoggingContext loggingContext1 = null, LoggingContext loggingContext2 = null, LoggingContext loggingContext3 = null)
  207. {
  208. if (HTTPManager.Logger.Level == Logger.Loglevels.All)
  209. HTTPManager.Logger.Verbose("HTTPConnection", string.Format("[{0}] - LoadFromCache for Uri: {1}", context, uri.ToString()), loggingContext1, loggingContext2, loggingContext3);
  210. var cacheEntity = HTTPCacheService.GetEntity(uri);
  211. if (cacheEntity == null)
  212. {
  213. HTTPManager.Logger.Warning("HTTPConnection", string.Format("[{0}] - LoadFromCache for Uri: {1} - Cached entity not found!", context, uri.ToString()), loggingContext1, loggingContext2, loggingContext3);
  214. return false;
  215. }
  216. request.Response.CacheFileInfo = cacheEntity;
  217. try
  218. {
  219. int bodyLength;
  220. using (var cacheStream = cacheEntity.GetBodyStream(out bodyLength))
  221. {
  222. if (cacheStream == null)
  223. return false;
  224. if (!request.Response.HasHeader("content-length"))
  225. request.Response.AddHeader("content-length", bodyLength.ToString());
  226. request.Response.IsFromCache = true;
  227. if (!request.CacheOnly)
  228. request.Response.ReadRaw(cacheStream, bodyLength);
  229. }
  230. }
  231. catch
  232. {
  233. return false;
  234. }
  235. return true;
  236. }
  237. public static bool TryLoadAllFromCache(string context, HTTPRequest request, LoggingContext loggingContext1 = null, LoggingContext loggingContext2 = null, LoggingContext loggingContext3 = null)
  238. {
  239. // We will try to read the response from the cache, but if something happens we will fallback to the normal way.
  240. try
  241. {
  242. //Unless specifically constrained by a cache-control (section 14.9) directive, a caching system MAY always store a successful response (see section 13.8) as a cache entity,
  243. // MAY return it without validation if it is fresh, and MAY return it after successful validation.
  244. // MAY return it without validation if it is fresh!
  245. if (HTTPManager.Logger.Level == Logger.Loglevels.All)
  246. HTTPManager.Logger.Verbose("ConnectionHelper", string.Format("[{0}] - TryLoadAllFromCache - whole response loading from cache", context), loggingContext1, loggingContext2, loggingContext3);
  247. HTTPCacheService.GetFullResponse(request);
  248. if (request.Response != null)
  249. return true;
  250. }
  251. catch
  252. {
  253. request.Response = null;
  254. HTTPManager.Logger.Verbose("ConnectionHelper", string.Format("[{0}] - TryLoadAllFromCache - failed to load content!", context), loggingContext1, loggingContext2, loggingContext3);
  255. HTTPCacheService.DeleteEntity(request.CurrentUri);
  256. }
  257. return false;
  258. }
  259. public static void TryStoreInCache(HTTPRequest request)
  260. {
  261. // if UseStreaming && !DisableCache then we already wrote the response to the cache
  262. if (!request.UseStreaming &&
  263. !request.DisableCache &&
  264. request.Response != null &&
  265. HTTPCacheService.IsSupported &&
  266. HTTPCacheService.IsCacheble(request.CurrentUri, request.MethodType, request.Response))
  267. {
  268. if (request.IsRedirected)
  269. HTTPCacheService.Store(request.Uri, request.MethodType, request.Response);
  270. else
  271. HTTPCacheService.Store(request.CurrentUri, request.MethodType, request.Response);
  272. request.Timing.Add(TimingEventNames.Writing_To_Cache);
  273. PluginEventHelper.EnqueuePluginEvent(new PluginEventInfo(PluginEvents.SaveCacheLibrary));
  274. }
  275. }
  276. #endif
  277. public static Uri GetRedirectUri(HTTPRequest request, string location)
  278. {
  279. Uri result = null;
  280. try
  281. {
  282. result = new Uri(location);
  283. if (result.IsFile || result.AbsolutePath == location)
  284. result = null;
  285. }
  286. catch
  287. {
  288. // Sometimes the server sends back only the path and query component of the new uri
  289. result = null;
  290. }
  291. if (result == null)
  292. {
  293. var baseURL = request.CurrentUri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
  294. if (!location.StartsWith("/"))
  295. {
  296. var segments = request.CurrentUri.Segments;
  297. segments[segments.Length - 1] = location;
  298. location = String.Join(string.Empty, segments);
  299. if (location.StartsWith("//"))
  300. location = location.Substring(1);
  301. }
  302. bool endsWithSlash = baseURL[baseURL.Length - 1] == '/';
  303. bool startsWithSlash = location[0] == '/';
  304. if (endsWithSlash && startsWithSlash)
  305. result = new Uri(baseURL + location.Substring(1));
  306. else if (!endsWithSlash && !startsWithSlash)
  307. result = new Uri(baseURL + '/' + location);
  308. else
  309. result = new Uri(baseURL + location);
  310. }
  311. return result;
  312. }
  313. }
  314. }