123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- #if !UNITY_WEBGL || UNITY_EDITOR
- using System;
- using System.Collections.Generic;
- #if !BESTHTTP_DISABLE_ALTERNATE_SSL
- using Best.HTTP.Hosts.Connections.HTTP2;
- #endif
- using Best.HTTP.Hosts.Connections.HTTP1;
- using Best.HTTP.HostSetting;
- using Best.HTTP.Request.Timings;
- using Best.HTTP.Shared;
- using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
- using Best.HTTP.Shared.PlatformSupport.Threading;
- using Best.HTTP.Shared.Streams;
- namespace Best.HTTP.Hosts.Connections
- {
- // DNS -> TCP -> [ Proxy ] -> [ BC TLS | Framework TLS ] -> (HTTP/1 | HTTP/2)
- /// <summary>
- /// Represents and manages a connection to a server.
- /// </summary>
- public sealed class HTTPOverTCPConnection : ConnectionBase, INegotiationPeer
- {
- public PeekableContentProviderStream TopStream { get => this._negotiator.Stream; }
- public TCPStreamer Streamer { get => this._negotiator.Streamer; }
- public IHTTPRequestHandler requestHandler;
- /// <summary>
- /// Number of assigned requests to process.
- /// </summary>
- public override int AssignedRequests { get => this.requestHandler != null ? this.requestHandler.AssignedRequests : base.AssignedRequests; }
-
- /// <summary>
- /// Maximum number of assignable requests.
- /// </summary>
- public override int MaxAssignedRequests { get => this.requestHandler != null ? this.requestHandler.MaxAssignedRequests : base.MaxAssignedRequests; }
- public override TimeSpan KeepAliveTime
- {
- get
- {
- if (this.requestHandler != null && this.requestHandler.KeepAlive != null)
- {
- if (this.requestHandler.KeepAlive.MaxRequests > 0)
- {
- if (base.KeepAliveTime < this.requestHandler.KeepAlive.TimeOut)
- return base.KeepAliveTime;
- else
- return this.requestHandler.KeepAlive.TimeOut;
- }
- else
- return TimeSpan.Zero;
- }
- return base.KeepAliveTime;
- }
- protected set
- {
- base.KeepAliveTime = value;
- }
- }
- public override bool CanProcessMultiple
- {
- get
- {
- if (this.requestHandler != null)
- return this.requestHandler.CanProcessMultiple;
- return base.CanProcessMultiple;
- }
- }
- private Negotiator _negotiator;
- internal HTTPOverTCPConnection(HostKey hostKey)
- : base(hostKey)
- { }
- internal override void Process(HTTPRequest request)
- {
- this.LastProcessedUri = request.CurrentUri;
- this.CurrentRequest = request;
- this.State = HTTPConnectionStates.Processing;
- if (this.requestHandler == null)
- {
- try
- {
- NegotiationParameters parameters = new NegotiationParameters();
- parameters.context = this.Context;
- parameters.proxy = CurrentRequest.ProxySettings.Proxy;
- parameters.targetUri = CurrentRequest.CurrentUri;
- parameters.negotiateTLS = HTTPProtocolFactory.IsSecureProtocol(CurrentRequest.CurrentUri);
- parameters.token = CurrentRequest.CancellationTokenSource.Token;
- //parameters.tryToKeepAlive = HTTPManager.PerHostSettings.Get(CurrentRequest.CurrentUri.Host).HTTP1ConnectionSettings.TryToReuseConnections;
- parameters.hostSettings = HTTPManager.PerHostSettings.Get(CurrentRequest.CurrentUri.Host);
- this._negotiator = new Negotiator(this, parameters);
- this._negotiator.Start();
- }
- catch(Exception ex)
- {
- HTTPManager.Logger.Exception(nameof(HTTPOverTCPConnection), $"Process({request})", ex, this.Context);
- TrySetErrorState(request, ex);
- }
- }
- else
- {
- this.requestHandler.Process(request);
- LastProcessTime = DateTime.Now;
- }
- }
- List<string> INegotiationPeer.GetSupportedProtocolNames(Negotiator negotiator)
- {
- List<string> protocols = new List<string>();
- SupportedProtocols protocol = HTTPProtocolFactory.GetProtocolFromUri(negotiator.Parameters.targetUri);
- #if !BESTHTTP_DISABLE_ALTERNATE_SSL
- if (protocol == SupportedProtocols.HTTP && negotiator.Parameters.hostSettings.HTTP2ConnectionSettings.EnableHTTP2Connections)
- {
- // http/2 over tls (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids)
- protocols.Add(HTTPProtocolFactory.W3C_HTTP2);
- }
- #endif
- protocols.Add(HTTPProtocolFactory.W3C_HTTP1);
- return protocols;
- }
- bool INegotiationPeer.MustStopAdvancingToNextStep(Negotiator negotiator, NegotiationSteps finishedStep, NegotiationSteps nextStep, Exception error)
- {
- if (TrySetErrorState(CurrentRequest, error))
- return true;
- switch (finishedStep)
- {
- case NegotiationSteps.Start:
- this.LastProcessTime = DateTime.Now;
- this.CurrentRequest.Timing.StartNext(TimingEventNames.DNS_Lookup);
- break;
- case NegotiationSteps.DNSQuery:
- this.CurrentRequest.Timing.StartNext(TimingEventNames.TCP_Connection);
- break;
- case NegotiationSteps.TCPRace:
- CurrentRequest.OnCancellationRequested += OnCancellationRequested;
- CurrentRequest.Timing.StartNext(TimingEventNames.Proxy_Negotiation);
- break;
- case NegotiationSteps.Proxy:
- CurrentRequest.Timing.StartNext(TimingEventNames.TLS_Negotiation);
- break;
- case NegotiationSteps.TLSNegotiation:
- break;
- case NegotiationSteps.Finish:
- break;
- }
- return false;
- }
- void INegotiationPeer.EvaluateProxyNegotiationFailure(Negotiator negotiator, Exception error, bool resendForAuthentication)
- {
- if (resendForAuthentication && !this.TrySetErrorState(CurrentRequest, null))
- {
- RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(CurrentRequest, RequestEvents.Resend));
- ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
- }
- else if (!this.TrySetErrorState(CurrentRequest, error))
- {
- // TODO: what?
- }
- }
- void INegotiationPeer.OnNegotiationFailed(Negotiator negotiator, Exception error)
- {
- PreprocessRequestState(error);
- }
- void INegotiationPeer.OnNegotiationFinished(Negotiator negotiator, PeekableContentProviderStream stream, TCPStreamer streamer, string negotiatedProtocol)
- {
- if (!PreprocessRequestState(null))
- StartWithNegotiatedProtocol(negotiatedProtocol, stream);
- }
- private void OnCancellationRequested(HTTPRequest req)
- {
-
- HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(OnCancellationRequested)}({req})", this.Context);
-
- CurrentRequest.OnCancellationRequested -= OnCancellationRequested;
- this._negotiator?.OnCancellationRequested();
- //ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
- }
- private bool PreprocessRequestState(Exception error)
- {
- CurrentRequest.OnCancellationRequested -= OnCancellationRequested;
- HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"PreprocessRequestState({CurrentRequest}, {error})", this.Context);
- // OnTLSNegotiated might get called _after_ the request is aborted. In this case, we must not set its State!
- // 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.
- if (CurrentRequest.State >= HTTPRequestStates.Finished)
- {
- ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
- return true;
- }
- return TrySetErrorState(CurrentRequest, error);
- }
- /// <summary>
- /// Returns true if an error state is set to the request and the connection is closing.
- /// </summary>
- bool TrySetErrorState(HTTPRequest request, Exception ex)
- {
- // Check wether the request is already in a finshed state.
- // For example it can happen in the following case:
- // 1.) HTTP proxy sends out a CONNECT request to the proxy
- // 2.) Request times out and RequestEventHelper.AbortRequestWhenTimedOut is called
- // 2.a) Request's state set to ConnectionTimedOut
- // 3.) Request's callback is called
- // 4.) Either the Proxy connects or fails to connect to the remote host, but one of the first call in the callbacks is TrySetErrorState,
- // 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)
- // then a new RequestEvents.StateChange event would be queued up and resulting in a new calling the request's callback again!
- if (request.State >= HTTPRequestStates.Finished)
- {
- ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
- return true;
- }
- if (ex != null)
- {
- request.Timing.StartNext(TimingEventNames.Queued);
- ConnectionHelper.EnqueueEvents(this,
- HTTPConnectionStates.Closed,
- request,
- ex is TimeoutException ? HTTPRequestStates.ConnectionTimedOut : HTTPRequestStates.Error,
- ex is TimeoutException ? (Exception)null : ex);
- return true;
- }
- else if (request.TimeoutSettings.IsConnectTimedOut(DateTime.Now))
- {
- TrySetErrorState(request, new TimeoutException("request.IsConnectTimedOut"));
- return true;
- }
- else if (request.IsCancellationRequested)
- {
- ConnectionHelper.EnqueueEvents(this, HTTPConnectionStates.Closed, request, HTTPRequestStates.Aborted, null);
- return true;
- }
- return false;
- }
- void StartWithNegotiatedProtocol(string negotiatedProtocol, PeekableContentProviderStream stream)
- {
- this.CurrentRequest.Timing.StartNext(TimingEventNames.Queued);
- if (string.IsNullOrEmpty(negotiatedProtocol))
- negotiatedProtocol = HTTPProtocolFactory.W3C_HTTP1;
- HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"Negotiated protocol through ALPN: '{negotiatedProtocol}'", this.Context);
- bool useShortLivingThread = false;
- switch (negotiatedProtocol)
- {
- case HTTPProtocolFactory.W3C_HTTP1:
- var http1Consumer = new HTTP1ContentConsumer(this);
- this.requestHandler = http1Consumer;
- stream.SetTwoWayBinding(http1Consumer);
- ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HostProtocolSupport.HTTP1));
- // https://github.com/Benedicht/BestHTTP-Issues/issues/179
- // Thoughts:
- // - Many requests, especially if they are uploading slowly, can occupy all background threads.
- // Use short-living thread when:
- // - It's a GET request
- // - The negotiated protocol is equal to HTTP/1.1
- // - It's not an upgrade request
- bool isRequestWithoutBody = this.CurrentRequest.MethodType == HTTPMethods.Get ||
- this.CurrentRequest.MethodType == HTTPMethods.Head ||
- this.CurrentRequest.MethodType == HTTPMethods.Delete ||
- this.CurrentRequest.MethodType == HTTPMethods.Options;
- bool isUpgrade = this.CurrentRequest.HasHeader("upgrade");
- useShortLivingThread = HTTPManager.PerHostSettings.Get(this.HostKey.Host).HTTP1ConnectionSettings.ForceUseThreadPool ||
- (isRequestWithoutBody && !isUpgrade);
- break;
- #if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
- case HTTPProtocolFactory.W3C_HTTP2:
- var http2Consumer = new HTTP2ContentConsumer(this);
- this.requestHandler = http2Consumer;
- stream.SetTwoWayBinding(http2Consumer);
- this.CurrentRequest = null;
- ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HostProtocolSupport.HTTP2));
- break;
- #endif
- default:
- HTTPManager.Logger.Error(nameof(HTTPOverTCPConnection), $"Unknown negotiated protocol: {negotiatedProtocol}", this.Context);
- RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(CurrentRequest, RequestEvents.Resend));
- ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
- return;
- }
- this.requestHandler.Context.Add("Connection", this.Context.GetStringField("Hash"));
- this.Context.Add("RequestHandler", this.requestHandler.Context.GetStringField("Hash"));
- LastProcessTime = DateTime.Now;
- if (IsThreaded)
- {
- if (useShortLivingThread)
- ThreadedRunner.RunShortLiving(ThreadFunc);
- else
- ThreadedRunner.RunLongLiving(ThreadFunc);
- }
- else
- ThreadFunc();
- }
- protected override void ThreadFunc()
- {
- this.requestHandler.RunHandler();
- }
- public override void Shutdown(ShutdownTypes type)
- {
- base.Shutdown(type);
- if (this.requestHandler != null)
- this.requestHandler.Shutdown(type);
- else
- {
- // if the request handler is null, we can't do a gentle shutdown.
- this._negotiator?.Streamer?.Close();
- }
- switch (this.ShutdownType)
- {
- case ShutdownTypes.Immediate:
- this._negotiator.Stream?.Dispose();
- break;
- //case ShutdownTypes.Gentle:
- // this._streamer?.Close();
- // break;
- }
- }
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- LastProcessedUri = null;
- if (this.State != HTTPConnectionStates.WaitForProtocolShutdown)
- {
- this._negotiator?.Stream?.Dispose();
- if (this.requestHandler != null)
- {
- try
- {
- this.requestHandler.Dispose();
- }
- catch
- { }
- this.requestHandler = null;
- }
- this._negotiator?.Streamer?.Dispose();
- }
- }
- base.Dispose(disposing);
- }
- }
- }
- #endif
|