| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237 |
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Text;
- #if !NETFX_CORE || UNITY_EDITOR
- using System.Net.Sockets;
- #endif
- using UnityEngine;
- namespace BestHTTP
- {
- #if !BESTHTTP_DISABLE_CACHING
- using BestHTTP.Caching;
- #endif
- using BestHTTP.Extensions;
- #if !BESTHTTP_DISABLE_COOKIES
- using BestHTTP.Cookies;
- #endif
-
- using System.Threading;
- using BestHTTP.Core;
- using BestHTTP.PlatformSupport.Memory;
- using BestHTTP.Logger;
- using BestHTTP.Timings;
- public class HTTPResponse : IDisposable
- {
- internal const byte CR = 13;
- internal const byte LF = 10;
- /// <summary>
- /// Minimum size of the read buffer.
- /// </summary>
- public static int MinReadBufferSize = 16 * 1024;
- #region Public Properties
- public int VersionMajor { get; protected set; }
- public int VersionMinor { get; protected set; }
- /// <summary>
- /// The status code that sent from the server.
- /// </summary>
- public int StatusCode { get; protected set; }
- /// <summary>
- /// Returns true if the status code is in the range of [200..300[ or 304 (Not Modified)
- /// </summary>
- public bool IsSuccess { get { return (this.StatusCode >= 200 && this.StatusCode < 300) || this.StatusCode == 304; } }
- /// <summary>
- /// The message that sent along with the StatusCode from the server. You can check it for errors from the server.
- /// </summary>
- public string Message { get; protected set; }
- /// <summary>
- /// True if it's a streamed response.
- /// </summary>
- public bool IsStreamed { get; protected set; }
-
- #if !BESTHTTP_DISABLE_CACHING
- /// <summary>
- /// Indicates that the response body is read from the cache.
- /// </summary>
- public bool IsFromCache { get; internal set; }
- /// <summary>
- /// Provides information about the file used for caching the request.
- /// </summary>
- public HTTPCacheFileInfo CacheFileInfo { get; internal set; }
- /// <summary>
- /// Determines if this response is only stored to cache.
- /// If both IsCacheOnly and IsStreamed are true, OnStreamingData isn't called.
- /// </summary>
- public bool IsCacheOnly { get; private set; }
- #endif
- /// <summary>
- /// True, if this is a response for a HTTPProxy request.
- /// </summary>
- public bool IsProxyResponse { get; private set; }
- /// <summary>
- /// The headers that sent from the server.
- /// </summary>
- public Dictionary<string, List<string>> Headers { get; protected set; }
- /// <summary>
- /// The data that downloaded from the server. All Transfer and Content encodings decoded if any(eg. chunked, gzip, deflate).
- /// </summary>
- public byte[] Data { get; internal set; }
- /// <summary>
- /// The normal HTTP protocol is upgraded to an other.
- /// </summary>
- public bool IsUpgraded { get; protected set; }
- #if !BESTHTTP_DISABLE_COOKIES
- /// <summary>
- /// The cookies that the server sent to the client.
- /// </summary>
- public List<Cookie> Cookies { get; internal set; }
- #endif
- /// <summary>
- /// Cached, converted data.
- /// </summary>
- protected string dataAsText;
- /// <summary>
- /// The data converted to an UTF8 string.
- /// </summary>
- public string DataAsText
- {
- get
- {
- if (Data == null)
- return string.Empty;
- if (!string.IsNullOrEmpty(dataAsText))
- return dataAsText;
- return dataAsText = Encoding.UTF8.GetString(Data, 0, Data.Length);
- }
- }
- /// <summary>
- /// Cached converted data.
- /// </summary>
- protected Texture2D texture;
- /// <summary>
- /// The data loaded to a Texture2D.
- /// </summary>
- public Texture2D DataAsTexture2D
- {
- get
- {
- if (Data == null)
- return null;
- if (texture != null)
- return texture;
- texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
- texture.LoadImage(Data, true);
- return texture;
- }
- }
- /// <summary>
- /// True if the connection's stream will be closed manually. Used in custom protocols (WebSocket, EventSource).
- /// </summary>
- public bool IsClosedManually { get; protected set; }
- /// <summary>
- /// IProtocol.LoggingContext implementation.
- /// </summary>
- public LoggingContext Context { get; private set; }
- /// <summary>
- /// Count of streaming data fragments sitting in the HTTPManager's request event queue.
- /// </summary>
- #if UNITY_EDITOR
- [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
- #endif
- internal long UnprocessedFragments;
- #endregion
- #region Internal Fields
- internal HTTPRequest baseRequest;
- #endregion
- #region Protected Properties And Fields
- protected Stream Stream;
- protected byte[] fragmentBuffer;
- protected int fragmentBufferDataLength;
- #if !BESTHTTP_DISABLE_CACHING
- protected Stream cacheStream;
- #endif
- protected int allFragmentSize;
- #endregion
- protected HTTPResponse(HTTPRequest request, bool isFromCache)
- {
- this.baseRequest = request;
- #if !BESTHTTP_DISABLE_CACHING
- this.IsFromCache = isFromCache;
- #endif
- this.Context = new LoggingContext(this);
- this.Context.Add("BaseRequest", request.Context);
- this.Context.Add("IsFromCache", isFromCache);
- }
- public HTTPResponse(HTTPRequest request, Stream stream, bool isStreamed, bool isFromCache, bool isProxyResponse = false)
- {
- this.baseRequest = request;
- this.Stream = stream;
- this.IsStreamed = isStreamed;
- #if !BESTHTTP_DISABLE_CACHING
- this.IsFromCache = isFromCache;
- this.IsCacheOnly = request.CacheOnly;
- #endif
- this.IsProxyResponse = isProxyResponse;
- this.IsClosedManually = false;
- this.Context = new LoggingContext(this);
- this.Context.Add("BaseRequest", request.GetHashCode());
- this.Context.Add("IsStreamed", isStreamed);
- this.Context.Add("IsFromCache", isFromCache);
- }
- public bool Receive(long forceReadRawContentLength = -1, bool readPayloadData = true, bool sendUpgradedEvent = true)
- {
- if (this.baseRequest.IsCancellationRequested)
- return false;
- string statusLine = string.Empty;
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("Receive. forceReadRawContentLength: '{0:N0}', readPayloadData: '{1:N0}'", forceReadRawContentLength, readPayloadData));
- // On WP platform we aren't able to determined sure enough whether the tcp connection is closed or not.
- // So if we get an exception here, we need to recreate the connection.
- try
- {
- // Read out 'HTTP/1.1' from the "HTTP/1.1 {StatusCode} {Message}"
- statusLine = ReadTo(Stream, (byte)' ');
- }
- catch
- {
- if (baseRequest.IsCancellationRequested)
- return false;
- if (baseRequest.Retries >= baseRequest.MaxRetries)
- {
- HTTPManager.Logger.Warning("HTTPResponse", "Failed to read Status Line! Retry is enabled, returning with false.", this.Context, this.baseRequest.Context);
- return false;
- }
- HTTPManager.Logger.Warning("HTTPResponse", "Failed to read Status Line! Retry is disabled, re-throwing exception.", this.Context, this.baseRequest.Context);
- throw;
- }
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("Status Line: '{0}'", statusLine));
- if (string.IsNullOrEmpty(statusLine))
- {
- if (baseRequest.Retries >= baseRequest.MaxRetries)
- return false;
- throw new Exception("Network error! TCP Connection got closed before receiving any data!");
- }
- if (!this.IsProxyResponse)
- baseRequest.Timing.Add(TimingEventNames.Waiting_TTFB);
- string[] versions = statusLine.Split(new char[] { '/', '.' });
- this.VersionMajor = int.Parse(versions[1]);
- this.VersionMinor = int.Parse(versions[2]);
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("HTTP Version: '{0}.{1}'", this.VersionMajor.ToString(), this.VersionMinor.ToString()));
- int statusCode;
- string statusCodeStr = NoTrimReadTo(Stream, (byte)' ', LF);
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("Status Code: '{0}'", statusCodeStr));
- if (baseRequest.Retries >= baseRequest.MaxRetries)
- statusCode = int.Parse(statusCodeStr);
- else if (!int.TryParse(statusCodeStr, out statusCode))
- return false;
- this.StatusCode = statusCode;
- if (statusCodeStr.Length > 0 && (byte)statusCodeStr[statusCodeStr.Length - 1] != LF && (byte)statusCodeStr[statusCodeStr.Length - 1] != CR)
- {
- this.Message = ReadTo(Stream, LF);
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("Status Message: '{0}'", this.Message));
- }
- else
- {
- HTTPManager.Logger.Warning("HTTPResponse", "Skipping Status Message reading!", this.Context, this.baseRequest.Context);
- this.Message = string.Empty;
- }
- //Read Headers
- ReadHeaders(Stream);
- if (!this.IsProxyResponse)
- baseRequest.Timing.Add(TimingEventNames.Headers);
- IsUpgraded = StatusCode == 101 && (HasHeaderWithValue("connection", "upgrade") || HasHeader("upgrade"));
- if (IsUpgraded)
- {
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging("Request Upgraded!");
- RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.baseRequest, RequestEvents.Upgraded));
- }
- if (!readPayloadData)
- return true;
- if (this.StatusCode == 200 && this.IsProxyResponse)
- return true;
- return ReadPayload(forceReadRawContentLength);
- }
- protected bool ReadPayload(long forceReadRawContentLength)
- {
- // Reading from an already unpacked stream (eq. From a file cache or all responses under webgl)
- if (forceReadRawContentLength != -1)
- {
- ReadRaw(Stream, forceReadRawContentLength);
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging("ReadPayload Finished!");
- return true;
- }
- // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
- // 1.Any response message which "MUST NOT" include a message-body (such as the 1xx, 204, and 304 responses and any response to a HEAD request)
- // is always terminated by the first empty line after the header fields, regardless of the entity-header fields present in the message.
- if ((StatusCode >= 100 && StatusCode < 200) || StatusCode == 204 || StatusCode == 304 || baseRequest.MethodType == HTTPMethods.Head)
- return true;
- #if (!UNITY_WEBGL || UNITY_EDITOR)
- if (HasHeaderWithValue("transfer-encoding", "chunked"))
- ReadChunked(Stream);
- else
- #endif
- {
- // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
- // Case 3 in the above link.
- List<string> contentLengthHeaders = GetHeaderValues("content-length");
- var contentRangeHeaders = GetHeaderValues("content-range");
- if (contentLengthHeaders != null && contentRangeHeaders == null)
- ReadRaw(Stream, long.Parse(contentLengthHeaders[0]));
- else if (contentRangeHeaders != null)
- {
- if (contentLengthHeaders != null)
- ReadRaw(Stream, long.Parse(contentLengthHeaders[0]));
- else
- {
- HTTPRange range = GetRange();
- ReadRaw(Stream, (range.LastBytePos - range.FirstBytePos) + 1);
- }
- }
- else
- ReadUnknownSize(Stream);
- }
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging("ReadPayload Finished!");
- return true;
- }
- #region Header Management
- protected void ReadHeaders(Stream stream)
- {
- var newHeaders = this.baseRequest.OnHeadersReceived != null ? new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase) : null;
- string headerName = ReadTo(stream, (byte)':', LF)/*.Trim()*/;
- while (headerName != string.Empty)
- {
- string value = ReadTo(stream, LF);
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("Header - '{0}': '{1}'", headerName, value));
- AddHeader(headerName, value);
- if (newHeaders != null)
- {
- List<string> values;
- if (!newHeaders.TryGetValue(headerName, out values))
- newHeaders.Add(headerName, values = new List<string>(1));
- values.Add(value);
- }
- headerName = ReadTo(stream, (byte)':', LF);
- }
- if (this.baseRequest.OnHeadersReceived != null)
- RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.baseRequest, newHeaders));
- }
- 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);
- bool isFromCache = false;
- #if !BESTHTTP_DISABLE_CACHING
- isFromCache = this.IsFromCache;
- #endif
- if (!isFromCache && name.Equals("alt-svc", StringComparison.Ordinal))
- PluginEventHelper.EnqueuePluginEvent(new PluginEventInfo(PluginEvents.AltSvcHeader, new AltSvcEventInfo(this.baseRequest.CurrentUri.Host, this)));
- }
- /// <summary>
- /// Returns the list of values that received from the server for the given header name.
- /// </summary>
- /// <param name="name">Name of the header</param>
- /// <returns>If no header found with the given name or there are no values in the list (eg. Count == 0) returns null.</returns>
- 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;
- }
- /// <summary>
- /// Returns the first value in the header list or null if there are no header or value.
- /// </summary>
- /// <param name="name">Name of the header</param>
- /// <returns>If no header found with the given name or there are no values in the list (eg. Count == 0) returns null.</returns>
- 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];
- }
- /// <summary>
- /// Checks if there is a header with the given name and value.
- /// </summary>
- /// <param name="headerName">Name of the header.</param>
- /// <param name="value"></param>
- /// <returns>Returns true if there is a header with the given name and value.</returns>
- 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;
- }
- /// <summary>
- /// Checks if there is a header with the given name.
- /// </summary>
- /// <param name="headerName">Name of the header.</param>
- /// <returns>Returns true if there is a header with the given name.</returns>
- public bool HasHeader(string headerName)
- {
- var values = GetHeaderValues(headerName);
- if (values == null)
- return false;
- return true;
- }
- /// <summary>
- /// Parses the 'Content-Range' header's value and returns a HTTPRange object.
- /// </summary>
- /// <remarks>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 of there are no 'Content-Range' header, this function will return null!</remarks>
- /// <returns>Returns null if no 'Content-Range' header found.</returns>
- public HTTPRange GetRange()
- {
- var rangeHeaders = 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
- #region Read Chunked Body
- protected int ReadChunkLength(Stream stream)
- {
- // Read until the end of line, then split the string so we will discard any optional chunk extensions
- string line = ReadTo(stream, LF);
- string[] splits = line.Split(';');
- string num = splits[0];
- int result;
- if (int.TryParse(num, System.Globalization.NumberStyles.AllowHexSpecifier, null, out result))
- return result;
- throw new Exception(string.Format("Can't parse '{0}' as a hex number!", num));
- }
- // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
- protected void ReadChunked(Stream stream)
- {
- BeginReceiveStreamFragments();
- string contentLengthHeader = GetFirstHeaderValue("Content-Length");
- bool hasContentLengthHeader = !string.IsNullOrEmpty(contentLengthHeader);
- int realLength = 0;
- if (hasContentLengthHeader)
- hasContentLengthHeader = int.TryParse(contentLengthHeader, out realLength);
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("ReadChunked - hasContentLengthHeader: {0}, contentLengthHeader: {1} realLength: {2:N0}", hasContentLengthHeader.ToString(), contentLengthHeader, realLength));
- using (var output = new BufferPoolMemoryStream())
- {
- int chunkLength = ReadChunkLength(stream);
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("chunkLength: {0:N0}", chunkLength));
- byte[] buffer = baseRequest.ReadBufferSizeOverride > 0 ? BufferPool.Get(baseRequest.ReadBufferSizeOverride, false) : BufferPool.Get(MinReadBufferSize, true);
- // wrap buffer in a PooledBuffer to release it back to the pool when leaving the current block.
- using (var _ = new PooledBuffer(buffer))
- {
- // Progress report:
- long Downloaded = 0;
- long DownloadLength = hasContentLengthHeader ? realLength : chunkLength;
- bool sendProgressChanged = this.baseRequest.OnDownloadProgress != null && (this.IsSuccess
- #if !BESTHTTP_DISABLE_CACHING
- || this.IsFromCache
- #endif
- );
- if (sendProgressChanged)
- RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.baseRequest, RequestEvents.DownloadProgress, Downloaded, DownloadLength));
- string encoding =
- #if !BESTHTTP_DISABLE_CACHING
- IsFromCache ? null :
- #endif
- GetFirstHeaderValue("content-encoding");
- bool compressed = !string.IsNullOrEmpty(encoding);
- Decompression.IDecompressor decompressor = baseRequest.UseStreaming ? Decompression.DecompressorFactory.GetDecompressor(encoding, this.Context) : null;
- while (chunkLength != 0)
- {
- if (this.baseRequest.IsCancellationRequested)
- return;
- int totalBytes = 0;
- // Fill up the buffer
- do
- {
- int tryToReadCount = (int)Math.Min(chunkLength - totalBytes, buffer.Length);
- int bytes = stream.Read(buffer, 0, tryToReadCount);
- if (bytes <= 0)
- throw ExceptionHelper.ServerClosedTCPStream();
- // Progress report:
- // Placing reporting inside this cycle will report progress much more frequent
- Downloaded += bytes;
- if (sendProgressChanged)
- RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.baseRequest, RequestEvents.DownloadProgress, Downloaded, DownloadLength));
- if (baseRequest.UseStreaming)
- {
- if (compressed)
- {
- var decompressed = decompressor.Decompress(buffer, 0, bytes, false, true);
- if (decompressed.Data != null)
- FeedStreamFragment(decompressed.Data, 0, decompressed.Length);
- }
- else
- FeedStreamFragment(buffer, 0, bytes);
- }
- else
- output.Write(buffer, 0, bytes);
- totalBytes += bytes;
- } while (totalBytes < chunkLength);
- // Every chunk data has a trailing CRLF
- ReadTo(stream, LF);
- // read the next chunk's length
- chunkLength = ReadChunkLength(stream);
- if (!hasContentLengthHeader)
- DownloadLength += chunkLength;
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("chunkLength: {0:N0}", chunkLength));
- }
- //BufferPool.Release(buffer);
- if (baseRequest.UseStreaming)
- {
- if (compressed)
- {
- var decompressed = decompressor.Decompress(null, 0, 0, true, true);
- if (decompressed.Data != null)
- FeedStreamFragment(decompressed.Data, 0, decompressed.Length);
- }
- FlushRemainingFragmentBuffer();
- }
- // Read the trailing headers or the CRLF
- ReadHeaders(stream);
- // HTTP servers sometimes use compression (gzip) or deflate methods to optimize transmission.
- // How both chunked and gzip encoding interact is dictated by the two-staged encoding of HTTP:
- // first the content stream is encoded as (Content-Encoding: gzip), after which the resulting byte stream is encoded for transfer using another encoder (Transfer-Encoding: chunked).
- // This means that in case both compression and chunked encoding are enabled, the chunk encoding itself is not compressed, and the data in each chunk should not be compressed individually.
- // The remote endpoint can decode the incoming stream by first decoding it with the Transfer-Encoding, followed by the specified Content-Encoding.
- // It would be a better implementation when the chunk would be decododed on-the-fly. Becouse now the whole stream must be downloaded, and then decoded. It needs more memory.
- if (!baseRequest.UseStreaming)
- this.Data = DecodeStream(output);
- if (decompressor != null)
- decompressor.Dispose();
- }
- }
- }
- #endregion
- #region Read Raw Body
- // No transfer-encoding just raw bytes.
- internal void ReadRaw(Stream stream, long contentLength)
- {
- BeginReceiveStreamFragments();
- // Progress report:
- long downloaded = 0;
- long downloadLength = contentLength;
- bool sendProgressChanged = this.baseRequest.OnDownloadProgress != null && (this.IsSuccess
- #if !BESTHTTP_DISABLE_CACHING
- || this.IsFromCache
- #endif
- );
- if (sendProgressChanged)
- RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.baseRequest, RequestEvents.DownloadProgress, downloaded, downloadLength));
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("ReadRaw - contentLength: {0:N0}", contentLength));
- string encoding =
- #if !BESTHTTP_DISABLE_CACHING
- IsFromCache ? null :
- #endif
- GetFirstHeaderValue("content-encoding");
- bool compressed = !string.IsNullOrEmpty(encoding);
- Decompression.IDecompressor decompressor = baseRequest.UseStreaming ? Decompression.DecompressorFactory.GetDecompressor(encoding, this.Context) : null;
- if (!baseRequest.UseStreaming && contentLength > 2147483646)
- {
- throw new OverflowException("You have to use STREAMING to download files bigger than 2GB!");
- }
- using (var output = new BufferPoolMemoryStream(baseRequest.UseStreaming ? 0 : (int)contentLength))
- {
- // Because of the last parameter, buffer's size can be larger than the requested but there's no reason to use
- // an exact sized one if there's an larger one available in the pool. Later we will use the whole buffer.
- byte[] buffer = baseRequest.ReadBufferSizeOverride > 0 ? BufferPool.Get(baseRequest.ReadBufferSizeOverride, false) : BufferPool.Get(MinReadBufferSize, true);
- // wrap buffer in a PooledBuffer to release it back to the pool when leaving the current block.
- using (var _ = new PooledBuffer(buffer))
- {
- int readBytes = 0;
- while (contentLength > 0)
- {
- if (this.baseRequest.IsCancellationRequested)
- return;
- readBytes = 0;
- do
- {
- // tryToReadCount contain how much bytes we want to read in once. We try to read the buffer fully in once,
- // but with a limit of the remaining contentLength.
- int tryToReadCount = (int)Math.Min(Math.Min(int.MaxValue, contentLength), buffer.Length - readBytes);
- int bytes = stream.Read(buffer, readBytes, tryToReadCount);
- if (bytes <= 0)
- throw ExceptionHelper.ServerClosedTCPStream();
- readBytes += bytes;
- contentLength -= bytes;
- // Progress report:
- if (sendProgressChanged)
- {
- downloaded += bytes;
- RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.baseRequest, RequestEvents.DownloadProgress, downloaded, downloadLength));
- }
- } while (readBytes < buffer.Length && contentLength > 0);
- if (baseRequest.UseStreaming)
- {
- if (compressed)
- {
- var decompressed = decompressor.Decompress(buffer, 0, readBytes, false, true);
- if (decompressed.Data != null)
- FeedStreamFragment(decompressed.Data, 0, decompressed.Length);
- }
- else
- FeedStreamFragment(buffer, 0, readBytes);
- }
- else
- output.Write(buffer, 0, readBytes);
- };
- //BufferPool.Release(buffer);
- if (baseRequest.UseStreaming)
- {
- if (compressed)
- {
- var decompressed = decompressor.Decompress(null, 0, 0, true, true);
- if (decompressed.Data != null)
- FeedStreamFragment(decompressed.Data, 0, decompressed.Length);
- }
- FlushRemainingFragmentBuffer();
- }
- if (!baseRequest.UseStreaming)
- this.Data = DecodeStream(output);
- }
- }
- if (decompressor != null)
- decompressor.Dispose();
- }
- #endregion
- #region Read Unknown Size
- protected void ReadUnknownSize(Stream stream)
- {
- // Progress report:
- long Downloaded = 0;
- long DownloadLength = 0;
- bool sendProgressChanged = this.baseRequest.OnDownloadProgress != null && (this.IsSuccess
- #if !BESTHTTP_DISABLE_CACHING
- || this.IsFromCache
- #endif
- );
- if (sendProgressChanged)
- RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.baseRequest, RequestEvents.DownloadProgress, Downloaded, DownloadLength));
- string encoding =
- #if !BESTHTTP_DISABLE_CACHING
- IsFromCache ? null :
- #endif
- GetFirstHeaderValue("content-encoding");
- bool compressed = !string.IsNullOrEmpty(encoding);
- Decompression.IDecompressor decompressor = baseRequest.UseStreaming ? Decompression.DecompressorFactory.GetDecompressor(encoding, this.Context) : null;
- using (var output = new BufferPoolMemoryStream())
- {
- byte[] buffer = baseRequest.ReadBufferSizeOverride > 0 ? BufferPool.Get(baseRequest.ReadBufferSizeOverride, false) : BufferPool.Get(MinReadBufferSize, true);
- // wrap buffer in a PooledBuffer to release it back to the pool when leaving the current block.
- using (var _ = new PooledBuffer(buffer))
- {
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("ReadUnknownSize - buffer size: {0:N0}", buffer.Length));
- int readBytes = 0;
- int bytes = 0;
- do
- {
- readBytes = 0;
- do
- {
- if (this.baseRequest.IsCancellationRequested)
- return;
- bytes = 0;
- #if !NETFX_CORE || UNITY_EDITOR
- NetworkStream networkStream = stream as NetworkStream;
- // If we have the good-old NetworkStream, than we can use the DataAvailable property. On WP8 platforms, these are omitted... :/
- if (networkStream != null && baseRequest.EnableSafeReadOnUnknownContentLength)
- {
- for (int i = readBytes; i < buffer.Length && networkStream.DataAvailable; ++i)
- {
- int read = stream.ReadByte();
- if (read >= 0)
- {
- buffer[i] = (byte)read;
- bytes++;
- }
- else
- break;
- }
- }
- else // This will be good anyway, but a little slower.
- #endif
- {
- bytes = stream.Read(buffer, readBytes, buffer.Length - readBytes);
- }
- readBytes += bytes;
- // Progress report:
- Downloaded += bytes;
- DownloadLength = Downloaded;
- if (sendProgressChanged)
- RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.baseRequest, RequestEvents.DownloadProgress, Downloaded, DownloadLength));
- } while (readBytes < buffer.Length && bytes > 0);
- if (baseRequest.UseStreaming)
- {
- if (compressed)
- {
- var decompressed = decompressor.Decompress(buffer, 0, readBytes, false, true);
- if (decompressed.Data != null)
- FeedStreamFragment(decompressed.Data, 0, decompressed.Length);
- }
- else
- FeedStreamFragment(buffer, 0, readBytes);
- }
- else if (readBytes > 0)
- output.Write(buffer, 0, readBytes);
- } while (bytes > 0);
- //BufferPool.Release(buffer);
- if (baseRequest.UseStreaming)
- {
- if (compressed)
- {
- var decompressed = decompressor.Decompress(null, 0, 0, true, true);
- if (decompressed.Data != null)
- FeedStreamFragment(decompressed.Data, 0, decompressed.Length);
- }
- FlushRemainingFragmentBuffer();
- }
- if (!baseRequest.UseStreaming)
- this.Data = DecodeStream(output);
- }
- }
- if (decompressor != null)
- decompressor.Dispose();
- }
- #endregion
- #region Stream Decoding
- protected byte[] DecodeStream(BufferPoolMemoryStream streamToDecode)
- {
- streamToDecode.Seek(0, SeekOrigin.Begin);
- // The cache stores the decoded data
- var encoding =
- #if !BESTHTTP_DISABLE_CACHING
- IsFromCache ? null :
- #endif
- GetHeaderValues("content-encoding");
- Stream decoderStream = Decompression.DecompressorFactory.GetDecoderStream(streamToDecode, encoding?[0]);
- // Under WebGL decoderStream is always null.
- if (decoderStream == null)
- return streamToDecode.ToArray(canBeLarger: false);
- #if !UNITY_WEBGL || UNITY_EDITOR
- using (var ms = new BufferPoolMemoryStream((int)streamToDecode.Length))
- {
- var buf = BufferPool.Get(HTTPResponse.MinReadBufferSize, true);
- int byteCount = 0;
- while ((byteCount = decoderStream.Read(buf, 0, buf.Length)) > 0)
- ms.Write(buf, 0, byteCount);
- BufferPool.Release(buf);
- decoderStream.Dispose();
- return ms.ToArray();
- }
- #else
- return streamToDecode.ToArray(canBeLarger: false);
- #endif
- }
- #endregion
- #region Streaming Fragments Support
-
- protected void BeginReceiveStreamFragments()
- {
- #if !BESTHTTP_DISABLE_CACHING
- if (!baseRequest.DisableCache && baseRequest.UseStreaming)
- {
- // If caching is enabled and the response not from cache and it's cacheble we will cache the downloaded data.
- if (!IsFromCache && HTTPCacheService.IsCacheble(baseRequest.CurrentUri, baseRequest.MethodType, this))
- cacheStream = HTTPCacheService.PrepareStreamed(baseRequest.CurrentUri, this);
- }
- #endif
- allFragmentSize = 0;
- }
- /// <summary>
- /// Add data to the fragments list.
- /// </summary>
- /// <param name="buffer">The buffer to be added.</param>
- /// <param name="pos">The position where we start copy the data.</param>
- /// <param name="length">How many data we want to copy.</param>
- protected void FeedStreamFragment(byte[] buffer, int pos, int length)
- {
- if (buffer == null || length == 0)
- return;
- // If reading from cache, we don't want to read too much data to memory. So we will wait until the loaded fragment processed.
- #if !UNITY_WEBGL || UNITY_EDITOR
- #if CSHARP_7_3_OR_NEWER
- SpinWait spinWait = new SpinWait();
- #endif
- while (!this.baseRequest.IsCancellationRequested &&
- this.baseRequest.State == HTTPRequestStates.Processing &&
- baseRequest.UseStreaming &&
- FragmentQueueIsFull())
- {
- VerboseLogging("WaitWhileFragmentQueueIsFull");
- #if CSHARP_7_3_OR_NEWER
- spinWait.SpinOnce();
- #elif !NETFX_CORE
- System.Threading.Thread.Sleep(1);
- #endif
- }
- #endif
- if (fragmentBuffer == null)
- {
- fragmentBuffer = BufferPool.Get(baseRequest.StreamFragmentSize, true);
- fragmentBufferDataLength = 0;
- }
- if (fragmentBufferDataLength + length <= fragmentBuffer.Length)
- {
- Array.Copy(buffer, pos, fragmentBuffer, fragmentBufferDataLength, length);
- fragmentBufferDataLength += length;
- if (fragmentBufferDataLength == fragmentBuffer.Length || baseRequest.StreamChunksImmediately)
- {
- AddStreamedFragment(fragmentBuffer, fragmentBufferDataLength);
- fragmentBuffer = null;
- fragmentBufferDataLength = 0;
- }
- }
- else
- {
- int remaining = fragmentBuffer.Length - fragmentBufferDataLength;
- FeedStreamFragment(buffer, pos, remaining);
- FeedStreamFragment(buffer, pos + remaining, length - remaining);
- }
- }
- protected void FlushRemainingFragmentBuffer()
- {
- if (fragmentBuffer != null)
- {
- AddStreamedFragment(fragmentBuffer, fragmentBufferDataLength);
- fragmentBuffer = null;
- fragmentBufferDataLength = 0;
- }
- #if !BESTHTTP_DISABLE_CACHING
- if (cacheStream != null)
- {
- cacheStream.Dispose();
- cacheStream = null;
- HTTPCacheService.SetBodyLength(baseRequest.CurrentUri, allFragmentSize);
- }
- #endif
- }
- #if NET_STANDARD_2_0 || NETFX_CORE
- [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
- #endif
- protected void AddStreamedFragment(byte[] buffer, int bufferLength)
- {
- #if !BESTHTTP_DISABLE_CACHING
- if (!IsCacheOnly)
- #endif
- {
- if (this.baseRequest.UseStreaming && buffer != null && bufferLength > 0)
- {
- RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.baseRequest, buffer, bufferLength));
- Interlocked.Increment(ref this.UnprocessedFragments);
- }
- }
- if (HTTPManager.Logger.Level == Logger.Loglevels.All && buffer != null)
- VerboseLogging(string.Format("AddStreamedFragment buffer length: {0:N0} UnprocessedFragments: {1:N0}", bufferLength, Interlocked.Read(ref this.UnprocessedFragments)));
- #if !BESTHTTP_DISABLE_CACHING
- if (cacheStream != null)
- {
- cacheStream.Write(buffer, 0, bufferLength);
- allFragmentSize += bufferLength;
- }
- #endif
- }
- #if NET_STANDARD_2_0 || NETFX_CORE
- [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
- #endif
- private bool FragmentQueueIsFull()
- {
- #if !UNITY_WEBGL || UNITY_EDITOR
- long unprocessedFragments = Interlocked.Read(ref UnprocessedFragments);
- bool result = unprocessedFragments >= baseRequest.MaxFragmentQueueLength;
- if (result && HTTPManager.Logger.Level == Logger.Loglevels.All)
- VerboseLogging(string.Format("FragmentQueueIsFull - {0} / {1}", unprocessedFragments, baseRequest.MaxFragmentQueueLength));
- return result;
- #else
- return false;
- #endif
- }
- #endregion
- void VerboseLogging(string str)
- {
- if (HTTPManager.Logger.Level == Logger.Loglevels.All)
- HTTPManager.Logger.Verbose("HTTPResponse", str, this.Context, this.baseRequest.Context);
- }
- /// <summary>
- /// IDisposable implementation.
- /// </summary>
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- // Release resources in case we are using ReadOnlyBufferedStream, it will not close its inner stream.
- // Otherwise, closing the (inner) Stream is the connection's responsibility
- if (Stream != null && Stream is ReadOnlyBufferedStream)
- (Stream as IDisposable).Dispose();
- Stream = null;
- #if !BESTHTTP_DISABLE_CACHING
- if (cacheStream != null)
- {
- cacheStream.Dispose();
- cacheStream = null;
- }
- #endif
- }
- }
- }
- }
|