PeekableHTTP1Response.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Threading;
  4. using Best.HTTP.Request.Timings;
  5. using Best.HTTP.Response;
  6. using Best.HTTP.Response.Decompression;
  7. using Best.HTTP.Shared;
  8. using Best.HTTP.Shared.Extensions;
  9. using Best.HTTP.Shared.PlatformSupport.Memory;
  10. using Best.HTTP.Shared.Streams;
  11. using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
  12. using static Best.HTTP.Response.HTTPStatusCodes;
  13. namespace Best.HTTP.Hosts.Connections.HTTP1
  14. {
  15. /// <summary>
  16. /// An HTTP 1.1 response implementation that can utilize a peekable stream.
  17. /// Its main entry point is the ProcessPeekable method that should be called after every chunk of data downloaded.
  18. /// </summary>
  19. public class PeekableHTTP1Response : HTTPResponse
  20. {
  21. public PeekableReadState ReadState
  22. {
  23. get => this._readState;
  24. private set
  25. {
  26. if (this._readState != value)
  27. HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), $"{this._readState} => {value}", this.Context);
  28. this._readState = value;
  29. }
  30. }
  31. private PeekableReadState _readState;
  32. public bool ForceDepleteContent;
  33. public enum ContentDeliveryMode
  34. {
  35. Raw,
  36. RawUnknownLength,
  37. Chunked,
  38. }
  39. public enum PeekableReadState
  40. {
  41. StatusLine,
  42. Headers,
  43. WaitForContentSent, // when received a 100-continue
  44. PrepareForContent,
  45. ContentSetup,
  46. Content,
  47. Finished
  48. }
  49. public ContentDeliveryMode DeliveryMode => this._deliveryMode;
  50. private ContentDeliveryMode _deliveryMode;
  51. private long _expectedLength;
  52. private Dictionary<string, List<string>> _newHeaders;
  53. long _downloaded = 0;
  54. IDecompressor _decompressor = null;
  55. bool _compressed = false;
  56. bool sendProgressChanged;
  57. int _chunkLength = -1;
  58. enum ReadChunkedStates
  59. {
  60. ReadChunkLength,
  61. ReadChunk,
  62. ReadTrailingCRLF,
  63. ReadTrailingHeaders
  64. }
  65. ReadChunkedStates _readChunkedState = ReadChunkedStates.ReadChunkLength;
  66. IDownloadContentBufferAvailable _bufferAvailableHandler;
  67. public PeekableHTTP1Response(HTTPRequest request, bool isFromCache, IDownloadContentBufferAvailable bufferAvailableHandler)
  68. : base(request, isFromCache)
  69. {
  70. this._bufferAvailableHandler = bufferAvailableHandler;
  71. }
  72. private int _isProccessing;
  73. public void ProcessPeekable(PeekableContentProviderStream peekable)
  74. {
  75. // To avoid executing ProcessPeekable in parallel on two threads, do an atomic CompareExchange and return if the old value wasn't 0.
  76. if (Interlocked.CompareExchange(ref this._isProccessing, 1, 0) != 0)
  77. return;
  78. if (HTTPManager.Logger.IsDiagnostic)
  79. HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"ProcessPeekable({this.ReadState}, {peekable.Length})", this.Context);
  80. try
  81. {
  82. // The first call after setting it to PeekableReadState.WaitForContentSent is after the the client could send its content.
  83. // This also works when "If the request did not contain an Expect header field containing the 100-continue expectation,
  84. // the client can simply discard this interim response."
  85. // (https://www.rfc-editor.org/rfc/rfc9110#section-15.2.1-3)
  86. if (this._readState == PeekableReadState.WaitForContentSent)
  87. {
  88. this._newHeaders?.Clear();
  89. this.Headers?.Clear();
  90. this._readState = PeekableReadState.StatusLine;
  91. }
  92. // It's an unexpected network closure, except when we reading the content in the RawUnknownLength delivery mode.
  93. if (peekable == null && ReadState != PeekableReadState.Content && this._deliveryMode != ContentDeliveryMode.RawUnknownLength)
  94. throw new Exception("Server closed the connection unexpectedly!");
  95. switch (ReadState)
  96. {
  97. case PeekableReadState.StatusLine:
  98. if (!IsNewLinePresent(peekable))
  99. return;
  100. Request.Timing.StartNext(TimingEventNames.Headers);
  101. var statusLine = HTTPResponse.ReadTo(peekable, (byte)' ');
  102. string[] versions = statusLine.Split(new char[] { '/', '.' });
  103. this.HTTPVersion = new Version(int.Parse(versions[1]), int.Parse(versions[2]));
  104. int statusCode;
  105. string statusCodeStr = NoTrimReadTo(peekable, (byte)' ', LF);
  106. if (!int.TryParse(statusCodeStr, out statusCode))
  107. throw new Exception($"Couldn't parse '{statusCodeStr}' as a status code!");
  108. this.StatusCode = statusCode;
  109. if (statusCodeStr.Length > 0 && (byte)statusCodeStr[statusCodeStr.Length - 1] != LF && (byte)statusCodeStr[statusCodeStr.Length - 1] != CR)
  110. this.Message = ReadTo(peekable, LF);
  111. else
  112. {
  113. HTTPManager.Logger.Warning(nameof(PeekableHTTP1Response), "Skipping Status Message reading!", this.Context);
  114. this.Message = string.Empty;
  115. }
  116. if (HTTPManager.Logger.IsDiagnostic)
  117. HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"HTTP/'{this.HTTPVersion}' '{this.StatusCode}' '{this.Message}'", this.Context);
  118. if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
  119. this._newHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
  120. this.ReadState = PeekableReadState.Headers;
  121. goto case PeekableReadState.Headers;
  122. case PeekableReadState.Headers:
  123. ProcessReadHeaders(peekable, PeekableReadState.PrepareForContent);
  124. if (this.ReadState == PeekableReadState.PrepareForContent)
  125. {
  126. #if !UNITY_WEBGL || UNITY_EDITOR
  127. // When upgraded, we don't want to read the content here, so set the state to Finished.
  128. if (this.StatusCode == 101 && (HasHeaderWithValue("connection", "upgrade") || HasHeader("upgrade")) && this.Request?.DownloadSettings?.OnUpgraded != null)
  129. {
  130. HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), "Request Upgraded!", this.Context);
  131. this.IsUpgraded = this.Request.DownloadSettings.OnUpgraded(this.Request, this, peekable);
  132. if (this.IsUpgraded)
  133. {
  134. this._readState = PeekableReadState.Finished;
  135. goto case PeekableReadState.Finished;
  136. }
  137. }
  138. #endif
  139. // If it's a 100-continue, restart reading the response after the client could send its content.
  140. if (this.StatusCode == Continue)
  141. {
  142. this._readState = PeekableReadState.WaitForContentSent;
  143. break;
  144. }
  145. // https://www.rfc-editor.org/rfc/rfc9110#name-informational-1xx
  146. // A 1xx response is terminated by the end of the header section; it cannot contain content or trailers.
  147. if ((this.StatusCode >= Continue && this.StatusCode < OK) ||
  148. // https://www.rfc-editor.org/rfc/rfc9110#name-204-no-content
  149. // A 204 response is terminated by the end of the header section; it cannot contain content or trailers.
  150. this.StatusCode == NoContent ||
  151. // https://www.rfc-editor.org/rfc/rfc9110#name-304-not-modified
  152. // A 304 response is terminated by the end of the header section; it cannot contain content or trailers.
  153. this.StatusCode == NotModified)
  154. {
  155. this._readState = PeekableReadState.Finished;
  156. goto case PeekableReadState.Finished;
  157. }
  158. Request.Timing.StartNext(TimingEventNames.Response_Received);
  159. // if not an upgraded response, or OnUpgraded returned false, go for the content too.
  160. goto case PeekableReadState.PrepareForContent;
  161. }
  162. break;
  163. case PeekableReadState.PrepareForContent:
  164. BeginReceiveContent();
  165. // A content-length header might come with chunked transfer-encoding too.
  166. var contentLengthHeader = GetFirstHeaderValue("content-length");
  167. long.TryParse(contentLengthHeader, out this._expectedLength);
  168. if (HasHeaderWithValue("transfer-encoding", "chunked") && string.IsNullOrEmpty(contentLengthHeader))
  169. {
  170. this._deliveryMode = ContentDeliveryMode.Chunked;
  171. this.ReadState = PeekableReadState.ContentSetup;
  172. }
  173. else
  174. {
  175. this._deliveryMode = ContentDeliveryMode.Raw;
  176. this.ReadState = PeekableReadState.ContentSetup;
  177. var contentRangeHeaders = GetHeaderValues("content-range");
  178. if (contentLengthHeader == null && contentRangeHeaders == null)
  179. {
  180. this._deliveryMode = ContentDeliveryMode.RawUnknownLength;
  181. }
  182. else if (contentLengthHeader == null && contentRangeHeaders != null)
  183. {
  184. HTTPRange range = GetRange();
  185. this._expectedLength = (range.LastBytePos - range.FirstBytePos) + 1;
  186. }
  187. }
  188. HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), $"PrepareForContent - delivery mode selected: {this._deliveryMode}, {this._expectedLength}!", this.Context);
  189. CreateDownloadStream(this._bufferAvailableHandler);
  190. string encoding = IsFromCache ? null : GetFirstHeaderValue("content-encoding");
  191. #if !UNITY_WEBGL || UNITY_EDITOR
  192. this._compressed = !string.IsNullOrEmpty(encoding);
  193. // https://github.com/Benedicht/BestHTTP-Issues/issues/183
  194. // If _decompressor is still null, remove the compressed flag and serve the content as-is.
  195. if ((this._decompressor = DecompressorFactory.GetDecompressor(encoding, this.Context)) == null)
  196. this._compressed = false;
  197. #endif
  198. this.sendProgressChanged = this.Request.DownloadSettings.OnDownloadProgress != null && this.IsSuccess;
  199. this.ReadState = PeekableReadState.Content;
  200. goto case PeekableReadState.Content;
  201. case PeekableReadState.Content:
  202. var downStream = this.DownStream;
  203. if (downStream != null && downStream.MaxBuffered <= downStream.Length)
  204. return;
  205. switch (this._deliveryMode)
  206. {
  207. case ContentDeliveryMode.Raw: ProcessReadRaw(peekable); break;
  208. case ContentDeliveryMode.RawUnknownLength: ProcessReadRawUnknownLength(peekable); break;
  209. case ContentDeliveryMode.Chunked: ProcessReadChunked(peekable); break;
  210. }
  211. if (this.ReadState == PeekableReadState.Finished)
  212. goto case PeekableReadState.Finished;
  213. break;
  214. case PeekableReadState.Finished:
  215. //baseRequest.Timing.StartNext(TimingEventNames.Queued_For_Disptach);
  216. break;
  217. }
  218. }
  219. finally
  220. {
  221. Interlocked.Exchange(ref this._isProccessing, 0);
  222. }
  223. }
  224. bool IsNewLinePresent(PeekableStream peekable)
  225. {
  226. peekable.BeginPeek();
  227. int nextByte = peekable.PeekByte();
  228. while (nextByte >= 0 && nextByte != 0x0A)
  229. nextByte = peekable.PeekByte();
  230. return nextByte == 0x0A;
  231. }
  232. private void ProcessReadHeaders(PeekableStream peekable, PeekableReadState targetState)
  233. {
  234. if (!IsNewLinePresent(peekable))
  235. return;
  236. do
  237. {
  238. string headerName = ReadTo(peekable, (byte)':', LF);
  239. if (headerName == string.Empty)
  240. {
  241. this.ReadState = targetState;
  242. if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
  243. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, this._newHeaders));
  244. return;
  245. }
  246. string value = ReadTo(peekable, LF);
  247. if (HTTPManager.Logger.IsDiagnostic)
  248. HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"Header - '{headerName}': '{value}'", this.Context);
  249. AddHeader(headerName, value);
  250. if (this._newHeaders != null)
  251. {
  252. List<string> values;
  253. if (!this._newHeaders.TryGetValue(headerName, out values))
  254. this._newHeaders.Add(headerName, values = new List<string>(1));
  255. values.Add(value);
  256. }
  257. } while (IsNewLinePresent(peekable));
  258. }
  259. private void ProcessReadRawUnknownLength(PeekableStream peekable)
  260. {
  261. if (peekable == null)
  262. {
  263. if (sendProgressChanged)
  264. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
  265. PostProcessContent();
  266. this.ReadState = PeekableReadState.Finished;
  267. return;
  268. }
  269. while (peekable.Length > 0)
  270. {
  271. var buffer = BufferPool.Get(64 * 1024, true, this.Context);
  272. var readCount = peekable.Read(buffer, 0, buffer.Length);
  273. ProcessChunk(buffer.AsBuffer(readCount));
  274. }
  275. if (sendProgressChanged)
  276. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
  277. }
  278. private bool TryReadChunkLength(PeekableStream peekable, out int result)
  279. {
  280. result = -1;
  281. if (!IsNewLinePresent(peekable))
  282. return false;
  283. // Read until the end of line, then split the string so we will discard any optional chunk extensions
  284. string line = ReadTo(peekable, LF);
  285. string[] splits = line.Split(';');
  286. string num = splits[0];
  287. return int.TryParse(num, System.Globalization.NumberStyles.AllowHexSpecifier, null, out result);
  288. }
  289. void ProcessReadChunked(PeekableStream peekable)
  290. {
  291. switch(this._readChunkedState)
  292. {
  293. case ReadChunkedStates.ReadChunkLength:
  294. this._readChunkedState = ReadChunkedStates.ReadChunkLength;
  295. if (TryReadChunkLength(peekable, out this._chunkLength))
  296. {
  297. if (this._chunkLength == 0)
  298. {
  299. PostProcessContent();
  300. if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
  301. this._newHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
  302. goto case ReadChunkedStates.ReadTrailingHeaders;
  303. }
  304. goto case ReadChunkedStates.ReadChunk;
  305. }
  306. break;
  307. case ReadChunkedStates.ReadChunk:
  308. this._readChunkedState = ReadChunkedStates.ReadChunk;
  309. while (this._chunkLength > 0 && peekable.Length > 0)
  310. {
  311. int targetReadCount = Math.Min(Math.Min(64 * 1024, this._chunkLength), (int)peekable.Length);
  312. var buffer = BufferPool.Get(targetReadCount, true, this.Context);
  313. var readCount = peekable.Read(buffer, 0, targetReadCount);
  314. if (readCount < 0)
  315. {
  316. BufferPool.Release(buffer);
  317. throw ExceptionHelper.ServerClosedTCPStream();
  318. }
  319. this._chunkLength -= readCount;
  320. ProcessChunk(buffer.AsBuffer(readCount));
  321. }
  322. if (sendProgressChanged)
  323. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
  324. // Every chunk data has a trailing CRLF
  325. if (this._chunkLength == 0)
  326. goto case ReadChunkedStates.ReadTrailingCRLF;
  327. break;
  328. case ReadChunkedStates.ReadTrailingCRLF:
  329. this._readChunkedState = ReadChunkedStates.ReadTrailingCRLF;
  330. if (IsNewLinePresent(peekable))
  331. {
  332. ReadTo(peekable, LF);
  333. goto case ReadChunkedStates.ReadChunkLength;
  334. }
  335. break;
  336. case ReadChunkedStates.ReadTrailingHeaders:
  337. this._readChunkedState = ReadChunkedStates.ReadTrailingHeaders;
  338. ProcessReadHeaders(peekable, PeekableReadState.Finished);
  339. break;
  340. }
  341. }
  342. void ProcessReadRaw(PeekableStream peekable)
  343. {
  344. if (this.DownStream == null)
  345. throw new ArgumentNullException(nameof(this.DownStream));
  346. if (peekable == null)
  347. throw new ArgumentNullException(nameof(peekable));
  348. while (peekable.Length > 0 && !this.DownStream.IsFull)
  349. {
  350. var buffer = BufferPool.Get(64 * 1024, true, this.Context);
  351. var readCount = peekable.Read(buffer, 0, buffer.Length);
  352. if (readCount < 0)
  353. {
  354. BufferPool.Release(buffer);
  355. throw ExceptionHelper.ServerClosedTCPStream();
  356. }
  357. ProcessChunk(buffer.AsBuffer(readCount));
  358. }
  359. if (sendProgressChanged)
  360. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
  361. if (this._downloaded >= this._expectedLength)
  362. {
  363. PostProcessContent();
  364. this.ReadState = PeekableReadState.Finished;
  365. }
  366. }
  367. void ProcessChunk(BufferSegment chunk)
  368. {
  369. this._downloaded += chunk.Count;
  370. if (this._compressed)
  371. {
  372. var (decompressed, release) = this._decompressor.Decompress(chunk, false, true, this.Context);
  373. if (decompressed != BufferSegment.Empty)
  374. FeedDownloadedContentChunk(decompressed);
  375. //if (decompressed.Data != chunk.Data)
  376. if (release)
  377. BufferPool.Release(chunk);
  378. }
  379. else
  380. {
  381. FeedDownloadedContentChunk(chunk);
  382. }
  383. }
  384. void PostProcessContent()
  385. {
  386. if (this._compressed)
  387. {
  388. var (decompressed, release) = this._decompressor.Decompress(BufferSegment.Empty, true, true, this.Context);
  389. if (decompressed != BufferSegment.Empty)
  390. FeedDownloadedContentChunk(decompressed);
  391. }
  392. FinishedContentReceiving();
  393. if (this._decompressor != null)
  394. {
  395. this._decompressor.Dispose();
  396. this._decompressor = null;
  397. }
  398. }
  399. protected override void Dispose(bool disposing)
  400. {
  401. base.Dispose(disposing);
  402. this._decompressor?.Dispose();
  403. }
  404. }
  405. }