458 lines
19 KiB
C#
458 lines
19 KiB
C#
#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, IContentConsumer
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
PeekableContentProviderStream IContentConsumer.ContentProvider { get; }
|
|
|
|
private Negotiator _negotiator;
|
|
private NegotiationSteps _lastStep;
|
|
|
|
internal HTTPOverTCPConnection(HostKey hostKey)
|
|
: base(hostKey)
|
|
{ }
|
|
|
|
internal override void Process(HTTPRequest request)
|
|
{
|
|
this.LastProcessedUri = request.CurrentUri;
|
|
this.CurrentRequest = request;
|
|
this.CurrentRequest.Context.Add("Connection", this.Context.Hash);
|
|
|
|
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.UtcNow;
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(INegotiationPeer.MustStopAdvancingToNextStep)}({negotiator}, {finishedStep}, {nextStep}, {error})", this.Context);
|
|
|
|
_lastStep = finishedStep;
|
|
|
|
if (TrySetErrorState(CurrentRequest, error))
|
|
return true;
|
|
|
|
switch (finishedStep)
|
|
{
|
|
case NegotiationSteps.Start:
|
|
this.LastProcessTime = DateTime.UtcNow;
|
|
|
|
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;
|
|
|
|
switch(nextStep)
|
|
{
|
|
case NegotiationSteps.Proxy:
|
|
CurrentRequest.Timing.StartNext(TimingEventNames.Proxy_Negotiation);
|
|
break;
|
|
case NegotiationSteps.TLSNegotiation:
|
|
CurrentRequest.Timing.StartNext(TimingEventNames.TLS_Negotiation);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case NegotiationSteps.Proxy:
|
|
if (nextStep == NegotiationSteps.TLSNegotiation)
|
|
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)
|
|
{
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(OnCancellationRequested)}({_lastStep}, {req}, {this._negotiator?.Peer}, {this._negotiator?.Streamer}, {this._negotiator?.Stream})", this.Context);
|
|
|
|
CurrentRequest.OnCancellationRequested -= OnCancellationRequested;
|
|
|
|
this._negotiator?.OnCancellationRequested();
|
|
|
|
if (_lastStep < NegotiationSteps.TLSNegotiation)
|
|
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
|
}
|
|
|
|
private bool PreprocessRequestState(Exception error)
|
|
{
|
|
CurrentRequest.OnCancellationRequested -= OnCancellationRequested;
|
|
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
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.UtcNow))
|
|
{
|
|
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;
|
|
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
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.UtcNow;
|
|
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);
|
|
}
|
|
|
|
void IContentConsumer.SetBinding(PeekableContentProviderStream contentProvider)
|
|
{
|
|
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(IContentConsumer.SetBinding)}({contentProvider})", this.Context);
|
|
}
|
|
|
|
void IContentConsumer.UnsetBinding()
|
|
{
|
|
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(IContentConsumer.UnsetBinding)}()", this.Context);
|
|
}
|
|
|
|
void IContentConsumer.OnContent()
|
|
{
|
|
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(IContentConsumer.OnContent)}()", this.Context);
|
|
}
|
|
|
|
void IContentConsumer.OnConnectionClosed()
|
|
{
|
|
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(IContentConsumer.OnConnectionClosed)}()", this.Context);
|
|
|
|
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
|
}
|
|
|
|
void IContentConsumer.OnError(Exception ex)
|
|
{
|
|
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(IContentConsumer.OnError)}({ex})", this.Context);
|
|
|
|
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|