This commit is contained in:
2026-06-15 18:18:16 +08:00
parent 97c9fba14e
commit 2b9f134e5f
4164 changed files with 386922 additions and 79 deletions

View File

@@ -0,0 +1,15 @@
namespace Best.HTTP.Hosts.Connections.HTTP1
{
public static class Constants
{
public const byte CR = 13;
public const byte LF = 10;
public static readonly byte[] EOL = { Constants.CR, Constants.LF };
public static readonly byte[] HeaderValueSeparator = { (byte)':', (byte)' ' };
// expect: 100-continue
//public static readonly byte[] Expect100Continue = { (byte)'e', (byte)'x', (byte)'p', (byte)'e', (byte)'c', (byte)'t', (byte)':', (byte)' ', (byte)'1', (byte)'0', (byte)'0', (byte)'-', (byte)'c', (byte)'o', (byte)'n', (byte)'t', (byte)'i', (byte)'n', (byte)'u', (byte)'e', CR, LF };
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7fa489dfa1912f42974e02a6cf10e9e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,605 @@
#if !UNITY_WEBGL || UNITY_EDITOR
using Best.HTTP.Request.Timings;
using Best.HTTP.Request.Upload;
using Best.HTTP.Response;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
using Best.HTTP.Shared.PlatformSupport.Threading;
using Best.HTTP.Shared.Streams;
using System;
using System.Threading;
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
namespace Best.HTTP.Hosts.Connections.HTTP1
{
public sealed class HTTP1ContentConsumer : IHTTPRequestHandler, IContentConsumer, IDownloadContentBufferAvailable, IThreadSignaler
{
public ShutdownTypes ShutdownType { get; private set; }
public KeepAliveHeader KeepAlive { get { return this._keepAlive; } }
private KeepAliveHeader _keepAlive;
public bool CanProcessMultiple { get { return false; } }
/// <summary>
/// Number of assigned requests to process.
/// </summary>
public int AssignedRequests => this.conn.CurrentRequest == null ? 0 : 1;
/// <summary>
/// Maximum number of assignable requests
/// </summary>
public int MaxAssignedRequests => 1;
public LoggingContext Context { get; private set; }
public PeekableContentProviderStream ContentProvider { get; private set; }
private readonly HTTPOverTCPConnection conn;
private PeekableHTTP1Response _response;
private int _isAlreadyProcessingContent;
private WriteOnlyBufferedStream _upStream;
private int _isSendingContent;
private int _hasMoreData;
private bool _isContentSendingFinished;
private int _acceptContentSignals;
// Initialize the progress report variables
private long _uploaded = 0;
public HTTP1ContentConsumer(HTTPOverTCPConnection conn)
{
this.Context = new LoggingContext(this);
this.conn = conn;
}
public void RunHandler()
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), "Started processing request", this.Context);
//ThreadedRunner.SetThreadName("Best.HTTP1 Write");
try
{
var now = DateTime.UtcNow;
if (this.conn.CurrentRequest.TimeoutSettings.IsTimedOut(now))
throw new TimeoutException();
if (this.conn.CurrentRequest.IsCancellationRequested)
throw new Exception("Cancellation requested!");
// create the response before we would send out the request, because sending out might cause an exception
// and the response is used for decision making in the FinishedProcessing call.
if (this._response == null)
this.conn.CurrentRequest.Response = this._response = new PeekableHTTP1Response(this.conn.CurrentRequest, false, this);
// Write the request to the stream
this.conn.CurrentRequest.TimeoutSettings.SetProcessing(now);
this.conn.CurrentRequest.Timing.StartNext(TimingEventNames.Request_Sent);
SendOutTo(this.conn.CurrentRequest, this.conn.TopStream);
this.conn.CurrentRequest.OnCancellationRequested += OnCancellationRequested;
}
catch (Exception e)
{
if (this.ShutdownType == ShutdownTypes.Immediate)
return;
FinishedProcessing(e);
}
}
private void SendOutTo(HTTPRequest request, System.IO.Stream stream)
{
request.Prepare();
string requestPathAndQuery =
request.ProxySettings.HasProxyFor(request.CurrentUri) ?
request.ProxySettings.Proxy.GetRequestPath(request.CurrentUri) :
request.CurrentUri.GetRequestPathAndQueryURL();
string requestLine = string.Format("{0} {1} HTTP/1.1", HTTPRequest.MethodNames[(byte)request.MethodType], requestPathAndQuery);
if (HTTPManager.Logger.Level <= Loglevels.Information)
HTTPManager.Logger.Information("HTTPRequest", string.Format("Sending request: '{0}'", requestLine), request.Context);
// Create a buffer stream that will not close 'stream' when disposed or closed.
// buffersize should be larger than UploadChunkSize as it might be used for uploading user data and
// it should have enough room for UploadChunkSize data and additional chunk information.
using (WriteOnlyBufferedStream bufferStream = new WriteOnlyBufferedStream(stream, (int)(request.UploadSettings.UploadChunkSize * 1.5f), request.Context))
{
var requestLineBytes = requestLine.GetASCIIBytes();
bufferStream.WriteBufferSegment(requestLineBytes);
bufferStream.WriteArray(EOL);
BufferPool.Release(requestLineBytes);
// Write headers to the buffer
request.EnumerateHeaders((header, values) =>
{
if (string.IsNullOrEmpty(header) || values == null)
return;
//var headerName = string.Concat(header, ": ").GetASCIIBytes();
var headerName = header.GetASCIIBytes();
for (int i = 0; i < values.Count; ++i)
{
if (string.IsNullOrEmpty(values[i]))
{
HTTPManager.Logger.Warning("HTTPRequest", string.Format("Null/empty value for header: {0}", header), request.Context);
continue;
}
if (HTTPManager.Logger.Level <= Loglevels.Information)
HTTPManager.Logger.Verbose("HTTPRequest", $"Header - '{header}': '{values[i]}'", request.Context);
var valueBytes = values[i].GetASCIIBytes();
bufferStream.WriteBufferSegment(headerName);
bufferStream.WriteArray(HeaderValueSeparator);
bufferStream.WriteBufferSegment(valueBytes);
bufferStream.WriteArray(EOL);
BufferPool.Release(valueBytes);
}
BufferPool.Release(headerName);
}, /*callBeforeSendCallback:*/ true);
bufferStream.WriteArray(EOL);
// Send body content
if (this.conn.CurrentRequest.UploadSettings.UploadStream is Request.Upload.UploadStreamBase upStream)
upStream.BeforeSendBody(this.conn.CurrentRequest, this);
Interlocked.Exchange(ref this._isSendingContent, 1);
Interlocked.Exchange(ref this._acceptContentSignals, 1);
SendContent(bufferStream);
} // bufferStream.Dispose
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information("HTTPRequest", "Sent out '" + requestLine + "'", this.Context);
}
void SendContent(WriteOnlyBufferedStream streamToSend)
{
Interlocked.Exchange(ref this._hasMoreData, 0);
var request = this.conn.CurrentRequest;
System.IO.Stream uploadStream = request.UploadSettings.UploadStream;
try
{
if (uploadStream == null)
{
this._isContentSendingFinished = true;
}
else
{
if (streamToSend == null)
{
if (this._upStream == null)
this._upStream = new WriteOnlyBufferedStream(this.conn.TopStream, (int)(request.UploadSettings.UploadChunkSize * 1.5f), request.Context);
streamToSend = this._upStream;
}
long uploadLength = uploadStream.Length;
bool isChunked = uploadLength == BodyLengths.UnknownWithChunkedTransferEncoding;
// Upload buffer. First we will read the data into this buffer from the UploadStream, then write this buffer to our outStream
byte[] buffer = BufferPool.Get(this.conn.CurrentRequest.UploadSettings.UploadChunkSize, true);
using var _ = new AutoReleaseBuffer(buffer);
// How many bytes was read from the UploadStream
int count = uploadStream.Read(buffer, 0, buffer.Length);
while (count > 0)
{
if (isChunked)
{
var countBytes = count.ToString("X").GetASCIIBytes();
streamToSend.WriteBufferSegment(countBytes);
streamToSend.WriteArray(EOL);
BufferPool.Release(countBytes);
}
// write out the buffer to the wire
streamToSend.Write(buffer, 0, count);
// chunk trailing EOL
if (uploadLength < 0)
streamToSend.WriteArray(EOL);
// update how many bytes are uploaded
_uploaded += count;
if (this.conn.CurrentRequest.UploadSettings.OnUploadProgress != null)
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.conn.CurrentRequest, RequestEvents.UploadProgress, _uploaded, uploadLength));
if (this.conn.CurrentRequest.IsCancellationRequested)
return;
count = uploadStream.Read(buffer, 0, buffer.Length);
}
this._isContentSendingFinished = count == UploadReadConstants.Completed;
// All data from the stream are sent, write the 'end' chunk if necessary
if (isChunked && this._isContentSendingFinished)
{
ReadOnlySpan<byte> bytes = stackalloc byte[] { (byte)'0' };
streamToSend.Write(bytes);
streamToSend.WriteArray(EOL);
streamToSend.WriteArray(EOL);
}
}
}
finally
{
// Make sure all remaining data will be on the wire
streamToSend?.Flush();
if (this._isContentSendingFinished)
{
this._upStream?.Dispose();
this._upStream = null;
if (this.conn.CurrentRequest.UploadSettings.DisposeStream)
uploadStream?.Dispose();
this.conn.CurrentRequest.Timing.StartNext(TimingEventNames.Waiting_TTFB);
Interlocked.Exchange(ref this._isSendingContent, 0);
}
else
{
Interlocked.Exchange(ref this._isSendingContent, 0);
if (this._hasMoreData > 0)
(this as IThreadSignaler).SignalThread(SignalHandlerTypes.Signal);
}
}
}
void IDownloadContentBufferAvailable.BufferAvailable(DownloadContentStream stream)
{
//HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), "IDownloadContentBufferAvailable.BufferAvailable", this.Context);
// TODO: Do NOT call OnContent on the Unity main thread
if (this._response != null)
OnContent();
}
public void SetBinding(PeekableContentProviderStream contentProvider) => this.ContentProvider = contentProvider;
public void UnsetBinding() => this.ContentProvider = null;
public void OnContent()
{
if (Interlocked.CompareExchange(ref this._isAlreadyProcessingContent, 1, 0) != 0)
return;
try
{
//HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnContent({peekable?.Length}, {this._response?.ReadState})", this.Context, this.conn.CurrentRequest.Context);
try
{
if (this.conn.CurrentRequest.TimeoutSettings.IsTimedOut(DateTime.UtcNow))
throw new TimeoutException();
if (this.conn.CurrentRequest.IsCancellationRequested)
throw new Exception("Cancellation requested!");
this._response.ProcessPeekable(this.ContentProvider);
}
catch (Exception e)
{
if (this.ShutdownType == ShutdownTypes.Immediate)
return;
FinishedProcessing(e);
}
// After an exception, this._response will be null!
if (this._response != null)
{
if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
FinishedProcessing(null);
else if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.WaitForContentSent)
{
(this as IThreadSignaler).SignalThread(SignalHandlerTypes.Signal);
}
}
}
finally
{
Interlocked.Exchange(ref this._isAlreadyProcessingContent, 0);
}
}
public void OnConnectionClosed()
{
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnConnectionClosed({this.ContentProvider?.Length}, {this._response?.ReadState})", this.Context);
if (this._response != null &&
this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Content &&
this._response.DeliveryMode == PeekableHTTP1Response.ContentDeliveryMode.RawUnknownLength &&
"close".Equals(this._response.GetFirstHeaderValue("connection"), StringComparison.OrdinalIgnoreCase))
{
FinishedProcessing(null);
}
else if (this.ContentProvider != null && this.ContentProvider.Length > 0 &&
this._response != null &&
this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Content &&
this._response.DownStream != null)
{
// Let the stream comsume any buffered data first, and handle closure when the buffer depletes.
// TODO: This require however that the PeekableResponse ping this HTTP1ContentConsumer when buffer space is available in the down-stream.
// Or force-add all remaining data to the stream and see whether we finished downloading or not.
// Problems:
// 1.) OnContent might be already called and a call to it would be dropped. We could spin up a new thread waiting for its finish, then call it again.
//throw new NotImplementedException();
ThreadedRunner.RunShortLiving(() =>
{
SpinWait spinWait = new SpinWait();
while (Interlocked.CompareExchange(ref this._isAlreadyProcessingContent, 1, 0) == 1)
spinWait.SpinOnce();
try
{
try
{
this._response.DownStream.EmergencyIncreaseMaxBuffered();
this._response.ProcessPeekable(this.ContentProvider);
}
catch (Exception e)
{
if (this.ShutdownType == ShutdownTypes.Immediate)
return;
FinishedProcessing(e);
}
finally
{
// After an exception, this._response will be null!
if (this._response != null)
{
if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
FinishedProcessing(null);
else
FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
}
}
}
finally
{
Interlocked.Exchange(ref this._isAlreadyProcessingContent, 0);
}
});
return;
}
// If the consumer still have a request: error it and close the connection
if (this.conn.CurrentRequest != null && this._response != null)
{
FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
}
else // If no current request: close the connection
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this.conn, HTTPConnectionStates.Closed));
}
public void OnError(Exception e)
{
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnError({this.ContentProvider?.Length}, {this._response?.ReadState}, {this.ShutdownType})", this.Context);
if (this.ShutdownType == ShutdownTypes.Immediate)
return;
FinishedProcessing(e);
}
private void OnCancellationRequested(HTTPRequest req)
{
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), "OnCancellationRequested()", this.Context);
Interlocked.Exchange(ref this._response, null);
req.OnCancellationRequested -= OnCancellationRequested;
this.conn?.Streamer?.Dispose();
}
void FinishedProcessing(Exception ex)
{
// Warning: FinishedProcessing might be called from different threads in parallel:
// - send thread triggered by a write failure
// - read thread oncontent/OnError/OnConnectionClosed
var resp = Interlocked.Exchange(ref this._response, null);
if (resp == null)
return;
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), $"{nameof(FinishedProcessing)}({resp.ReadState}, {ex})", this.Context);
// Unset the consumer, we no longer expect another OnContent call until further notice.
//if (conn.TopStream is IPeekableContentProvider provider && provider?.Consumer == this)
// provider.Consumer = null;
this.ContentProvider.UnbindIf(this);
var req = this.conn.CurrentRequest;
req.OnCancellationRequested -= OnCancellationRequested;
bool resendRequest = false;
HTTPRequestStates requestState = HTTPRequestStates.Finished;
HTTPConnectionStates connectionState = ex != null ? HTTPConnectionStates.Closed : HTTPConnectionStates.Recycle;
// We could finish the request, ignore the error.
if (resp.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
ex = null;
Exception error = ex;
if (error != null)
{
// Timeout is a non-retryable error
if (ex is TimeoutException)
{
error = null;
requestState = HTTPRequestStates.TimedOut;
}
else
{
if (req.RetrySettings.Retries < req.RetrySettings.MaxRetries && !HTTPManager.IsQuitting)
{
req.RetrySettings.Retries++;
error = null;
resendRequest = true;
}
else
{
requestState = HTTPRequestStates.Error;
}
}
// Any exception means that the connection is in an unknown state, we shouldn't try to reuse it.
connectionState = HTTPConnectionStates.Closed;
resp.Dispose();
}
else
{
// After HandleResponse connectionState can have the following values:
// - Processing: nothing interesting, caller side can decide what happens with the connection (recycle connection).
// - Closed: server sent an connection: close header.
// - ClosedResendRequest: in this case resendRequest is true, and the connection must not be reused.
// In this case we can send only one ConnectionEvent to handle both case and avoid concurrency issues.
error = ConnectionHelper.HandleResponse(req, out resendRequest, out connectionState, ref this._keepAlive, this.Context);
if (error != null)
requestState = HTTPRequestStates.Error;
else if (!resendRequest && resp.IsUpgraded)
requestState = HTTPRequestStates.Processing;
}
req.Timing.StartNext(TimingEventNames.Queued);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), $"{nameof(FinishedProcessing)} final decision. ResendRequest: {resendRequest}, RequestState: {requestState}, ConnectionState: {connectionState}", this.Context);
// If HandleResponse returned with ClosedResendRequest or there were an error and we can retry the request
if (connectionState == HTTPConnectionStates.ClosedResendRequest || (resendRequest && connectionState == HTTPConnectionStates.Closed))
{
ConnectionHelper.ResendRequestAndCloseConnection(this.conn, req);
}
else if (resendRequest && requestState == HTTPRequestStates.Finished)
{
(conn.TopStream as IPeekableContentProvider).SwitchIf(null, this.conn as IContentConsumer);
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, RequestEvents.Resend));
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this.conn, connectionState));
}
else
{
if (resp == null || !resp.IsUpgraded)
(conn.TopStream as IPeekableContentProvider).SwitchIf(null, this.conn as IContentConsumer);
// Otherwise set the request's then the connection's state
ConnectionHelper.EnqueueEvents(this.conn, connectionState, req, requestState, error);
}
}
public void Process(HTTPRequest request)
{
(conn.TopStream as IPeekableContentProvider).SetTwoWayBinding(this);
// https://github.com/Benedicht/BestHTTP-Issues/issues/179
// Toughts:
// - Many requests, especially if they are uploading slowly, can occupy all background threads.
// Use short-living thread when:
// - It's a GET request
// - It's not an upgrade request
bool isRequestWithoutBody = request.MethodType == HTTPMethods.Get ||
request.MethodType == HTTPMethods.Head ||
request.MethodType == HTTPMethods.Delete ||
request.MethodType == HTTPMethods.Options;
bool isUpgrade = request.HasHeader("upgrade");
var useShortLivingThread = HTTPManager.PerHostSettings.Get(request.CurrentHostKey).HTTP1ConnectionSettings.ForceUseThreadPool ||
(isRequestWithoutBody && !isUpgrade);
if (useShortLivingThread)
ThreadedRunner.RunShortLiving(RunHandler);
else
ThreadedRunner.RunLongLiving(RunHandler);
}
public void Shutdown(ShutdownTypes type)
{
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), string.Format($"Shutdown({type})"), this.Context);
this.ShutdownType = type;
}
void IThreadSignaler.SignalThread(SignalHandlerTypes signalType)
{
Interlocked.Exchange(ref this._hasMoreData, 1);
if (this._acceptContentSignals == 1 && Interlocked.CompareExchange(ref this._isSendingContent, 1, 0) == 0)
{
switch (signalType)
{
case SignalHandlerTypes.Signal:
ThreadedRunner.RunShortLiving(SignalThreadHandler);
break;
case SignalHandlerTypes.InlineIfPossible:
SignalThreadHandler();
break;
}
}
}
void SignalThreadHandler()
{
try
{
SendContent(null);
}
catch (Exception e)
{
if (this.ShutdownType == ShutdownTypes.Immediate)
return;
FinishedProcessing(e);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
this._upStream?.Dispose();
this._upStream = null;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1c761c8505fda1a4e9e6be9f79b74054
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,540 @@
using Best.HTTP.Request.Timings;
using Best.HTTP.Response.Decompression;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
using static Best.HTTP.Response.HTTPStatusCodes;
namespace Best.HTTP.Hosts.Connections.HTTP1
{
/// <summary>
/// An HTTP 1.1 response implementation that can utilize a peekable stream.
/// Its main entry point is the ProcessPeekable method that should be called after every chunk of data downloaded.
/// </summary>
public class PeekableHTTP1Response : HTTPResponse
{
public PeekableReadState ReadState
{
get => this._readState;
private set
{
if (this._readState != value && HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), $"{this._readState} => {value}", this.Context);
this._readState = value;
}
}
private PeekableReadState _readState;
public bool ForceDepleteContent;
public enum ContentDeliveryMode
{
Raw,
RawUnknownLength,
Chunked,
}
public enum PeekableReadState
{
StatusLine,
Headers,
WaitForContentSent, // when received a 100-continue
PrepareForContent,
ContentSetup,
Content,
Finished
}
public ContentDeliveryMode DeliveryMode => this._deliveryMode;
private ContentDeliveryMode _deliveryMode;
private long _expectedLength;
private Dictionary<string, List<string>> _newHeaders;
long _downloaded = 0;
IDecompressor _decompressor = null;
bool _compressed = false;
bool sendProgressChanged;
int _chunkLength = -1;
enum ReadChunkedStates
{
ReadChunkLength,
ReadChunk,
ReadTrailingCRLF,
ReadTrailingHeaders
}
ReadChunkedStates _readChunkedState = ReadChunkedStates.ReadChunkLength;
IDownloadContentBufferAvailable _bufferAvailableHandler;
public PeekableHTTP1Response(HTTPRequest request, bool isFromCache, IDownloadContentBufferAvailable bufferAvailableHandler)
: base(request, isFromCache)
{
this._bufferAvailableHandler = bufferAvailableHandler;
}
private int _isProccessing;
public void ProcessPeekable(PeekableContentProviderStream peekable)
{
// To avoid executing ProcessPeekable in parallel on two threads, do an atomic CompareExchange and return if the old value wasn't 0.
if (Interlocked.CompareExchange(ref this._isProccessing, 1, 0) != 0)
return;
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"ProcessPeekable({this.ReadState}, {peekable.Length})", this.Context);
try
{
// The first call after setting it to PeekableReadState.WaitForContentSent is after the the client could send its content.
// This also works when "If the request did not contain an Expect header field containing the 100-continue expectation,
// the client can simply discard this interim response."
// (https://www.rfc-editor.org/rfc/rfc9110#section-15.2.1-3)
if (this._readState == PeekableReadState.WaitForContentSent)
{
this._newHeaders?.Clear();
this.Headers?.Clear();
this._readState = PeekableReadState.StatusLine;
}
// It's an unexpected network closure, except when we reading the content in the RawUnknownLength delivery mode.
if (peekable == null && ReadState != PeekableReadState.Content && this._deliveryMode != ContentDeliveryMode.RawUnknownLength)
throw new Exception("Server closed the connection unexpectedly!");
switch (ReadState)
{
case PeekableReadState.StatusLine:
if (!IsNewLinePresent(peekable))
return;
Request.Timing.StartNext(TimingEventNames.Headers);
var statusLine = HTTPResponse.ReadTo(peekable, (byte)' ');
string[] versions = statusLine.Split(new char[] { '/', '.' });
this.HTTPVersion = new Version(int.Parse(versions[1]), int.Parse(versions[2]));
int statusCode;
string statusCodeStr = NoTrimReadTo(peekable, (byte)' ', LF);
if (!int.TryParse(statusCodeStr, out statusCode))
throw new Exception($"Couldn't parse '{statusCodeStr}' as a status code!");
this.StatusCode = statusCode;
if (statusCodeStr.Length > 0 && (byte)statusCodeStr[statusCodeStr.Length - 1] != LF && (byte)statusCodeStr[statusCodeStr.Length - 1] != CR)
this.Message = ReadTo(peekable, LF);
else
{
HTTPManager.Logger.Warning(nameof(PeekableHTTP1Response), "Skipping Status Message reading!", this.Context);
this.Message = string.Empty;
}
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"HTTP/'{this.HTTPVersion}' '{this.StatusCode}' '{this.Message}'", this.Context);
if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
this._newHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
this.ReadState = PeekableReadState.Headers;
goto case PeekableReadState.Headers;
case PeekableReadState.Headers:
ProcessReadHeaders(peekable, PeekableReadState.PrepareForContent);
if (this.ReadState == PeekableReadState.PrepareForContent)
{
#if !UNITY_WEBGL || UNITY_EDITOR
// When upgraded, we don't want to read the content here, so set the state to Finished.
if (this.StatusCode == 101 && (HasHeaderWithValue("connection", "upgrade") || HasHeader("upgrade")) && this.Request?.DownloadSettings?.OnUpgraded != null)
{
HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), "Request Upgraded!", this.Context);
this.IsUpgraded = this.Request.DownloadSettings.OnUpgraded(this.Request, this, peekable);
if (this.IsUpgraded)
{
this._readState = PeekableReadState.Finished;
goto case PeekableReadState.Finished;
}
}
#endif
// If it's a 100-continue, restart reading the response after the client could send its content.
if (this.StatusCode == Continue)
{
this._readState = PeekableReadState.WaitForContentSent;
break;
}
// https://www.rfc-editor.org/rfc/rfc9110#name-informational-1xx
// A 1xx response is terminated by the end of the header section; it cannot contain content or trailers.
if ((this.StatusCode >= Continue && this.StatusCode < OK) ||
// https://www.rfc-editor.org/rfc/rfc9110#name-204-no-content
// A 204 response is terminated by the end of the header section; it cannot contain content or trailers.
this.StatusCode == NoContent ||
// https://www.rfc-editor.org/rfc/rfc9110#name-304-not-modified
// A 304 response is terminated by the end of the header section; it cannot contain content or trailers.
this.StatusCode == NotModified ||
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3
// Responses to the HEAD request method (Section 4.3.2
// of [RFC7231]) never include a message body because the associated
// response header fields (e.g., Transfer-Encoding, Content-Length,
// etc.), if present, indicate only what their values would have been if
// the request method had been GET
this.Request.MethodType == HTTPMethods.Head)
{
this._readState = PeekableReadState.Finished;
goto case PeekableReadState.Finished;
}
Request.Timing.StartNext(TimingEventNames.Response_Received);
// if not an upgraded response, or OnUpgraded returned false, go for the content too.
goto case PeekableReadState.PrepareForContent;
}
break;
case PeekableReadState.PrepareForContent:
BeginReceiveContent();
// A content-length header might come with chunked transfer-encoding too.
var contentLengthHeader = GetFirstHeaderValue("content-length");
long.TryParse(contentLengthHeader, out this._expectedLength);
if (HasHeaderWithValue("transfer-encoding", "chunked") && string.IsNullOrEmpty(contentLengthHeader))
{
this._deliveryMode = ContentDeliveryMode.Chunked;
this.ReadState = PeekableReadState.ContentSetup;
}
else
{
this._deliveryMode = ContentDeliveryMode.Raw;
this.ReadState = PeekableReadState.ContentSetup;
var contentRangeHeaders = GetHeaderValues("content-range");
if (contentLengthHeader == null && contentRangeHeaders == null)
{
this._deliveryMode = ContentDeliveryMode.RawUnknownLength;
}
else if (contentLengthHeader == null && contentRangeHeaders != null)
{
HTTPRange range = GetRange();
this._expectedLength = (range.LastBytePos - range.FirstBytePos) + 1;
}
}
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), $"PrepareForContent - delivery mode selected: {this._deliveryMode}, {this._expectedLength}!", this.Context);
CreateDownloadStream(this._bufferAvailableHandler);
string encoding = IsFromCache ? null : GetFirstHeaderValue("content-encoding");
#if !UNITY_WEBGL || UNITY_EDITOR
this._compressed = !string.IsNullOrEmpty(encoding);
// https://github.com/Benedicht/BestHTTP-Issues/issues/183
// If _decompressor is still null, remove the compressed flag and serve the content as-is.
if ((this._decompressor = DecompressorFactory.GetDecompressor(encoding, this.Context)) == null)
this._compressed = false;
#endif
this.sendProgressChanged = this.Request.DownloadSettings.OnDownloadProgress != null && this.IsSuccess;
this.ReadState = PeekableReadState.Content;
goto case PeekableReadState.Content;
case PeekableReadState.Content:
var downStream = this.DownStream;
if (downStream != null && downStream.MaxBuffered <= downStream.Length)
return;
switch (this._deliveryMode)
{
case ContentDeliveryMode.Raw: ProcessReadRaw(peekable); break;
case ContentDeliveryMode.RawUnknownLength: ProcessReadRawUnknownLength(peekable); break;
case ContentDeliveryMode.Chunked: ProcessReadChunked(peekable); break;
}
if (this.ReadState == PeekableReadState.Finished)
goto case PeekableReadState.Finished;
break;
case PeekableReadState.Finished:
//baseRequest.Timing.StartNext(TimingEventNames.Queued_For_Disptach);
break;
}
}
finally
{
Interlocked.Exchange(ref this._isProccessing, 0);
}
}
bool IsNewLinePresent(PeekableStream peekable)
{
peekable.BeginPeek();
int nextByte = peekable.PeekByte();
while (nextByte >= 0 && nextByte != 0x0A)
nextByte = peekable.PeekByte();
return nextByte == 0x0A;
}
private void ProcessReadHeaders(PeekableStream peekable, PeekableReadState targetState)
{
if (!IsNewLinePresent(peekable))
return;
do
{
string headerName = ReadTo(peekable, (byte)':', LF);
if (headerName == string.Empty)
{
this.ReadState = targetState;
if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, this._newHeaders));
return;
}
string value = ReadTo(peekable, LF);
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"Header - '{headerName}': '{value}'", this.Context);
AddHeader(headerName, value);
if (this._newHeaders != null)
{
List<string> values;
if (!this._newHeaders.TryGetValue(headerName, out values))
this._newHeaders.Add(headerName, values = new List<string>(1));
values.Add(value);
}
} while (IsNewLinePresent(peekable));
}
private void ProcessReadRawUnknownLength(PeekableStream peekable)
{
if (peekable == null)
{
if (sendProgressChanged)
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
PostProcessContent();
this.ReadState = PeekableReadState.Finished;
return;
}
while (peekable.Length > 0)
{
var buffer = BufferPool.Get(64 * 1024, true, this.Context);
var readCount = peekable.Read(buffer, 0, buffer.Length);
ProcessChunk(buffer.AsBuffer(readCount));
}
if (sendProgressChanged)
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
}
private bool TryReadChunkLength(PeekableStream peekable, out int result)
{
result = -1;
if (!IsNewLinePresent(peekable))
return false;
// Read until the end of line, then split the string so we will discard any optional chunk extensions
var buff = ReadToAsByte(peekable, LF);
if (buff == BufferSegment.Empty)
return false;
try
{
var chars = MemoryMarshal.Cast<byte, char>(buff.AsSpan());
var idx = chars.IndexOf(';');
if (idx == -1)
idx = chars.Length;
return int.TryParse(chars.Slice(0, idx), System.Globalization.NumberStyles.AllowHexSpecifier, null, out result);
}
finally
{
BufferPool.Release(buff);
}
}
void ProcessReadChunked(PeekableStream peekable)
{
switch(this._readChunkedState)
{
case ReadChunkedStates.ReadChunkLength:
this._readChunkedState = ReadChunkedStates.ReadChunkLength;
if (TryReadChunkLength(peekable, out this._chunkLength))
{
if (this._chunkLength == 0)
{
PostProcessContent();
if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
this._newHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
goto case ReadChunkedStates.ReadTrailingHeaders;
}
goto case ReadChunkedStates.ReadChunk;
}
break;
case ReadChunkedStates.ReadChunk:
this._readChunkedState = ReadChunkedStates.ReadChunk;
while (this._chunkLength > 0 && peekable.Length > 0)
{
int targetReadCount = Math.Min(Math.Min(64 * 1024, this._chunkLength), (int)peekable.Length);
var buffer = BufferPool.Get(targetReadCount, true, this.Context);
var readCount = peekable.Read(buffer, 0, targetReadCount);
if (readCount < 0)
{
BufferPool.Release(buffer);
throw ExceptionHelper.ServerClosedTCPStream();
}
this._chunkLength -= readCount;
ProcessChunk(buffer.AsBuffer(readCount));
}
if (sendProgressChanged)
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
// Every chunk data has a trailing CRLF
if (this._chunkLength == 0)
goto case ReadChunkedStates.ReadTrailingCRLF;
break;
case ReadChunkedStates.ReadTrailingCRLF:
this._readChunkedState = ReadChunkedStates.ReadTrailingCRLF;
if (IsNewLinePresent(peekable))
{
BufferPool.Release(HTTPResponse.ReadToAsByte(peekable, LF));
goto case ReadChunkedStates.ReadChunkLength;
}
break;
case ReadChunkedStates.ReadTrailingHeaders:
this._readChunkedState = ReadChunkedStates.ReadTrailingHeaders;
ProcessReadHeaders(peekable, PeekableReadState.Finished);
break;
}
}
void ProcessReadRaw(PeekableStream peekable)
{
if (this.DownStream == null)
throw new ArgumentNullException(nameof(this.DownStream));
if (peekable == null)
throw new ArgumentNullException(nameof(peekable));
while (peekable.Length > 0 && !this.DownStream.IsFull)
{
var buffer = BufferPool.Get(64 * 1024, true, this.Context);
var readCount = peekable.Read(buffer, 0, buffer.Length);
if (readCount < 0)
{
BufferPool.Release(buffer);
throw ExceptionHelper.ServerClosedTCPStream();
}
ProcessChunk(buffer.AsBuffer(readCount));
}
if (sendProgressChanged)
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
if (this._downloaded >= this._expectedLength)
{
PostProcessContent();
this.ReadState = PeekableReadState.Finished;
}
}
void ProcessChunk(BufferSegment chunk)
{
this._downloaded += chunk.Count;
if (this._compressed)
{
var (decompressed, release) = this._decompressor.Decompress(chunk, false, true, this.Context);
if (decompressed != BufferSegment.Empty)
FeedDownloadedContentChunk(decompressed);
//if (decompressed.Data != chunk.Data)
if (release)
BufferPool.Release(chunk);
}
else
{
FeedDownloadedContentChunk(chunk);
}
}
void PostProcessContent()
{
if (this._compressed)
{
var (decompressed, release) = this._decompressor.Decompress(BufferSegment.Empty, true, true, this.Context);
if (decompressed != BufferSegment.Empty)
FeedDownloadedContentChunk(decompressed);
}
FinishedContentReceiving();
if (this._decompressor != null)
{
this._decompressor.Dispose();
this._decompressor = null;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this._decompressor?.Dispose();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52e5f71e9a1d66d4ab61f2f2eca03ff0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: