Uwaa/HTTP/HttpStream.cs

284 lines
8.2 KiB
C#
Raw Permalink Normal View History

2024-11-22 07:40:43 +01:00
using System.Buffers;
using System.Collections.Specialized;
using System.Net.Sockets;
using System.Text;
using System.Web;
namespace Uwaa.HTTP;
class HttpStream : IDisposable
{
/// <summary>
/// The underlying TCP stream.
/// </summary>
readonly Stream Stream;
/// <summary>
/// The read/write buffer.
/// </summary>
readonly BufferedStream Buffer;
/// <summary>
/// Text decoder.
/// </summary>
readonly Decoder Decoder;
2024-12-25 23:14:53 +01:00
/// <summary>
/// The maximum time the socket may be inactive before it is presumed dead and closed.
/// </summary>
public TimeSpan Timeout;
public HttpStream(Stream stream, TimeSpan timeout) : base()
2024-11-22 07:40:43 +01:00
{
Stream = stream;
2024-12-25 23:14:53 +01:00
Timeout = timeout;
2024-11-22 07:40:43 +01:00
Buffer = new BufferedStream(stream);
Decoder = Encoding.ASCII.GetDecoder();
2024-12-25 23:14:53 +01:00
2024-11-22 07:40:43 +01:00
}
public async ValueTask<string> ReadLine()
{
2024-12-25 23:14:53 +01:00
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
2024-11-22 07:40:43 +01:00
const int maxChars = 4096;
byte[] dataBuffer = ArrayPool<byte>.Shared.Rent(1);
char[] charBuffer = ArrayPool<char>.Shared.Rent(maxChars);
try
{
int charBufferIndex = 0;
while (true)
{
2024-12-25 23:14:53 +01:00
if (await Buffer.ReadAsync(dataBuffer.AsMemory(0, 1), cancelSrc.Token) == 0)
2024-11-22 07:40:43 +01:00
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<char>.Shared.Return(charBuffer, true);
ArrayPool<byte>.Shared.Return(dataBuffer);
}
}
2024-11-30 12:18:33 +01:00
public async ValueTask<int> Read(Memory<byte> buffer)
2024-11-22 07:40:43 +01:00
{
2024-12-25 23:14:53 +01:00
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
2024-11-22 07:40:43 +01:00
try
{
2024-11-30 12:18:33 +01:00
int index = 0;
while (index < buffer.Length)
{
2024-12-25 23:14:53 +01:00
int count = await Buffer.ReadAsync(buffer[index..], cancelSrc.Token);
2024-11-30 12:18:33 +01:00
if (count == 0)
break;
index += count;
}
return index;
2024-11-22 07:40:43 +01:00
}
catch (IOException e)
{
if (e.InnerException is SocketException se)
throw se;
else
throw;
}
}
public ValueTask Write(string text)
{
2024-12-25 23:14:53 +01:00
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
2024-11-22 07:40:43 +01:00
byte[] data = Encoding.ASCII.GetBytes(text);
2024-12-25 23:14:53 +01:00
return Buffer.WriteAsync(data, cancelSrc.Token);
2024-11-22 07:40:43 +01:00
}
public ValueTask WriteLine(string text)
{
return Write(text + "\r\n");
}
public ValueTask WriteLine()
{
return Write("\r\n");
}
public ValueTask Write(ReadOnlyMemory<byte> bytes)
{
2024-12-25 23:14:53 +01:00
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
2024-11-22 07:40:43 +01:00
try
{
2024-12-25 23:14:53 +01:00
return Buffer.WriteAsync(bytes, cancelSrc.Token);
2024-11-22 07:40:43 +01:00
}
catch (IOException e)
{
if (e.InnerException is SocketException se)
throw se;
else
throw;
}
}
public async Task Flush()
{
2024-12-25 23:14:53 +01:00
CancellationTokenSource cancelSrc = new CancellationTokenSource();
cancelSrc.CancelAfter(Timeout);
2024-11-22 07:40:43 +01:00
try
{
2024-12-25 23:14:53 +01:00
await Buffer.FlushAsync(cancelSrc.Token);
await Stream.FlushAsync(cancelSrc.Token);
2024-11-22 07:40:43 +01:00
}
catch (IOException e)
{
if (e.InnerException is SocketException se)
throw se;
else
throw;
}
}
public async ValueTask<HttpFields> 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<HttpContent?> 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);
}
2024-11-25 23:54:35 +01:00
public async ValueTask<(HttpMethod Method, string Path)> ReadRequestHeader()
2024-11-22 07:40:43 +01:00
{
//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");
2024-11-25 23:54:35 +01:00
//Path
string path = parts[1].Replace("\\", "/");
2024-11-22 07:40:43 +01:00
2024-11-25 23:54:35 +01:00
return (method, path);
2024-11-22 07:40:43 +01:00
}
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<HttpRequest> ReadRequest()
{
try
{
2024-11-25 23:54:35 +01:00
(HttpMethod method, string path) = await ReadRequestHeader();
2024-11-22 07:40:43 +01:00
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<HttpResponse> 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);
}
}
/// <summary>
/// Disposes the underlying stream.
/// </summary>
public void Dispose()
{
((IDisposable)Stream).Dispose();
}
}