using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; using static Best.HTTP.Response.HTTPStatusCodes; namespace Best.HTTP { using Best.HTTP.Caching; using Best.HTTP.Hosts.Connections; using Best.HTTP.Response; using Best.HTTP.Shared; using Best.HTTP.Shared.Extensions; using Best.HTTP.Shared.Logger; using Best.HTTP.Shared.PlatformSupport.Memory; /// /// Represents an HTTP response received from a remote server, containing information about the response status, headers, and data. /// /// /// /// The HTTPResponse class represents an HTTP response received from a remote server. It contains information about the response status, headers, and the data content. /// /// /// Key Features: /// /// /// Response Properties /// Provides access to various properties such as , , , and more, to inspect the response details. /// /// /// Data Access /// Allows access to the response data in various forms, including raw bytes, UTF-8 text, and as a for image data. /// /// /// Header Management /// Provides methods to add, retrieve, and manipulate HTTP headers associated with the response, making it easy to inspect and work with header information. /// /// /// Caching Support /// Supports response caching, enabling the storage of downloaded data in local cache storage for future use. /// /// /// Stream Management /// Manages the download process and data streaming through a () to optimize memory usage and ensure efficient handling of large response bodies. /// /// /// /// public class HTTPResponse : IDisposable { #region Public Properties /// /// Gets the version of the HTTP protocol with which the response was received. Typically, this is HTTP/1.1 for local file and cache responses, even if the original response received with a different version. /// public Version HTTPVersion { get; protected set; } /// /// Gets the HTTP status code sent from the server, indicating the outcome of the HTTP request. /// public int StatusCode { get; protected set; } /// /// Gets the message sent along with the status code from the server. This message can add some details, but it's empty for HTTP/2 responses. /// public string Message { get; protected set; } /// /// Gets a value indicating whether the response represents a successful HTTP request. Returns true if the status code is in the range of [200..300[ or 304 (Not Modified). /// public bool IsSuccess { get { return (this.StatusCode >= OK && this.StatusCode < MultipleChoices) || this.StatusCode == NotModified; } } /// /// Gets a value indicating whether the response body is read from the cache. /// public bool IsFromCache { get; internal set; } /// /// Gets the headers sent from the server as key-value pairs. You can use additional methods to manage and retrieve header information. /// /// /// The Headers property provides access to the headers sent by the server in the HTTP response. You can use the following methods to work with headers: /// /// Adds an HTTP header with the specified name and value to the response headers. /// Retrieves the list of values for a given header name as received from the server. /// Retrieves the first value for a given header name as received from the server. /// Checks if a header with the specified name and value exists in the response headers. /// Checks if a header with the specified name exists in the response headers. /// Parses the 'Content-Range' header's value and returns a object representing the byte range of the response content. /// /// public Dictionary> Headers { get; protected set; } /// /// The data that downloaded from the server. All Transfer and Content encodings decoded if any(eg. chunked, gzip, deflate). /// public byte[] Data { get { if (this._data != null) return this._data; if (this.DownStream == null) return null; CheckDisposed(); this._data = new byte[this.DownStream.Length]; try { this.DownStream.Read(this._data, 0, this._data.Length); } catch (Exception ex) { this._data = null; HTTPManager.Logger.Exception(nameof(HTTPResponse), "get_Data", ex, this.Context); } finally { this.DownStream.Dispose(); } return this._data; } } private byte[] _data; /// /// The normal HTTP protocol is upgraded to an other. /// public bool IsUpgraded { get; internal set; } /// /// Cached, converted data. /// protected string dataAsText; /// /// The data converted to an UTF8 string. /// public string DataAsText { get { if (Data == null) return string.Empty; if (!string.IsNullOrEmpty(dataAsText)) return dataAsText; CheckDisposed(); return dataAsText = Encoding.UTF8.GetString(Data, 0, Data.Length); } } /// /// Cached converted data. /// protected Texture2D texture; /// /// The data loaded to a Texture2D. /// public Texture2D DataAsTexture2D { get { if (Data == null) return null; if (texture != null) return texture; CheckDisposed(); texture = new Texture2D(1, 1, TextureFormat.RGBA32, false); texture.LoadImage(Data, true); return texture; } } /// /// Reference to the instance that contains the downloaded data. /// public DownloadContentStream DownStream { get; internal set; } /// /// IProtocol.LoggingContext implementation. /// public LoggingContext Context { get; private set; } /// /// The original request that this response is created for. /// public HTTPRequest Request { get; private set; } #endregion protected HTTPCacheContentWriter _cacheWriter; private bool _isDisposed; internal HTTPResponse(HTTPRequest request, bool isFromCache) { this.Request = request; this.IsFromCache = isFromCache; this.Context = new LoggingContext(this); this.Context.Add("BaseRequest", request.Context); } #region Header Management /// /// Adds an HTTP header with the specified name and value to the response headers. /// /// The name of the header. /// The value of the header. public void AddHeader(string name, string value) => this.Headers = this.Headers.AddHeader(name, value); /// /// Retrieves the list of values for a given header name as received from the server. /// /// The name of the header. /// /// A list of header values if the header exists and contains values; otherwise, returns null. /// public List GetHeaderValues(string name) => this.Headers.GetHeaderValues(name); /// /// Retrieves the first value for a given header name as received from the server. /// /// The name of the header. /// /// The first header value if the header exists and contains values; otherwise, returns null. /// public string GetFirstHeaderValue(string name) => this.Headers.GetFirstHeaderValue(name); /// /// Checks if a header with the specified name and value exists in the response headers. /// /// The name of the header to check. /// The value to check for in the header. /// /// true if a header with the given name and value exists in the response headers; otherwise, false. /// public bool HasHeaderWithValue(string headerName, string value) => this.Headers.HasHeaderWithValue(headerName, value); /// /// Checks if a header with the specified name exists in the response headers. /// /// The name of the header to check. /// /// true if a header with the given name exists in the response headers; otherwise, false. /// public bool HasHeader(string headerName) => this.Headers.HasHeader(headerName); /// /// Parses the 'Content-Range' header's value and returns a object representing the byte range of the response content. /// /// /// If the server ignores a byte-range-spec because it is syntactically invalid, the server SHOULD treat the request as if the invalid Range header field did not exist. /// (Normally, this means return a 200 response containing the full entity). In this case because there are no 'Content-Range' header values, this function will return null. /// /// /// A object representing the byte range of the response content, or null if no 'Content-Range' header is found. /// public HTTPRange GetRange() { var rangeHeaders = this.Headers.GetHeaderValues("content-range"); if (rangeHeaders == null) return null; // A byte-content-range-spec with a byte-range-resp-spec whose last- byte-pos value is less than its first-byte-pos value, // or whose instance-length value is less than or equal to its last-byte-pos value, is invalid. // The recipient of an invalid byte-content-range- spec MUST ignore it and any content transferred along with it. // A valid content-range sample: "bytes 500-1233/1234" var ranges = rangeHeaders[0].Split(new char[] { ' ', '-', '/' }, StringSplitOptions.RemoveEmptyEntries); // A server sending a response with status code 416 (Requested range not satisfiable) SHOULD include a Content-Range field with a byte-range-resp-spec of "*". // The instance-length specifies the current length of the selected resource. // "bytes */1234" if (ranges[1] == "*") return new HTTPRange(int.Parse(ranges[2])); return new HTTPRange(int.Parse(ranges[1]), int.Parse(ranges[2]), ranges[3] != "*" ? int.Parse(ranges[3]) : -1); } #endregion #region Static Stream Management Helper Functions internal static string ReadTo(Stream stream, byte blocker) { byte[] readBuf = BufferPool.Get(1024, true); try { int bufpos = 0; int ch = stream.ReadByte(); while (ch != blocker && ch != -1) { if (ch > 0x7f) //replaces asciitostring ch = '?'; //make buffer larger if too short if (readBuf.Length <= bufpos) BufferPool.Resize(ref readBuf, readBuf.Length * 2, true, false); if (bufpos > 0 || !char.IsWhiteSpace((char)ch)) //trimstart readBuf[bufpos++] = (byte)ch; ch = stream.ReadByte(); } while (bufpos > 0 && char.IsWhiteSpace((char)readBuf[bufpos - 1])) bufpos--; return System.Text.Encoding.UTF8.GetString(readBuf, 0, bufpos); } finally { BufferPool.Release(readBuf); } } internal static string ReadTo(Stream stream, byte blocker1, byte blocker2) { byte[] readBuf = BufferPool.Get(1024, true); try { int bufpos = 0; int ch = stream.ReadByte(); while (ch != blocker1 && ch != blocker2 && ch != -1) { if (ch > 0x7f) //replaces asciitostring ch = '?'; //make buffer larger if too short if (readBuf.Length <= bufpos) BufferPool.Resize(ref readBuf, readBuf.Length * 2, true, true); if (bufpos > 0 || !char.IsWhiteSpace((char)ch)) //trimstart readBuf[bufpos++] = (byte)ch; ch = stream.ReadByte(); } while (bufpos > 0 && char.IsWhiteSpace((char)readBuf[bufpos - 1])) bufpos--; return System.Text.Encoding.UTF8.GetString(readBuf, 0, bufpos); } finally { BufferPool.Release(readBuf); } } internal static string NoTrimReadTo(Stream stream, byte blocker1, byte blocker2) { byte[] readBuf = BufferPool.Get(1024, true); try { int bufpos = 0; int ch = stream.ReadByte(); while (ch != blocker1 && ch != blocker2 && ch != -1) { if (ch > 0x7f) //replaces asciitostring ch = '?'; //make buffer larger if too short if (readBuf.Length <= bufpos) BufferPool.Resize(ref readBuf, readBuf.Length * 2, true, true); if (bufpos > 0 || !char.IsWhiteSpace((char)ch)) //trimstart readBuf[bufpos++] = (byte)ch; ch = stream.ReadByte(); } return System.Text.Encoding.UTF8.GetString(readBuf, 0, bufpos); } finally { BufferPool.Release(readBuf); } } #endregion protected void BeginReceiveContent() { CheckDisposed(); if (!Request.DownloadSettings.DisableCache && !IsFromCache) { // If caching is enabled and the response not from cache and it's cacheble we will cache the downloaded data // by writing it to the stream returned by BeginCache _cacheWriter = HTTPManager.LocalCache?.BeginCache(Request.MethodType, Request.CurrentUri, this.StatusCode, this.Headers, this.Context); } } /// /// Add data to the fragments list. /// /// The buffer to be added. /// The position where we start copy the data. /// How many data we want to copy. protected void FeedDownloadedContentChunk(BufferSegment segment) { if (segment == BufferSegment.Empty) return; CheckDisposed(); _cacheWriter?.Write(segment); if (!this.Request.DownloadSettings.CacheOnly) this.DownStream.Write(segment); else BufferPool.Release(segment); } protected void FinishedContentReceiving() { CheckDisposed(); _cacheWriter?.Cache?.EndCache(_cacheWriter, true, this.Context); _cacheWriter = null; } protected void CreateDownloadStream(IDownloadContentBufferAvailable bufferAvailable) { if (this.DownStream != null) this.DownStream.Dispose(); this.DownStream = this.Request.DownloadSettings.DownloadStreamFactory(this.Request, this, bufferAvailable); HTTPManager.Logger.Information(this.GetType().Name, $"{nameof(DownloadContentStream)} initialized with Maximum Buffer Size: {this.DownStream.MaxBuffered:N0}", this.Context); // Send download-started event only when the final content is expected (2xx status codes). // Otherwise, for one request multiple download-started even would be trigger every time it gets redirected. if (this.StatusCode >= OK && this.StatusCode < MultipleChoices) RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadStarted)); } protected void CheckDisposed() { if (this._isDisposed) throw new ObjectDisposedException(this.GetType().Name); } /// /// IDisposable implementation. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing && !this._isDisposed) { _cacheWriter?.Cache?.EndCache(_cacheWriter, false, this.Context); _cacheWriter = null; if (this.DownStream != null && !this.DownStream.IsDetached) { this.DownStream.Dispose(); this.DownStream = null; } } this._isDisposed = true; } } }