using System.Buffers;
using System.Collections.Specialized;
using System.Net.Sockets;
using System.Text;
using System.Web;
namespace Uwaa.HTTP;
class HttpStream : IDisposable
{
///
/// The underlying TCP stream.
///
readonly Stream Stream;
///
/// The read/write buffer.
///
readonly BufferedStream Buffer;
///
/// Text decoder.
///
readonly Decoder Decoder;
///
/// The maximum time the socket may be inactive before it is presumed dead and closed.
///
public TimeSpan Timeout;
public HttpStream(Stream stream, TimeSpan timeout) : base()
{
Stream = stream;
Timeout = timeout;
Buffer = new BufferedStream(stream);
Decoder = Encoding.ASCII.GetDecoder();
}
public async ValueTask ReadLine()
{
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
const int maxChars = 4096;
byte[] dataBuffer = ArrayPool.Shared.Rent(1);
char[] charBuffer = ArrayPool.Shared.Rent(maxChars);
try
{
int charBufferIndex = 0;
while (true)
{
if (await Buffer.ReadAsync(dataBuffer.AsMemory(0, 1), cancelSrc.Token) == 0)
if (charBufferIndex == 0)
throw new SocketException((int)SocketError.ConnectionReset);
else
break;
if (charBufferIndex >= maxChars)
throw new HttpException("Header is too large");
charBufferIndex += Decoder.GetChars(dataBuffer, 0, 1, charBuffer, charBufferIndex, false);
if (charBufferIndex >= 2 && charBuffer[charBufferIndex - 1] == '\n' && charBuffer[charBufferIndex - 2] == '\r')
{
charBufferIndex -= 2;
break;
}
}
Decoder.Reset();
return new string(charBuffer, 0, charBufferIndex);
}
catch (IOException e)
{
if (e.InnerException is SocketException se)
throw se;
else
throw;
}
finally
{
//Clearing the array is unnecessary but it is good security just in case.
ArrayPool.Shared.Return(charBuffer, true);
ArrayPool.Shared.Return(dataBuffer);
}
}
public async ValueTask Read(Memory buffer)
{
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
try
{
int index = 0;
while (index < buffer.Length)
{
int count = await Buffer.ReadAsync(buffer[index..], cancelSrc.Token);
if (count == 0)
break;
index += count;
}
return index;
}
catch (IOException e)
{
if (e.InnerException is SocketException se)
throw se;
else
throw;
}
}
public ValueTask Write(string text)
{
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
byte[] data = Encoding.ASCII.GetBytes(text);
return Buffer.WriteAsync(data, cancelSrc.Token);
}
public ValueTask WriteLine(string text)
{
return Write(text + "\r\n");
}
public ValueTask WriteLine()
{
return Write("\r\n");
}
public ValueTask Write(ReadOnlyMemory bytes)
{
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
try
{
return Buffer.WriteAsync(bytes, cancelSrc.Token);
}
catch (IOException e)
{
if (e.InnerException is SocketException se)
throw se;
else
throw;
}
}
public async Task Flush()
{
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
try
{
await Buffer.FlushAsync(cancelSrc.Token);
await Stream.FlushAsync(cancelSrc.Token);
}
catch (IOException e)
{
if (e.InnerException is SocketException se)
throw se;
else
throw;
}
}
public async ValueTask ReadFields()
{
HttpFields fields = new HttpFields();
while (true)
{
string? headerStr = await ReadLine();
if (string.IsNullOrWhiteSpace(headerStr))
break; //End of headers
if (fields.Misc != null && fields.Misc.Count >= 30)
throw new HttpException("Too many headers");
int splitPoint = headerStr.IndexOf(':');
if (splitPoint == -1)
throw new HttpException("A header is invalid");
string name = headerStr.Remove(splitPoint).Trim();
string value = headerStr.Substring(splitPoint + 1).Trim();
fields[name] = value;
}
return fields;
}
public async ValueTask ReadContent(HttpFields headers)
{
if (!headers.ContentLength.HasValue)
return null;
if (!headers.ContentType.HasValue)
throw new HttpException("Content length was sent but no content type");
if (headers.ContentLength.Value > 10_000_000)
throw new HttpException("Too much content (max: 10 MB)");
byte[] data = new byte[headers.ContentLength.Value];
await Read(data);
return new HttpContent(headers.ContentType.Value, data);
}
public async ValueTask<(HttpMethod Method, string Path)> ReadRequestHeader()
{
//Read initial header
string header = await ReadLine();
string[] parts = header.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2) //breaks specification, must require 3, but impl genuinely only needs 2
throw new HttpException("Invalid initial header");
//Method
if (!Enum.TryParse(parts[0], true, out HttpMethod method))
throw new HttpException("Unknown HTTP method");
//Path
string path = parts[1].Replace("\\", "/");
return (method, path);
}
public async ValueTask<(int Code, string Message)> ReadResponseHeader()
{
string responseHeader = await ReadLine();
string[] parts = responseHeader.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 3)
throw new HttpException("Invalid initial header");
if (!parts[0].Equals("HTTP/1.0", StringComparison.OrdinalIgnoreCase) && !parts[0].Equals("HTTP/1.1", StringComparison.OrdinalIgnoreCase))
throw new HttpException("Unsupported HTTP version");
if (!int.TryParse(parts[1], out int statusCode))
throw new HttpException("Invalid status code");
return (statusCode, parts[2]);
}
public async Task ReadRequest()
{
try
{
(HttpMethod method, string path) = await ReadRequestHeader();
HttpFields fields = await ReadFields();
HttpContent? content = await ReadContent(fields);
return new HttpRequest(method, path, fields, content);
}
catch (FormatException e)
{
throw new HttpException(e.Message, e);
}
}
public async Task ReadResponse()
{
try
{
(int statusCode, string statusMessage) = await ReadResponseHeader();
HttpFields fields = await ReadFields();
HttpContent? content = await ReadContent(fields);
return new HttpResponse(statusCode, statusMessage, fields, content);
}
catch (FormatException e)
{
throw new HttpException(e.Message, e);
}
}
///
/// Disposes the underlying stream.
///
public void Dispose()
{
((IDisposable)Stream).Dispose();
}
}