123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- #if !UNITY_WEBGL || UNITY_EDITOR
- using System;
- using System.Collections.Generic;
- using System.Threading;
- using Best.HTTP.Shared;
- using Best.HTTP.Shared.Extensions;
- using Best.HTTP.Shared.PlatformSupport.Memory;
- using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
- using Best.HTTP.Shared.Streams;
- using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
- namespace Best.HTTP.Proxies
- {
- internal sealed class HTTPProxyResponse : IContentConsumer
- {
- public PeekableReadState ReadState
- {
- get => this._readState;
- private set
- {
- if (this._readState != value)
- HTTPManager.Logger.Information(nameof(HTTPProxyResponse), $"{this._readState} => {value}", this._parameters.context);
- this._readState = value;
- }
- }
- public int VersionMajor { get; private set; }
- public int VersionMinor { get; private set; }
- public int StatusCode { get; private set; }
- public string Message { get; private set; }
- public Dictionary<string, List<string>> Headers { get; private set; }
- public PeekableContentProviderStream ContentProvider { get; private set; }
- private PeekableReadState _readState;
- enum ContentDeliveryMode
- {
- Raw,
- RawUnknownLength,
- Chunked,
- }
- public enum PeekableReadState
- {
- StatusLine,
- Headers,
- PrepareForContent,
- ContentSetup,
- RawContent,
- Content,
- Finished
- }
- private ContentDeliveryMode _deliveryMode;
- private ProxyConnectParameters _parameters;
- private long _expectedLength;
- private BufferPoolMemoryStream _output;
- int _chunkLength = -1;
- enum ReadChunkedStates
- {
- ReadChunkLength,
- ReadChunk,
- ReadTrailingCRLF,
- ReadTrailingHeaders
- }
- ReadChunkedStates _readChunkedState = ReadChunkedStates.ReadChunkLength;
- private long _downloaded;
- public Action<ProxyConnectParameters, HTTPProxyResponse, Exception> OnFinished;
- public string DataAsText { get; private set; }
- public HTTPProxyResponse(ProxyConnectParameters parameters)
- {
- this._parameters = parameters;
- this._parameters.stream.SetTwoWayBinding(this);
- this.Headers = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
- }
- public void SetBinding(PeekableContentProviderStream contentProvider) => this.ContentProvider = contentProvider;
- public void UnsetBinding() => this.ContentProvider = null;
- public void OnConnectionClosed()
- {
- Exception error = null;
- if (this.ReadState == PeekableReadState.Content && this._deliveryMode == ContentDeliveryMode.RawUnknownLength)
- {
- PostProcessContent();
- error = new Exception($"Proxy returned with {this.StatusCode} - '{this.Message}' : \"{this.DataAsText}\"");
- }
- else
- {
- error = new Exception("Connection to the proxy closed unexpectedly!");
- }
- CallFinished(error);
- }
- public void OnError(Exception ex)
- {
- //(this._parameters.stream as IPeekableContentProvider).Consumer = null;
- this.ContentProvider.Unbind();
- CallFinished(ex);
- }
- void CallFinished(Exception error)
- {
- var callback = Interlocked.Exchange(ref this.OnFinished, null);
- callback?.Invoke(this._parameters, this, error);
- }
- public void OnContent()
- {
- switch (ReadState)
- {
- case PeekableReadState.StatusLine:
- if (!IsNewLinePresent(this.ContentProvider))
- return;
- var statusLine = HTTPResponse.ReadTo(this.ContentProvider, (byte)' ');
- string[] versions = statusLine.Split(new char[] { '/', '.' });
- this.VersionMajor = int.Parse(versions[1]);
- this.VersionMinor = int.Parse(versions[2]);
- int statusCode;
- string statusCodeStr = HTTPResponse.NoTrimReadTo(this.ContentProvider, (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 = HTTPResponse.ReadTo(this.ContentProvider, LF);
- else
- {
- HTTPManager.Logger.Warning(nameof(HTTPProxyResponse), "Skipping Status Message reading!", this._parameters.context);
- this.Message = string.Empty;
- }
- if (HTTPManager.Logger.IsDiagnostic)
- VerboseLogging($"HTTP/'{this.VersionMajor}.{this.VersionMinor}' '{this.StatusCode}' '{this.Message}'");
- this.ReadState = PeekableReadState.Headers;
- goto case PeekableReadState.Headers;
- case PeekableReadState.Headers:
- ProcessReadHeaders(this.ContentProvider, PeekableReadState.PrepareForContent);
- if (this.ReadState == PeekableReadState.PrepareForContent)
- {
- if (this.StatusCode == 200)
- {
- this.ReadState = PeekableReadState.Finished;
- goto case PeekableReadState.Finished;
- }
- // if it's an error response from the proxy, read all from the network
- goto case PeekableReadState.PrepareForContent;
- }
- break;
- case PeekableReadState.PrepareForContent:
- // A content-length header might come with chunked transfer-encoding too.
- List<string> contentLengthHeaders = GetHeaderValues("content-length");
- if (contentLengthHeaders != null)
- this._expectedLength = long.Parse(contentLengthHeaders[0]);
- if (HasHeaderWithValue("transfer-encoding", "chunked"))
- {
- this._deliveryMode = ContentDeliveryMode.Chunked;
- this.ReadState = PeekableReadState.ContentSetup;
- }
- else
- {
- this._deliveryMode = ContentDeliveryMode.Raw;
- this.ReadState = PeekableReadState.ContentSetup;
- var contentRangeHeaders = GetHeaderValues("content-range");
- if (contentLengthHeaders == null && contentRangeHeaders == null)
- {
- this._deliveryMode = ContentDeliveryMode.RawUnknownLength;
- }
- else if (contentLengthHeaders == null && contentRangeHeaders != null)
- {
- throw new NotImplementedException("ranges");
- }
- }
- this._output = new BufferPoolMemoryStream(1024);
- this.ReadState = PeekableReadState.Content;
- goto case PeekableReadState.Content;
- case PeekableReadState.Content:
- switch (this._deliveryMode)
- {
- case ContentDeliveryMode.Raw: ProcessReadRaw(this.ContentProvider); break;
- case ContentDeliveryMode.RawUnknownLength: ProcessReadRawUnknownLength(this.ContentProvider); break;
- case ContentDeliveryMode.Chunked: ProcessReadChunked(this.ContentProvider); break;
- }
- if (this.ReadState == PeekableReadState.Finished)
- goto case PeekableReadState.Finished;
- break;
- case PeekableReadState.Finished:
- //(this._parameters.stream as IPeekableContentProvider).Consumer = null;
- this.ContentProvider.Unbind();
- if (this.StatusCode == 200)
- {
- CallFinished(null);
- }
- else
- {
- CallFinished(new Exception($"Proxy returned with {this.StatusCode} - '{this.Message}' : \"{this.DataAsText}\""));
- }
- break;
- }
- }
- public List<string> GetHeaderValues(string name)
- {
- if (Headers == null)
- return null;
- List<string> values;
- if (!Headers.TryGetValue(name, out values) || values.Count == 0)
- return null;
- return values;
- }
- public string GetFirstHeaderValue(string name)
- {
- if (Headers == null)
- return null;
- List<string> values;
- if (!Headers.TryGetValue(name, out values) || values.Count == 0)
- return null;
- return values[0];
- }
- public bool HasHeaderWithValue(string headerName, string value)
- {
- var values = GetHeaderValues(headerName);
- if (values == null)
- return false;
- for (int i = 0; i < values.Count; ++i)
- if (string.Compare(values[i], value, StringComparison.OrdinalIgnoreCase) == 0)
- return true;
- return false;
- }
- public void AddHeader(string name, string value)
- {
- if (Headers == null)
- Headers = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
- List<string> values;
- if (!Headers.TryGetValue(name, out values))
- Headers.Add(name, values = new List<string>(1));
- values.Add(value);
- }
- private void VerboseLogging(string v)
- {
- HTTPManager.Logger.Verbose(nameof(HTTPProxyResponse), v, this._parameters.context);
- }
- 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 = HTTPResponse.ReadTo(peekable, (byte)':', LF);
- if (headerName == string.Empty)
- {
- this.ReadState = targetState;
- return;
- }
- string value = HTTPResponse.ReadTo(peekable, LF);
- if (HTTPManager.Logger.IsDiagnostic)
- VerboseLogging($"Header - '{headerName}': '{value}'");
- AddHeader(headerName, value);
- } while (IsNewLinePresent(peekable));
- }
- private void ProcessReadRawUnknownLength(PeekableStream peekable)
- {
- while (peekable.Length > 0)
- {
- var buffer = BufferPool.Get(64 * 1024, true, this._parameters.context);
- using var _ = new AutoReleaseBuffer(buffer);
- var readCount = peekable.Read(buffer, 0, buffer.Length);
- ProcessChunk(buffer.AsBuffer(readCount));
- }
- }
- 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
- string line = HTTPResponse.ReadTo(peekable, LF);
- string[] splits = line.Split(';');
- string num = splits[0];
- return int.TryParse(num, System.Globalization.NumberStyles.AllowHexSpecifier, null, out result);
- }
- 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();
- 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._parameters.context);
- using var _ = new AutoReleaseBuffer(buffer);
- var readCount = peekable.Read(buffer, 0, targetReadCount);
- if (readCount < 0)
- throw ExceptionHelper.ServerClosedTCPStream();
- this._chunkLength -= readCount;
- ProcessChunk(buffer.AsBuffer(readCount));
- }
- // 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))
- {
- HTTPResponse.ReadTo(peekable, LF);
- goto case ReadChunkedStates.ReadChunkLength;
- }
- break;
- case ReadChunkedStates.ReadTrailingHeaders:
- this._readChunkedState = ReadChunkedStates.ReadTrailingHeaders;
- ProcessReadHeaders(peekable, PeekableReadState.Finished);
- break;
- }
- }
- void ProcessReadRaw(PeekableStream peekable)
- {
- while (peekable.Length > 0)
- {
- var buffer = BufferPool.Get(64 * 1024, true, this._parameters.context);
- using var _ = new AutoReleaseBuffer(buffer);
- var readCount = peekable.Read(buffer, 0, buffer.Length);
- if (readCount < 0)
- throw ExceptionHelper.ServerClosedTCPStream();
- ProcessChunk(buffer.AsBuffer(readCount));
- }
- if (this._downloaded >= this._expectedLength)
- {
- PostProcessContent();
- }
- }
- void ProcessChunk(BufferSegment chunk)
- {
- this._downloaded += chunk.Count;
- this._output.Write(chunk.Data, chunk.Offset, chunk.Count);
- }
- void PostProcessContent()
- {
- this.ReadState = PeekableReadState.Finished;
- if (this._output != null)
- {
- var buff = this._output.GetBuffer();
- this.DataAsText = System.Text.Encoding.UTF8.GetString(buff, 0, (int)this._output.Length);
- this._output.Dispose();
- this._output = null;
- }
- }
- public override string ToString() => $"{StatusCode} - {Message}: \"{this.DataAsText}\"";
- }
- }
- #endif
|