#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)
///
/// Represents and manages a connection to a server.
///
public sealed class HTTPOverTCPConnection : ConnectionBase, INegotiationPeer
{
public PeekableContentProviderStream TopStream { get => this._negotiator.Stream; }
public TCPStreamer Streamer { get => this._negotiator.Streamer; }
public IHTTPRequestHandler requestHandler;
///
/// Number of assigned requests to process.
///
public override int AssignedRequests { get => this.requestHandler != null ? this.requestHandler.AssignedRequests : base.AssignedRequests; }
///
/// Maximum number of assignable requests.
///
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 INegotiationPeer.GetSupportedProtocolNames(Negotiator negotiator)
{
List protocols = new List();
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);
}
///
/// Returns true if an error state is set to the request and the connection is closing.
///
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