283 lines
8.2 KiB
C#
283 lines
8.2 KiB
C#
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;
|
|
|
|
/// <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()
|
|
{
|
|
Stream = stream;
|
|
Timeout = timeout;
|
|
Buffer = new BufferedStream(stream);
|
|
Decoder = Encoding.ASCII.GetDecoder();
|
|
|
|
}
|
|
|
|
public async ValueTask<string> ReadLine()
|
|
{
|
|
CancellationTokenSource cancelSrc = new CancellationTokenSource();
|
|
cancelSrc.CancelAfter(Timeout);
|
|
|
|
const int maxChars = 4096;
|
|
byte[] dataBuffer = ArrayPool<byte>.Shared.Rent(1);
|
|
char[] charBuffer = ArrayPool<char>.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<char>.Shared.Return(charBuffer, true);
|
|
ArrayPool<byte>.Shared.Return(dataBuffer);
|
|
}
|
|
}
|
|
|
|
public async ValueTask<int> Read(Memory<byte> 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<byte> 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<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);
|
|
}
|
|
|
|
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<HttpRequest> 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<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();
|
|
}
|
|
}
|