HTTPResponse.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text;
  5. using UnityEngine;
  6. using static Best.HTTP.Response.HTTPStatusCodes;
  7. namespace Best.HTTP
  8. {
  9. using Best.HTTP.Caching;
  10. using Best.HTTP.Hosts.Connections;
  11. using Best.HTTP.Response;
  12. using Best.HTTP.Shared;
  13. using Best.HTTP.Shared.Extensions;
  14. using Best.HTTP.Shared.Logger;
  15. using Best.HTTP.Shared.PlatformSupport.Memory;
  16. /// <summary>
  17. /// Represents an HTTP response received from a remote server, containing information about the response status, headers, and data.
  18. /// </summary>
  19. /// <remarks>
  20. /// <para>
  21. /// The HTTPResponse class represents an HTTP response received from a remote server. It contains information about the response status, headers, and the data content.
  22. /// </para>
  23. /// <para>
  24. /// Key Features:
  25. /// <list type="bullet">
  26. /// <item>
  27. /// <term>Response Properties</term>
  28. /// <description>Provides access to various properties such as <see cref="HTTPVersion"/>, <see cref="StatusCode"/>, <see cref="Message"/>, and more, to inspect the response details.</description>
  29. /// </item>
  30. /// <item>
  31. /// <term>Data Access</term>
  32. /// <description>Allows access to the response data in various forms, including raw bytes, UTF-8 text, and as a <see cref="Texture2D"/> for image data.</description>
  33. /// </item>
  34. /// <item>
  35. /// <term>Header Management</term>
  36. /// <description>Provides methods to add, retrieve, and manipulate HTTP headers associated with the response, making it easy to inspect and work with header information.</description>
  37. /// </item>
  38. /// <item>
  39. /// <term>Caching Support</term>
  40. /// <description>Supports response caching, enabling the storage of downloaded data in local cache storage for future use.</description>
  41. /// </item>
  42. /// <item>
  43. /// <term>Stream Management</term>
  44. /// <description>Manages the download process and data streaming through a <see cref="DownloadContentStream"/> (<see cref="DownStream"/>) to optimize memory usage and ensure efficient handling of large response bodies.</description>
  45. /// </item>
  46. /// </list>
  47. /// </para>
  48. /// </remarks>
  49. public class HTTPResponse : IDisposable
  50. {
  51. #region Public Properties
  52. /// <summary>
  53. /// 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.
  54. /// </summary>
  55. public Version HTTPVersion { get; protected set; }
  56. /// <summary>
  57. /// Gets the HTTP status code sent from the server, indicating the outcome of the HTTP request.
  58. /// </summary>
  59. public int StatusCode { get; protected set; }
  60. /// <summary>
  61. /// 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.
  62. /// </summary>
  63. public string Message { get; protected set; }
  64. /// <summary>
  65. /// 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).
  66. /// </summary>
  67. public bool IsSuccess { get { return (this.StatusCode >= OK && this.StatusCode < MultipleChoices) || this.StatusCode == NotModified; } }
  68. /// <summary>
  69. /// Gets a value indicating whether the response body is read from the cache.
  70. /// </summary>
  71. public bool IsFromCache { get; internal set; }
  72. /// <summary>
  73. /// Gets the headers sent from the server as key-value pairs. You can use additional methods to manage and retrieve header information.
  74. /// </summary>
  75. /// <remarks>
  76. /// 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:
  77. /// <list type="bullet">
  78. /// <item><term><see cref="AddHeader(string, string)"/> </term><description>Adds an HTTP header with the specified name and value to the response headers.</description></item>
  79. /// <item><term><see cref="GetHeaderValues(string)"/> </term><description>Retrieves the list of values for a given header name as received from the server.</description></item>
  80. /// <item><term><see cref="GetFirstHeaderValue(string)"/> </term><description>Retrieves the first value for a given header name as received from the server.</description></item>
  81. /// <item><term><see cref="HasHeaderWithValue(string, string)"/> </term><description>Checks if a header with the specified name and value exists in the response headers.</description></item>
  82. /// <item><term><see cref="HasHeader(string)"/> </term><description>Checks if a header with the specified name exists in the response headers.</description></item>
  83. /// <item><term><see cref="GetRange()"/></term><description>Parses the 'Content-Range' header's value and returns a <see cref="HTTPRange"/> object representing the byte range of the response content.</description></item>
  84. /// </list>
  85. /// </remarks>
  86. public Dictionary<string, List<string>> Headers { get; protected set; }
  87. /// <summary>
  88. /// The data that downloaded from the server. All Transfer and Content encodings decoded if any(eg. chunked, gzip, deflate).
  89. /// </summary>
  90. public byte[] Data
  91. {
  92. get
  93. {
  94. if (this._data != null)
  95. return this._data;
  96. if (this.DownStream == null)
  97. return null;
  98. CheckDisposed();
  99. this._data = new byte[this.DownStream.Length];
  100. try
  101. {
  102. this.DownStream.Read(this._data, 0, this._data.Length);
  103. }
  104. catch (Exception ex)
  105. {
  106. this._data = null;
  107. HTTPManager.Logger.Exception(nameof(HTTPResponse), "get_Data", ex, this.Context);
  108. }
  109. finally
  110. {
  111. this.DownStream.Dispose();
  112. }
  113. return this._data;
  114. }
  115. }
  116. private byte[] _data;
  117. /// <summary>
  118. /// The normal HTTP protocol is upgraded to an other.
  119. /// </summary>
  120. public bool IsUpgraded { get; internal set; }
  121. /// <summary>
  122. /// Cached, converted data.
  123. /// </summary>
  124. protected string dataAsText;
  125. /// <summary>
  126. /// The data converted to an UTF8 string.
  127. /// </summary>
  128. public string DataAsText
  129. {
  130. get
  131. {
  132. if (Data == null)
  133. return string.Empty;
  134. if (!string.IsNullOrEmpty(dataAsText))
  135. return dataAsText;
  136. CheckDisposed();
  137. return dataAsText = Encoding.UTF8.GetString(Data, 0, Data.Length);
  138. }
  139. }
  140. /// <summary>
  141. /// Cached converted data.
  142. /// </summary>
  143. protected Texture2D texture;
  144. /// <summary>
  145. /// The data loaded to a Texture2D.
  146. /// </summary>
  147. public Texture2D DataAsTexture2D
  148. {
  149. get
  150. {
  151. if (Data == null)
  152. return null;
  153. if (texture != null)
  154. return texture;
  155. CheckDisposed();
  156. texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
  157. texture.LoadImage(Data, true);
  158. return texture;
  159. }
  160. }
  161. /// <summary>
  162. /// Reference to the <see cref="DownloadContentStream"/> instance that contains the downloaded data.
  163. /// </summary>
  164. public DownloadContentStream DownStream { get; internal set; }
  165. /// <summary>
  166. /// IProtocol.LoggingContext implementation.
  167. /// </summary>
  168. public LoggingContext Context { get; private set; }
  169. /// <summary>
  170. /// The original request that this response is created for.
  171. /// </summary>
  172. public HTTPRequest Request { get; private set; }
  173. #endregion
  174. protected HTTPCacheContentWriter _cacheWriter;
  175. private bool _isDisposed;
  176. internal HTTPResponse(HTTPRequest request, bool isFromCache)
  177. {
  178. this.Request = request;
  179. this.IsFromCache = isFromCache;
  180. this.Context = new LoggingContext(this);
  181. this.Context.Add("BaseRequest", request.Context);
  182. }
  183. #region Header Management
  184. /// <summary>
  185. /// Adds an HTTP header with the specified name and value to the response headers.
  186. /// </summary>
  187. /// <param name="name">The name of the header.</param>
  188. /// <param name="value">The value of the header.</param>
  189. public void AddHeader(string name, string value) => this.Headers = this.Headers.AddHeader(name, value);
  190. /// <summary>
  191. /// Retrieves the list of values for a given header name as received from the server.
  192. /// </summary>
  193. /// <param name="name">The name of the header.</param>
  194. /// <returns>
  195. /// A list of header values if the header exists and contains values; otherwise, returns <c>null</c>.
  196. /// </returns>
  197. public List<string> GetHeaderValues(string name) => this.Headers.GetHeaderValues(name);
  198. /// <summary>
  199. /// Retrieves the first value for a given header name as received from the server.
  200. /// </summary>
  201. /// <param name="name">The name of the header.</param>
  202. /// <returns>
  203. /// The first header value if the header exists and contains values; otherwise, returns <c>null</c>.
  204. /// </returns>
  205. public string GetFirstHeaderValue(string name) => this.Headers.GetFirstHeaderValue(name);
  206. /// <summary>
  207. /// Checks if a header with the specified name and value exists in the response headers.
  208. /// </summary>
  209. /// <param name="headerName">The name of the header to check.</param>
  210. /// <param name="value">The value to check for in the header.</param>
  211. /// <returns>
  212. /// <c>true</c> if a header with the given name and value exists in the response headers; otherwise, <c>false</c>.
  213. /// </returns>
  214. public bool HasHeaderWithValue(string headerName, string value) => this.Headers.HasHeaderWithValue(headerName, value);
  215. /// <summary>
  216. /// Checks if a header with the specified name exists in the response headers.
  217. /// </summary>
  218. /// <param name="headerName">The name of the header to check.</param>
  219. /// <returns>
  220. /// <c>true</c> if a header with the given name exists in the response headers; otherwise, <c>false</c>.
  221. /// </returns>
  222. public bool HasHeader(string headerName) => this.Headers.HasHeader(headerName);
  223. /// <summary>
  224. /// Parses the <c>'Content-Range'</c> header's value and returns a <see cref="HTTPRange"/> object representing the byte range of the response content.
  225. /// </summary>
  226. /// <remarks>
  227. /// 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.
  228. /// (Normally, this means return a 200 response containing the full entity). In this case because there are no <c>'Content-Range'</c> header values, this function will return <c>null</c>.
  229. /// </remarks>
  230. /// <returns>
  231. /// A <see cref="HTTPRange"/> object representing the byte range of the response content, or <c>null</c> if no '<c>Content-Range</c>' header is found.
  232. /// </returns>
  233. public HTTPRange GetRange()
  234. {
  235. var rangeHeaders = this.Headers.GetHeaderValues("content-range");
  236. if (rangeHeaders == null)
  237. return null;
  238. // A byte-content-range-spec with a byte-range-resp-spec whose last- byte-pos value is less than its first-byte-pos value,
  239. // or whose instance-length value is less than or equal to its last-byte-pos value, is invalid.
  240. // The recipient of an invalid byte-content-range- spec MUST ignore it and any content transferred along with it.
  241. // A valid content-range sample: "bytes 500-1233/1234"
  242. var ranges = rangeHeaders[0].Split(new char[] { ' ', '-', '/' }, StringSplitOptions.RemoveEmptyEntries);
  243. // 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 "*".
  244. // The instance-length specifies the current length of the selected resource.
  245. // "bytes */1234"
  246. if (ranges[1] == "*")
  247. return new HTTPRange(int.Parse(ranges[2]));
  248. return new HTTPRange(int.Parse(ranges[1]), int.Parse(ranges[2]), ranges[3] != "*" ? int.Parse(ranges[3]) : -1);
  249. }
  250. #endregion
  251. #region Static Stream Management Helper Functions
  252. internal static string ReadTo(Stream stream, byte blocker)
  253. {
  254. byte[] readBuf = BufferPool.Get(1024, true);
  255. try
  256. {
  257. int bufpos = 0;
  258. int ch = stream.ReadByte();
  259. while (ch != blocker && ch != -1)
  260. {
  261. if (ch > 0x7f) //replaces asciitostring
  262. ch = '?';
  263. //make buffer larger if too short
  264. if (readBuf.Length <= bufpos)
  265. BufferPool.Resize(ref readBuf, readBuf.Length * 2, true, false);
  266. if (bufpos > 0 || !char.IsWhiteSpace((char)ch)) //trimstart
  267. readBuf[bufpos++] = (byte)ch;
  268. ch = stream.ReadByte();
  269. }
  270. while (bufpos > 0 && char.IsWhiteSpace((char)readBuf[bufpos - 1]))
  271. bufpos--;
  272. return System.Text.Encoding.UTF8.GetString(readBuf, 0, bufpos);
  273. }
  274. finally
  275. {
  276. BufferPool.Release(readBuf);
  277. }
  278. }
  279. internal static string ReadTo(Stream stream, byte blocker1, byte blocker2)
  280. {
  281. byte[] readBuf = BufferPool.Get(1024, true);
  282. try
  283. {
  284. int bufpos = 0;
  285. int ch = stream.ReadByte();
  286. while (ch != blocker1 && ch != blocker2 && ch != -1)
  287. {
  288. if (ch > 0x7f) //replaces asciitostring
  289. ch = '?';
  290. //make buffer larger if too short
  291. if (readBuf.Length <= bufpos)
  292. BufferPool.Resize(ref readBuf, readBuf.Length * 2, true, true);
  293. if (bufpos > 0 || !char.IsWhiteSpace((char)ch)) //trimstart
  294. readBuf[bufpos++] = (byte)ch;
  295. ch = stream.ReadByte();
  296. }
  297. while (bufpos > 0 && char.IsWhiteSpace((char)readBuf[bufpos - 1]))
  298. bufpos--;
  299. return System.Text.Encoding.UTF8.GetString(readBuf, 0, bufpos);
  300. }
  301. finally
  302. {
  303. BufferPool.Release(readBuf);
  304. }
  305. }
  306. internal static string NoTrimReadTo(Stream stream, byte blocker1, byte blocker2)
  307. {
  308. byte[] readBuf = BufferPool.Get(1024, true);
  309. try
  310. {
  311. int bufpos = 0;
  312. int ch = stream.ReadByte();
  313. while (ch != blocker1 && ch != blocker2 && ch != -1)
  314. {
  315. if (ch > 0x7f) //replaces asciitostring
  316. ch = '?';
  317. //make buffer larger if too short
  318. if (readBuf.Length <= bufpos)
  319. BufferPool.Resize(ref readBuf, readBuf.Length * 2, true, true);
  320. if (bufpos > 0 || !char.IsWhiteSpace((char)ch)) //trimstart
  321. readBuf[bufpos++] = (byte)ch;
  322. ch = stream.ReadByte();
  323. }
  324. return System.Text.Encoding.UTF8.GetString(readBuf, 0, bufpos);
  325. }
  326. finally
  327. {
  328. BufferPool.Release(readBuf);
  329. }
  330. }
  331. #endregion
  332. protected void BeginReceiveContent()
  333. {
  334. CheckDisposed();
  335. if (!Request.DownloadSettings.DisableCache && !IsFromCache)
  336. {
  337. // If caching is enabled and the response not from cache and it's cacheble we will cache the downloaded data
  338. // by writing it to the stream returned by BeginCache
  339. _cacheWriter = HTTPManager.LocalCache?.BeginCache(Request.MethodType, Request.CurrentUri, this.StatusCode, this.Headers, this.Context);
  340. }
  341. }
  342. /// <summary>
  343. /// Add data to the fragments list.
  344. /// </summary>
  345. /// <param name="buffer">The buffer to be added.</param>
  346. /// <param name="pos">The position where we start copy the data.</param>
  347. /// <param name="length">How many data we want to copy.</param>
  348. protected void FeedDownloadedContentChunk(BufferSegment segment)
  349. {
  350. if (segment == BufferSegment.Empty)
  351. return;
  352. CheckDisposed();
  353. _cacheWriter?.Write(segment);
  354. if (!this.Request.DownloadSettings.CacheOnly)
  355. this.DownStream.Write(segment);
  356. else
  357. BufferPool.Release(segment);
  358. }
  359. protected void FinishedContentReceiving()
  360. {
  361. CheckDisposed();
  362. _cacheWriter?.Cache?.EndCache(_cacheWriter, true, this.Context);
  363. _cacheWriter = null;
  364. }
  365. protected void CreateDownloadStream(IDownloadContentBufferAvailable bufferAvailable)
  366. {
  367. if (this.DownStream != null)
  368. this.DownStream.Dispose();
  369. this.DownStream = this.Request.DownloadSettings.DownloadStreamFactory(this.Request, this, bufferAvailable);
  370. HTTPManager.Logger.Information(this.GetType().Name, $"{nameof(DownloadContentStream)} initialized with Maximum Buffer Size: {this.DownStream.MaxBuffered:N0}", this.Context);
  371. // Send download-started event only when the final content is expected (2xx status codes).
  372. // Otherwise, for one request multiple download-started even would be trigger every time it gets redirected.
  373. if (this.StatusCode >= OK && this.StatusCode < MultipleChoices)
  374. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadStarted));
  375. }
  376. protected void CheckDisposed()
  377. {
  378. if (this._isDisposed)
  379. throw new ObjectDisposedException(this.GetType().Name);
  380. }
  381. /// <summary>
  382. /// IDisposable implementation.
  383. /// </summary>
  384. public void Dispose()
  385. {
  386. Dispose(true);
  387. GC.SuppressFinalize(this);
  388. }
  389. protected virtual void Dispose(bool disposing)
  390. {
  391. if (disposing && !this._isDisposed)
  392. {
  393. _cacheWriter?.Cache?.EndCache(_cacheWriter, false, this.Context);
  394. _cacheWriter = null;
  395. if (this.DownStream != null && !this.DownStream.IsDetached)
  396. {
  397. this.DownStream.Dispose();
  398. this.DownStream = null;
  399. }
  400. }
  401. this._isDisposed = true;
  402. }
  403. }
  404. }