Skip to content

Commit

Permalink
add some chunked transfer support for data without known length
Browse files Browse the repository at this point in the history
  • Loading branch information
atauenis committed Nov 20, 2023
1 parent f49fea0 commit 88b9747
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 11 deletions.
37 changes: 27 additions & 10 deletions HttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,22 +123,31 @@ public string ProtocolVersionString
/// <summary>
/// Gets or sets the number of bytes in the body data included in the response.
/// </summary>
/// <returns>The value of the response's Content-Length header.</returns>
/// <returns>The value of the response's Content-Length header. Or &quot;-1&quot; to use chunked transfer.</returns>
public long ContentLength64
{
get => contentLength64;
set
{
contentLength64 = value;
if (MshttpapiBackend != null) MshttpapiBackend.ContentLength64 = contentLength64;
if (value < 0 && MshttpapiBackend == null) // Content-Length less than 0 means content with unknown length and chunked transfer
{
#if EnableChunkedTransfer
if (Headers["Transfer-Encoding"] == null)
AddHeader("Transfer-Encoding", "chunked");
else
Headers["Transfer-Encoding"] = "chunked";
#endif
return;
}
if (value < 0 && MshttpapiBackend != null) return;
if (value > -1 && MshttpapiBackend != null) { MshttpapiBackend.ContentLength64 = value; }

if (Headers["Content-Length"] == null)
AddHeader("Content-Length", contentLength64.ToString());
else
Headers["Content-Length"] = contentLength64.ToString();
}

//think about chunked transfers with unknown content length
}

/// <summary>
Expand Down Expand Up @@ -174,10 +183,16 @@ public Stream OutputStream
{
get
{
if (false) throw new InvalidOperationException("Content-Length not set."); //todo in future
if (HeadersSent)
{
if (MshttpapiBackend != null) return outputStream; //MsHttpApi processes everything itself
if (outputStream is HttpResponseContentStream) return outputStream; //already configured

if (HeadersSent) return outputStream;
else throw new InvalidOperationException("Call SendHeaders() first before sending body.");
// Set up HttpResponseContentStream according to headers data (transfer-encoding)
outputStream = new HttpResponseContentStream(outputStream, ContentLength64 < 0);
return outputStream;
}
else { throw new InvalidOperationException("Call SendHeaders() first before sending body."); }
}
set
{ outputStream = value; }
Expand Down Expand Up @@ -239,7 +254,7 @@ public HttpResponse(TcpClient Backend)
public HttpResponse(SslStream Backend)
{
SslBackend = Backend;
outputStream = Backend;
OutputStream = Backend;
ProtocolVersion = new Version(1, 1);

Headers = new WebHeaderCollection();
Expand Down Expand Up @@ -302,13 +317,15 @@ public void Close()
if (MshttpapiBackend != null) { MshttpapiBackend.Close(); return; }
if (TcpclientBackend != null)
{
TcpclientBackend.GetStream().Flush();
if (outputStream is HttpResponseContentStream) (outputStream as HttpResponseContentStream).WriteTerminator();
if (TcpclientBackend.Connected) TcpclientBackend.GetStream().Flush();
if (!KeepAlive) TcpclientBackend.Close();
return;
}
if (SslBackend != null)
{
SslBackend.Flush();
if (outputStream is HttpResponseContentStream) (outputStream as HttpResponseContentStream).WriteTerminator();
if (SslBackend.CanWrite) SslBackend.Flush();
if (!KeepAlive) SslBackend.Close();
return;
}
Expand Down
114 changes: 114 additions & 0 deletions HttpResponseContentStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System;
using System.IO;
using System.Text;

namespace WebOne
{
/// <summary>
/// A wrapper around a <see cref="Stream"/> that can write HTTP response bodies in according to RFC 9112 §7.
/// </summary>
public class HttpResponseContentStream : Stream
{
//RTFM: https://datatracker.ietf.org/doc/html/rfc9112#section-7

private readonly Stream inner;
private bool UseChunkedTransfer;
//private bool UseCompressedTransfer;
//private string UseCompressionAlgorithm;

/// <summary>
/// Initialize this HttpResponseContentStream instance.
/// </summary>
/// <param name="inner">The stream used to communicate with client.</param>
/// <param name="chunked">Use HTTP Chunked Transfer</param>
public HttpResponseContentStream(Stream inner, bool chunked)
{
this.inner = inner;
UseChunkedTransfer = chunked;
//todo: add compression support (and compression + chunked).

#if !EnableChunkedTransfer
UseChunkedTransfer = false;
#else
//DOES NOT REALLY WORKING, HELP IS WANTED - @atauenis, 20.11.2023
#endif
}

public override void Flush()
{ inner.Flush(); }

public override long Seek(long offset, SeekOrigin origin)
{ throw new NotImplementedException(); }

public override void SetLength(long value)
{ throw new NotImplementedException(); }

public override int Read(byte[] buffer, int offset, int count)
{ throw new NotImplementedException(); }

/// <summary>
/// Writes a sequence of bytes to the client.
/// </summary>
/// <param name="buffer">Array of bytes containing the data payload.</param>
/// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
/// <param name="count">The number of bytes to be written to the current stream.</param>
public override void Write(byte[] buffer, int offset, int count)
{
if (UseChunkedTransfer)
{
// Send chunk
Console.WriteLine("Chunked: Write {0} bytes.1", count - offset);
byte[] StartBuffer = Encoding.ASCII.GetBytes((count - offset).ToString("X") + "\r\n");
byte[] EndBuffer = Encoding.ASCII.GetBytes("\r\n");
inner.Write(StartBuffer, 0, StartBuffer.Length);
inner.Write(buffer, offset, count);
inner.Write(EndBuffer, 0, EndBuffer.Length);
inner.Flush();
Console.WriteLine("Chunked: Write {0} bytes.2", count - offset);
//SOMEWHY INTERPRETTED AS PLAIN TEXT - HELP NEED TO SOLVE
}
else
{
// Just write the body
inner.Write(buffer, offset, count);
#if EnableChunkedTransfer
Console.WriteLine("Plain: Write {0} bytes.", count - offset);
#endif
}
}

/// <summary>
/// If the data transfer channel between server and client is based on encoded transfer, send mark of content end,
/// required to properly finish the transfer session.
/// </summary>
/// <param name="trailer">Trailing header (if any)</param>
public void WriteTerminator(string trailer = "")
{
if (UseChunkedTransfer)
{
// Write terminating chunk if need
Console.WriteLine("Chunked: Terminate by {0} bytes.1", trailer.Length);
byte[] TerminatorStartBuffer = Encoding.ASCII.GetBytes("0\r\n");
byte[] TerminatorEndBuffer = Encoding.ASCII.GetBytes(trailer + "\r\n");
try
{
inner.Write(TerminatorStartBuffer, 0, TerminatorStartBuffer.Length);
inner.Write(TerminatorEndBuffer, 0, TerminatorEndBuffer.Length);
}
catch { /* Sometimes an connection lost may occur here. */ };
Console.WriteLine("Chunked: Terminate by {0} bytes.2", trailer.Length);
}
}

public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;

public override long Length
{
get { throw new NotImplementedException(); }
}

public override long Position { get; set; }
}
}
2 changes: 1 addition & 1 deletion HttpTransit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2063,7 +2063,7 @@ private void SendStream(Stream Potok, string ContentType, bool Close = true)
ClientResponse.StatusCode = 200;
ClientResponse.ProtocolVersion = new Version(1, 0);
ClientResponse.ContentType = ContentType;
if (Potok.CanSeek) ClientResponse.ContentLength64 = Potok.Length;
if (Potok.CanSeek) ClientResponse.ContentLength64 = Potok.Length; else ClientResponse.ContentLength64 = -1;
if (Potok.CanSeek) Potok.Position = 0;
ClientResponse.SendHeaders();
Potok.CopyTo(ClientResponse.OutputStream);
Expand Down
7 changes: 7 additions & 0 deletions WebOne.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ fi
</PostRemoveScript>
</PropertyGroup>

<!--
Enable dirty support for "Transfer-Encoding: chunked" instead of raw length-less transfer.
-->
<!--<PropertyGroup>
<DefineConstants>EnableChunkedTransfer</DefineConstants>
</PropertyGroup>-->

<ItemGroup>
<Compile Remove="EXE\**" />
<EmbeddedResource Remove="EXE\**" />
Expand Down

0 comments on commit 88b9747

Please sign in to comment.