private static async Task <byte[]> GetDecodedChunkedContentBytesAsync(Stream stream, HttpResponseContentHeaders headerStruct, CancellationToken ctsToken = default) { return(await GetDecodedChunkedContentBytesAsync(stream, null, headerStruct, ctsToken)); }
private static async Task <byte[]> GetDecodedChunkedContentBytesAsync(Stream stream, HttpRequestContentHeaders requestHeaders, HttpResponseContentHeaders responseHeaders, CancellationToken ctsToken = default) { if (responseHeaders is null) { if (requestHeaders is null) { throw new ArgumentException("Response and request headers cannot be both null."); } } else { if (requestHeaders != null) { throw new ArgumentException("Either response or request headers has to be null."); } } // https://tools.ietf.org/html/rfc7230#section-4.1.3 // 4.1.3.Decoding Chunked // A process for decoding the chunked transfer coding can be represented // in pseudo - code as: // length := 0 // read chunk - size, chunk - ext(if any), and CRLF // while (chunk - size > 0) // { // read chunk-data and CRLF // append chunk-data to decoded-body // length:= length + chunk - size // read chunk-size, chunk - ext(if any), and CRLF // } // read trailer field // while (trailer field is not empty) { // if (trailer field is allowed to be sent in a trailer) { // append trailer field to existing header fields // } // read trailer-field // } // Content - Length := length // Remove "chunked" from Transfer-Encoding // Remove Trailer from existing header fields long length = 0; var firstChunkLine = await ReadCRLFLineAsync(stream, Encoding.ASCII, ctsToken : ctsToken); ParseFistChunkLine(firstChunkLine, out long chunkSize, out _); // We will not do anything with the chunk extensions, because: // https://tools.ietf.org/html/rfc7230#section-4.1.1 // A recipient MUST ignore unrecognized chunk extensions. var decodedBody = new List <byte>(); // https://tools.ietf.org/html/rfc7230#section-4.1 // The chunked transfer coding is complete // when a chunk with a chunk-size of zero is received, possibly followed // by a trailer, and finally terminated by an empty line. while (chunkSize > 0) { var chunkData = await ReadBytesTillLengthAsync(stream, chunkSize, ctsToken); if (await ReadCRLFLineAsync(stream, Encoding.ASCII, ctsToken) != "") { throw new FormatException("Chunk does not end with CRLF."); } decodedBody.AddRange(chunkData); length += chunkSize; firstChunkLine = await ReadCRLFLineAsync(stream, Encoding.ASCII, ctsToken : ctsToken); ParseFistChunkLine(firstChunkLine, out long cs, out _); chunkSize = cs; } // https://tools.ietf.org/html/rfc7230#section-4.1.2 // A trailer allows the sender to include additional fields at the end // of a chunked message in order to supply metadata that might be // dynamically generated while the message body is sent string trailerHeaders = await ReadHeadersAsync(stream, ctsToken); var trailerHeaderSection = await HeaderSection.CreateNewAsync(trailerHeaders); RemoveInvalidTrailers(trailerHeaderSection); if (responseHeaders != null) { var trailerHeaderStruct = trailerHeaderSection.ToHttpResponseHeaders(); AssertValidHeaders(trailerHeaderStruct.ResponseHeaders, trailerHeaderStruct.ContentHeaders); // https://tools.ietf.org/html/rfc7230#section-4.1.2 // When a chunked message containing a non-empty trailer is received, // the recipient MAY process the fields(aside from those forbidden // above) as if they were appended to the message's header section. CopyHeaders(trailerHeaderStruct.ResponseHeaders, responseHeaders.ResponseHeaders); responseHeaders.ResponseHeaders.Remove("Transfer-Encoding"); responseHeaders.ContentHeaders.TryAddWithoutValidation("Content-Length", length.ToString()); responseHeaders.ResponseHeaders.Remove("Trailer"); } if (requestHeaders != null) { var trailerHeaderStruct = trailerHeaderSection.ToHttpRequestHeaders(); AssertValidHeaders(trailerHeaderStruct.RequestHeaders, trailerHeaderStruct.ContentHeaders); // https://tools.ietf.org/html/rfc7230#section-4.1.2 // When a chunked message containing a non-empty trailer is received, // the recipient MAY process the fields(aside from those forbidden // above) as if they were appended to the message's header section. CopyHeaders(trailerHeaderStruct.RequestHeaders, requestHeaders.RequestHeaders); requestHeaders.RequestHeaders.Remove("Transfer-Encoding"); requestHeaders.ContentHeaders.TryAddWithoutValidation("Content-Length", length.ToString()); requestHeaders.RequestHeaders.Remove("Trailer"); } return(decodedBody.ToArray()); }
public static async Task <byte[]> GetContentBytesAsync(Stream stream, HttpResponseContentHeaders headerStruct, HttpMethod requestMethod, StatusLine statusLine, CancellationToken ctsToken = default) { // https://tools.ietf.org/html/rfc7230#section-3.3.3 // The length of a message body is determined by one of the following // (in order of precedence): // 1.Any response to a HEAD request and any response with a 1xx // (Informational), 204(No Content), or 304(Not Modified) status // code is always terminated by the first empty line after the // header fields, regardless of the header fields present in the // message, and thus cannot contain a message body. if (requestMethod == HttpMethod.Head || HttpStatusCodeHelper.IsInformational(statusLine.StatusCode) || statusLine.StatusCode == HttpStatusCode.NoContent || statusLine.StatusCode == HttpStatusCode.NotModified) { return(GetDummyOrNullContentBytes(headerStruct.ContentHeaders)); } // https://tools.ietf.org/html/rfc7230#section-3.3.3 // 2.Any 2xx(Successful) response to a CONNECT request implies that // the connection will become a tunnel immediately after the empty // line that concludes the header fields.A client MUST ignore any // Content - Length or Transfer-Encoding header fields received in // such a message. if (requestMethod == new HttpMethod("CONNECT")) { if (HttpStatusCodeHelper.IsSuccessful(statusLine.StatusCode)) { return(null); } } // https://tools.ietf.org/html/rfc7230#section-3.3.3 // 3.If a Transfer-Encoding header field is present and the chunked // transfer coding(Section 4.1) is the final encoding, the message // body length is determined by reading and decoding the chunked // data until the transfer coding indicates the data is complete. if (headerStruct?.ResponseHeaders != null && headerStruct.ResponseHeaders.Contains("Transfer-Encoding")) { // https://tools.ietf.org/html/rfc7230#section-4 // All transfer-coding names are case-insensitive if ("chunked".Equals(headerStruct.ResponseHeaders.TransferEncoding.Last().Value, StringComparison.OrdinalIgnoreCase)) { return(await GetDecodedChunkedContentBytesAsync(stream, headerStruct, ctsToken)); } // https://tools.ietf.org/html/rfc7230#section-3.3.3 // If a Transfer - Encoding header field is present in a response and // the chunked transfer coding is not the final encoding, the // message body length is determined by reading the connection until // it is closed by the server. If a Transfer - Encoding header field // is present in a request and the chunked transfer coding is not // the final encoding, the message body length cannot be determined // reliably; the server MUST respond with the 400(Bad Request) // status code and then close the connection. return(await GetBytesTillEndAsync(stream, ctsToken)); } // https://tools.ietf.org/html/rfc7230#section-3.3.3 // 5.If a valid Content - Length header field is present without // Transfer - Encoding, its decimal value defines the expected message // body length in octets.If the sender closes the connection or // the recipient times out before the indicated number of octets are // received, the recipient MUST consider the message to be // incomplete and close the connection. if (headerStruct.ContentHeaders.Contains("Content-Length")) { long?contentLength = headerStruct.ContentHeaders?.ContentLength; return(await ReadBytesTillLengthAsync(stream, contentLength, ctsToken)); } // https://tools.ietf.org/html/rfc7230#section-3.3.3 // 6.If this is a request message and none of the above are true, then // the message body length is zero (no message body is present). // 7. Otherwise, this is a response message without a declared message // body length, so the message body length is determined by the // number of octets received prior to the server closing the // connection. return(await GetBytesTillEndAsync(stream, ctsToken)); }
private static async Task <HttpContent> GetDecodedChunkedContentAsync(Stream stream, HttpResponseContentHeaders headerStruct, CancellationToken ctsToken = default) { return(await GetDecodedChunkedContentAsync(stream, null, headerStruct, ctsToken).ConfigureAwait(false)); }