HTTPOverTCPConnection.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. #if !UNITY_WEBGL || UNITY_EDITOR
  2. using System;
  3. using System.Collections.Generic;
  4. #if !BESTHTTP_DISABLE_ALTERNATE_SSL
  5. using Best.HTTP.Hosts.Connections.HTTP2;
  6. #endif
  7. using Best.HTTP.Hosts.Connections.HTTP1;
  8. using Best.HTTP.HostSetting;
  9. using Best.HTTP.Request.Timings;
  10. using Best.HTTP.Shared;
  11. using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
  12. using Best.HTTP.Shared.PlatformSupport.Threading;
  13. using Best.HTTP.Shared.Streams;
  14. namespace Best.HTTP.Hosts.Connections
  15. {
  16. // DNS -> TCP -> [ Proxy ] -> [ BC TLS | Framework TLS ] -> (HTTP/1 | HTTP/2)
  17. /// <summary>
  18. /// Represents and manages a connection to a server.
  19. /// </summary>
  20. public sealed class HTTPOverTCPConnection : ConnectionBase, INegotiationPeer
  21. {
  22. public PeekableContentProviderStream TopStream { get => this._negotiator.Stream; }
  23. public TCPStreamer Streamer { get => this._negotiator.Streamer; }
  24. public IHTTPRequestHandler requestHandler;
  25. /// <summary>
  26. /// Number of assigned requests to process.
  27. /// </summary>
  28. public override int AssignedRequests { get => this.requestHandler != null ? this.requestHandler.AssignedRequests : base.AssignedRequests; }
  29. /// <summary>
  30. /// Maximum number of assignable requests.
  31. /// </summary>
  32. public override int MaxAssignedRequests { get => this.requestHandler != null ? this.requestHandler.MaxAssignedRequests : base.MaxAssignedRequests; }
  33. public override TimeSpan KeepAliveTime
  34. {
  35. get
  36. {
  37. if (this.requestHandler != null && this.requestHandler.KeepAlive != null)
  38. {
  39. if (this.requestHandler.KeepAlive.MaxRequests > 0)
  40. {
  41. if (base.KeepAliveTime < this.requestHandler.KeepAlive.TimeOut)
  42. return base.KeepAliveTime;
  43. else
  44. return this.requestHandler.KeepAlive.TimeOut;
  45. }
  46. else
  47. return TimeSpan.Zero;
  48. }
  49. return base.KeepAliveTime;
  50. }
  51. protected set
  52. {
  53. base.KeepAliveTime = value;
  54. }
  55. }
  56. public override bool CanProcessMultiple
  57. {
  58. get
  59. {
  60. if (this.requestHandler != null)
  61. return this.requestHandler.CanProcessMultiple;
  62. return base.CanProcessMultiple;
  63. }
  64. }
  65. private Negotiator _negotiator;
  66. internal HTTPOverTCPConnection(HostKey hostKey)
  67. : base(hostKey)
  68. { }
  69. internal override void Process(HTTPRequest request)
  70. {
  71. this.LastProcessedUri = request.CurrentUri;
  72. this.CurrentRequest = request;
  73. this.State = HTTPConnectionStates.Processing;
  74. if (this.requestHandler == null)
  75. {
  76. try
  77. {
  78. NegotiationParameters parameters = new NegotiationParameters();
  79. parameters.context = this.Context;
  80. parameters.proxy = CurrentRequest.ProxySettings.Proxy;
  81. parameters.targetUri = CurrentRequest.CurrentUri;
  82. parameters.negotiateTLS = HTTPProtocolFactory.IsSecureProtocol(CurrentRequest.CurrentUri);
  83. parameters.token = CurrentRequest.CancellationTokenSource.Token;
  84. //parameters.tryToKeepAlive = HTTPManager.PerHostSettings.Get(CurrentRequest.CurrentUri.Host).HTTP1ConnectionSettings.TryToReuseConnections;
  85. parameters.hostSettings = HTTPManager.PerHostSettings.Get(CurrentRequest.CurrentUri.Host);
  86. this._negotiator = new Negotiator(this, parameters);
  87. this._negotiator.Start();
  88. }
  89. catch(Exception ex)
  90. {
  91. HTTPManager.Logger.Exception(nameof(HTTPOverTCPConnection), $"Process({request})", ex, this.Context);
  92. TrySetErrorState(request, ex);
  93. }
  94. }
  95. else
  96. {
  97. this.requestHandler.Process(request);
  98. LastProcessTime = DateTime.Now;
  99. }
  100. }
  101. List<string> INegotiationPeer.GetSupportedProtocolNames(Negotiator negotiator)
  102. {
  103. List<string> protocols = new List<string>();
  104. SupportedProtocols protocol = HTTPProtocolFactory.GetProtocolFromUri(negotiator.Parameters.targetUri);
  105. #if !BESTHTTP_DISABLE_ALTERNATE_SSL
  106. if (protocol == SupportedProtocols.HTTP && negotiator.Parameters.hostSettings.HTTP2ConnectionSettings.EnableHTTP2Connections)
  107. {
  108. // http/2 over tls (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids)
  109. protocols.Add(HTTPProtocolFactory.W3C_HTTP2);
  110. }
  111. #endif
  112. protocols.Add(HTTPProtocolFactory.W3C_HTTP1);
  113. return protocols;
  114. }
  115. bool INegotiationPeer.MustStopAdvancingToNextStep(Negotiator negotiator, NegotiationSteps finishedStep, NegotiationSteps nextStep, Exception error)
  116. {
  117. if (TrySetErrorState(CurrentRequest, error))
  118. return true;
  119. switch (finishedStep)
  120. {
  121. case NegotiationSteps.Start:
  122. this.LastProcessTime = DateTime.Now;
  123. this.CurrentRequest.Timing.StartNext(TimingEventNames.DNS_Lookup);
  124. break;
  125. case NegotiationSteps.DNSQuery:
  126. this.CurrentRequest.Timing.StartNext(TimingEventNames.TCP_Connection);
  127. break;
  128. case NegotiationSteps.TCPRace:
  129. CurrentRequest.OnCancellationRequested += OnCancellationRequested;
  130. CurrentRequest.Timing.StartNext(TimingEventNames.Proxy_Negotiation);
  131. break;
  132. case NegotiationSteps.Proxy:
  133. CurrentRequest.Timing.StartNext(TimingEventNames.TLS_Negotiation);
  134. break;
  135. case NegotiationSteps.TLSNegotiation:
  136. break;
  137. case NegotiationSteps.Finish:
  138. break;
  139. }
  140. return false;
  141. }
  142. void INegotiationPeer.EvaluateProxyNegotiationFailure(Negotiator negotiator, Exception error, bool resendForAuthentication)
  143. {
  144. if (resendForAuthentication && !this.TrySetErrorState(CurrentRequest, null))
  145. {
  146. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(CurrentRequest, RequestEvents.Resend));
  147. ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
  148. }
  149. else if (!this.TrySetErrorState(CurrentRequest, error))
  150. {
  151. // TODO: what?
  152. }
  153. }
  154. void INegotiationPeer.OnNegotiationFailed(Negotiator negotiator, Exception error)
  155. {
  156. PreprocessRequestState(error);
  157. }
  158. void INegotiationPeer.OnNegotiationFinished(Negotiator negotiator, PeekableContentProviderStream stream, TCPStreamer streamer, string negotiatedProtocol)
  159. {
  160. if (!PreprocessRequestState(null))
  161. StartWithNegotiatedProtocol(negotiatedProtocol, stream);
  162. }
  163. private void OnCancellationRequested(HTTPRequest req)
  164. {
  165. HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(OnCancellationRequested)}({req})", this.Context);
  166. CurrentRequest.OnCancellationRequested -= OnCancellationRequested;
  167. this._negotiator?.OnCancellationRequested();
  168. //ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
  169. }
  170. private bool PreprocessRequestState(Exception error)
  171. {
  172. CurrentRequest.OnCancellationRequested -= OnCancellationRequested;
  173. HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"PreprocessRequestState({CurrentRequest}, {error})", this.Context);
  174. // OnTLSNegotiated might get called _after_ the request is aborted. In this case, we must not set its State!
  175. // So here we have to check its State, if it's one of the Finished state (Finished, Error, etc.) we have to quit early and only enqueue a connection event.
  176. if (CurrentRequest.State >= HTTPRequestStates.Finished)
  177. {
  178. ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
  179. return true;
  180. }
  181. return TrySetErrorState(CurrentRequest, error);
  182. }
  183. /// <summary>
  184. /// Returns true if an error state is set to the request and the connection is closing.
  185. /// </summary>
  186. bool TrySetErrorState(HTTPRequest request, Exception ex)
  187. {
  188. // Check wether the request is already in a finshed state.
  189. // For example it can happen in the following case:
  190. // 1.) HTTP proxy sends out a CONNECT request to the proxy
  191. // 2.) Request times out and RequestEventHelper.AbortRequestWhenTimedOut is called
  192. // 2.a) Request's state set to ConnectionTimedOut
  193. // 3.) Request's callback is called
  194. // 4.) Either the Proxy connects or fails to connect to the remote host, but one of the first call in the callbacks is TrySetErrorState,
  195. // where we would try to set the request's State. If we would set a different state (like Error or TimedOut) than the one we already set (ConnectionTimedOut in this specific case)
  196. // then a new RequestEvents.StateChange event would be queued up and resulting in a new calling the request's callback again!
  197. if (request.State >= HTTPRequestStates.Finished)
  198. {
  199. ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
  200. return true;
  201. }
  202. if (ex != null)
  203. {
  204. request.Timing.StartNext(TimingEventNames.Queued);
  205. ConnectionHelper.EnqueueEvents(this,
  206. HTTPConnectionStates.Closed,
  207. request,
  208. ex is TimeoutException ? HTTPRequestStates.ConnectionTimedOut : HTTPRequestStates.Error,
  209. ex is TimeoutException ? (Exception)null : ex);
  210. return true;
  211. }
  212. else if (request.TimeoutSettings.IsConnectTimedOut(DateTime.Now))
  213. {
  214. TrySetErrorState(request, new TimeoutException("request.IsConnectTimedOut"));
  215. return true;
  216. }
  217. else if (request.IsCancellationRequested)
  218. {
  219. ConnectionHelper.EnqueueEvents(this, HTTPConnectionStates.Closed, request, HTTPRequestStates.Aborted, null);
  220. return true;
  221. }
  222. return false;
  223. }
  224. void StartWithNegotiatedProtocol(string negotiatedProtocol, PeekableContentProviderStream stream)
  225. {
  226. this.CurrentRequest.Timing.StartNext(TimingEventNames.Queued);
  227. if (string.IsNullOrEmpty(negotiatedProtocol))
  228. negotiatedProtocol = HTTPProtocolFactory.W3C_HTTP1;
  229. HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"Negotiated protocol through ALPN: '{negotiatedProtocol}'", this.Context);
  230. bool useShortLivingThread = false;
  231. switch (negotiatedProtocol)
  232. {
  233. case HTTPProtocolFactory.W3C_HTTP1:
  234. var http1Consumer = new HTTP1ContentConsumer(this);
  235. this.requestHandler = http1Consumer;
  236. stream.SetTwoWayBinding(http1Consumer);
  237. ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HostProtocolSupport.HTTP1));
  238. // https://github.com/Benedicht/BestHTTP-Issues/issues/179
  239. // Thoughts:
  240. // - Many requests, especially if they are uploading slowly, can occupy all background threads.
  241. // Use short-living thread when:
  242. // - It's a GET request
  243. // - The negotiated protocol is equal to HTTP/1.1
  244. // - It's not an upgrade request
  245. bool isRequestWithoutBody = this.CurrentRequest.MethodType == HTTPMethods.Get ||
  246. this.CurrentRequest.MethodType == HTTPMethods.Head ||
  247. this.CurrentRequest.MethodType == HTTPMethods.Delete ||
  248. this.CurrentRequest.MethodType == HTTPMethods.Options;
  249. bool isUpgrade = this.CurrentRequest.HasHeader("upgrade");
  250. useShortLivingThread = HTTPManager.PerHostSettings.Get(this.HostKey.Host).HTTP1ConnectionSettings.ForceUseThreadPool ||
  251. (isRequestWithoutBody && !isUpgrade);
  252. break;
  253. #if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
  254. case HTTPProtocolFactory.W3C_HTTP2:
  255. var http2Consumer = new HTTP2ContentConsumer(this);
  256. this.requestHandler = http2Consumer;
  257. stream.SetTwoWayBinding(http2Consumer);
  258. this.CurrentRequest = null;
  259. ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HostProtocolSupport.HTTP2));
  260. break;
  261. #endif
  262. default:
  263. HTTPManager.Logger.Error(nameof(HTTPOverTCPConnection), $"Unknown negotiated protocol: {negotiatedProtocol}", this.Context);
  264. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(CurrentRequest, RequestEvents.Resend));
  265. ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
  266. return;
  267. }
  268. this.requestHandler.Context.Add("Connection", this.Context.GetStringField("Hash"));
  269. this.Context.Add("RequestHandler", this.requestHandler.Context.GetStringField("Hash"));
  270. LastProcessTime = DateTime.Now;
  271. if (IsThreaded)
  272. {
  273. if (useShortLivingThread)
  274. ThreadedRunner.RunShortLiving(ThreadFunc);
  275. else
  276. ThreadedRunner.RunLongLiving(ThreadFunc);
  277. }
  278. else
  279. ThreadFunc();
  280. }
  281. protected override void ThreadFunc()
  282. {
  283. this.requestHandler.RunHandler();
  284. }
  285. public override void Shutdown(ShutdownTypes type)
  286. {
  287. base.Shutdown(type);
  288. if (this.requestHandler != null)
  289. this.requestHandler.Shutdown(type);
  290. else
  291. {
  292. // if the request handler is null, we can't do a gentle shutdown.
  293. this._negotiator?.Streamer?.Close();
  294. }
  295. switch (this.ShutdownType)
  296. {
  297. case ShutdownTypes.Immediate:
  298. this._negotiator.Stream?.Dispose();
  299. break;
  300. //case ShutdownTypes.Gentle:
  301. // this._streamer?.Close();
  302. // break;
  303. }
  304. }
  305. protected override void Dispose(bool disposing)
  306. {
  307. if (disposing)
  308. {
  309. LastProcessedUri = null;
  310. if (this.State != HTTPConnectionStates.WaitForProtocolShutdown)
  311. {
  312. this._negotiator?.Stream?.Dispose();
  313. if (this.requestHandler != null)
  314. {
  315. try
  316. {
  317. this.requestHandler.Dispose();
  318. }
  319. catch
  320. { }
  321. this.requestHandler = null;
  322. }
  323. this._negotiator?.Streamer?.Dispose();
  324. }
  325. }
  326. base.Dispose(disposing);
  327. }
  328. }
  329. }
  330. #endif