ConnectionHelper.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. using System;
  2. using System.Collections.Generic;
  3. using Best.HTTP.Caching;
  4. using Best.HTTP.Cookies;
  5. using Best.HTTP.Shared;
  6. using Best.HTTP.Shared.Extensions;
  7. using Best.HTTP.Shared.Logger;
  8. using static Best.HTTP.Response.HTTPStatusCodes;
  9. namespace Best.HTTP.Hosts.Connections
  10. {
  11. /// <summary>
  12. /// https://tools.ietf.org/html/draft-thomson-hybi-http-timeout-03
  13. /// Test servers: http://tools.ietf.org/ http://nginx.org/
  14. /// </summary>
  15. public sealed class KeepAliveHeader
  16. {
  17. /// <summary>
  18. /// 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.
  19. /// </summary>
  20. public TimeSpan TimeOut { get; private set; }
  21. /// <summary>
  22. /// 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.
  23. /// </summary>
  24. public int MaxRequests { get; private set; }
  25. public void Parse(List<string> headerValues)
  26. {
  27. HeaderParser parser = new HeaderParser(headerValues[0]);
  28. HeaderValue value;
  29. this.TimeOut = TimeSpan.MaxValue;
  30. this.MaxRequests = int.MaxValue;
  31. if (parser.TryGet("timeout", out value) && value.HasValue)
  32. {
  33. int intValue = 0;
  34. if (int.TryParse(value.Value, out intValue) && intValue > 1)
  35. this.TimeOut = TimeSpan.FromSeconds(intValue - 1);
  36. }
  37. if (parser.TryGet("max", out value) && value.HasValue)
  38. {
  39. int intValue = 0;
  40. if (int.TryParse("max", out intValue))
  41. this.MaxRequests = intValue;
  42. }
  43. }
  44. }
  45. /// <summary>
  46. /// Static helper class to handle cases where the plugin has to do additional logic based on the received response. These are like connection management, handling redirections, loading from local cache, authentication challanges, etc.
  47. /// </summary>
  48. public static class ConnectionHelper
  49. {
  50. public static void ResendRequestAndCloseConnection(ConnectionBase connection, HTTPRequest request)
  51. {
  52. ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(connection, request));
  53. }
  54. public static void EnqueueEvents(ConnectionBase connection, HTTPConnectionStates connectionState, HTTPRequest request, HTTPRequestStates requestState, Exception error)
  55. {
  56. // SetState
  57. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(request, requestState, error));
  58. ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(connection, connectionState));
  59. }
  60. /// <summary>
  61. /// Called when the whole response received
  62. /// </summary>
  63. public static Exception HandleResponse(HTTPRequest request,
  64. out bool resendRequest,
  65. out HTTPConnectionStates proposedConnectionState,
  66. ref KeepAliveHeader keepAlive,
  67. LoggingContext loggingContext)
  68. {
  69. resendRequest = false;
  70. proposedConnectionState = HTTPConnectionStates.Recycle;
  71. var resp = request.Response;
  72. var currentUri = request.CurrentUri;
  73. if (resp == null)
  74. return null;
  75. // Try to store cookies before we do anything else, as we may remove the response deleting the cookies as well.
  76. CookieJar.SetFromRequest(resp);
  77. switch (resp.StatusCode)
  78. {
  79. // Not authorized
  80. // https://www.rfc-editor.org/rfc/rfc9110.html#name-www-authenticate
  81. case Unauthorized:
  82. if (request.Authenticator != null)
  83. resendRequest = request.Authenticator.HandleChallange(request, resp);
  84. goto default;
  85. #if !UNITY_WEBGL || UNITY_EDITOR
  86. case ProxyAuthenticationRequired:
  87. if (request.ProxySettings == null)
  88. goto default;
  89. resendRequest = request.ProxySettings.Handle407(request);
  90. goto default;
  91. #endif
  92. // https://www.rfc-editor.org/rfc/rfc9110#name-417-expectation-failed
  93. case ExpectationFailed: // expectation failed
  94. // https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1-11.4
  95. // A client that receives a 417 (Expectation Failed) status code in response to a request
  96. // containing a 100-continue expectation SHOULD repeat that request without a 100-continue expectation,
  97. // since the 417 response merely indicates that the response chain does not support expectations (e.g., it passes through an HTTP/1.0 server).
  98. //request.UploadSettings.ResetExpects();
  99. //resendRequest = true;
  100. break;
  101. // Redirected
  102. case MovedPermanently: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.2
  103. case Found: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.3
  104. case SeeOther:
  105. case TemporaryRedirect: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.8
  106. case PermanentRedirect: // http://tools.ietf.org/html/rfc7238
  107. {
  108. if (request.RedirectSettings.RedirectCount >= request.RedirectSettings.MaxRedirects)
  109. goto default;
  110. request.RedirectSettings.RedirectCount++;
  111. string location = resp.GetFirstHeaderValue("location");
  112. if (!string.IsNullOrEmpty(location))
  113. {
  114. Uri redirectUri = ConnectionHelper.GetRedirectUri(request, location);
  115. if (HTTPManager.Logger.IsDiagnostic)
  116. HTTPManager.Logger.Verbose(nameof(ConnectionHelper), $"Redirected to Location: '{location}' redirectUri: '{redirectUri}'", loggingContext);
  117. if (redirectUri == request.CurrentUri)
  118. {
  119. HTTPManager.Logger.Information(nameof(ConnectionHelper), "Redirected to the same location!", loggingContext);
  120. goto default;
  121. }
  122. // Let the user to take some control over the redirection
  123. if (!request.RedirectSettings.CallOnBeforeRedirection(request, resp, redirectUri))
  124. {
  125. HTTPManager.Logger.Information(nameof(ConnectionHelper), "OnBeforeRedirection returned False", loggingContext);
  126. goto default;
  127. }
  128. if (!request.CurrentUri.Host.Equals(redirectUri.Host, StringComparison.OrdinalIgnoreCase))
  129. {
  130. #if !UNITY_WEBGL || UNITY_EDITOR
  131. //DNSCache.Prefetch(redirectUri.Host);
  132. Shared.PlatformSupport.Network.DNS.Cache.DNSCache.Query(new Shared.PlatformSupport.Network.DNS.Cache.DNSQueryParameters(redirectUri) { Context = loggingContext });
  133. #endif
  134. // Remove unsafe headers when redirected to an other host.
  135. // Just like for https://www.rfc-editor.org/rfc/rfc9110#name-redirection-3xx
  136. request.RemoveUnsafeHeaders();
  137. }
  138. // Set the Referer header to the last Uri.
  139. request.SetHeader("Referer", request.CurrentUri.ToString());
  140. // Set the new Uri, the CurrentUri will return this while the IsRedirected property is true
  141. request.RedirectSettings.RedirectUri = redirectUri;
  142. request.RedirectSettings.IsRedirected = true;
  143. resendRequest = true;
  144. }
  145. else
  146. return new Exception($"Got redirect status({resp.StatusCode}) without 'location' header!");
  147. goto default;
  148. }
  149. case NotModified:
  150. if (request.DownloadSettings.DisableCache || HTTPManager.LocalCache == null)
  151. break;
  152. var hash = HTTPCache.CalculateHash(request.MethodType, request.CurrentUri);
  153. if (HTTPManager.LocalCache.RefreshHeaders(hash, resp.Headers, request.Context))
  154. {
  155. HTTPManager.LocalCache.Redirect(request, hash);
  156. resendRequest = true;
  157. }
  158. break;
  159. // https://www.rfc-editor.org/rfc/rfc5861.html#section-4
  160. // In this context, an error is any situation that would result in a
  161. // 500, 502, 503, or 504 HTTP response status code being returned.
  162. case var statusCode when statusCode == InternalServerError || (statusCode >= BadGateway && statusCode <= GatewayTimeout):
  163. if (HTTPManager.LocalCache != null)
  164. {
  165. hash = HTTPCache.CalculateHash(request.MethodType, request.CurrentUri);
  166. if (HTTPManager.LocalCache.CanServeWithoutValidation(hash, ErrorTypeForValidation.ServerError, request.Context))
  167. {
  168. HTTPManager.LocalCache.Redirect(request, hash);
  169. resendRequest = true;
  170. }
  171. }
  172. break;
  173. default:
  174. break;
  175. }
  176. // If we have a response and the server telling us that it closed the connection after the message sent to us, then
  177. // we will close the connection too.
  178. bool closeByServer = resp.HasHeaderWithValue("connection", "close") ||
  179. resp.HasHeaderWithValue("proxy-connection", "close");
  180. bool tryToKeepAlive = !string.IsNullOrEmpty(currentUri.Host) && HTTPManager.PerHostSettings.Get(currentUri.Host).HTTP1ConnectionSettings.TryToReuseConnections;
  181. bool closeByClient = !tryToKeepAlive;
  182. // BugFix: We MUST NOT close the underlying connection when we just upgraded (switched) protocols!
  183. // - Explanation: Setting TryToReuseConnections to false would otherwise force the connection to close for websocket connections too!
  184. if ((closeByServer || closeByClient) && resp.StatusCode != SwitchingProtocols)
  185. {
  186. proposedConnectionState = HTTPConnectionStates.Closed;
  187. }
  188. else if (resp != null)
  189. {
  190. var keepAliveheaderValues = resp.GetHeaderValues("keep-alive");
  191. if (keepAliveheaderValues != null && keepAliveheaderValues.Count > 0)
  192. {
  193. if (keepAlive == null)
  194. keepAlive = new KeepAliveHeader();
  195. keepAlive.Parse(keepAliveheaderValues);
  196. }
  197. }
  198. // Null out the response here instead of the redirected cases (301, 302, 307, 308)
  199. // because response might have a Connection: Close header that we would miss to process.
  200. // If Connection: Close is present, the server is closing the connection and we would
  201. // reuse that closed connection.
  202. if (resendRequest)
  203. {
  204. HTTPManager.Logger.Verbose(nameof(ConnectionHelper), "HandleResponse - discarding response", request.Response?.Context ?? loggingContext);
  205. request.Response?.Dispose();
  206. // Discard the redirect response, we don't need it any more
  207. request.Response = null;
  208. if (proposedConnectionState == HTTPConnectionStates.Closed)
  209. proposedConnectionState = HTTPConnectionStates.ClosedResendRequest;
  210. }
  211. if (!resendRequest && proposedConnectionState < HTTPConnectionStates.Closed && resp.IsUpgraded)
  212. proposedConnectionState = HTTPConnectionStates.WaitForProtocolShutdown;
  213. else
  214. {
  215. // Do nothing here, the what we are timing for is decived in the caller, outside code.
  216. //request.Timing.Finish(TimingEventNames.Response_Received);
  217. }
  218. return null;
  219. }
  220. public static Uri GetRedirectUri(HTTPRequest request, string location)
  221. {
  222. Uri result = null;
  223. try
  224. {
  225. result = new Uri(location);
  226. if (result.IsFile || result.AbsolutePath == location)
  227. result = null;
  228. }
  229. catch
  230. {
  231. // Sometimes the server sends back only the path and query component of the new uri
  232. result = null;
  233. }
  234. if (result == null)
  235. {
  236. var baseURL = request.CurrentUri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
  237. if (!location.StartsWith("/"))
  238. {
  239. var segments = request.CurrentUri.Segments;
  240. segments[segments.Length - 1] = location;
  241. location = String.Join(string.Empty, segments);
  242. if (location.StartsWith("//"))
  243. location = location.Substring(1);
  244. }
  245. bool endsWithSlash = baseURL[baseURL.Length - 1] == '/';
  246. bool startsWithSlash = location[0] == '/';
  247. if (endsWithSlash && startsWithSlash)
  248. result = new Uri(baseURL + location.Substring(1));
  249. else if (!endsWithSlash && !startsWithSlash)
  250. result = new Uri(baseURL + '/' + location);
  251. else
  252. result = new Uri(baseURL + location);
  253. }
  254. return result;
  255. }
  256. }
  257. }