using System.Globalization;
namespace Sunfish.Http;
/// <summary>Base de los cuerpos de peticion: solo lectura asincrona.</summary>
internal abstract class RequestBodyStream : Stream
{
private Func<Task>? _onFirstRead;
protected RequestBodyStream(HttpRequestReader reader, Func<Task>? onFirstRead)
{
Reader = reader;
_onFirstRead = onFirstRead;
}
protected HttpRequestReader Reader { get; }
/// <summary>Se ha leido el cuerpo entero.</summary>
public abstract bool Completed { get; }
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken ct = default)
{
if (_onFirstRead is not null)
{
// Expect: 100-continue: el cliente espera permiso antes de enviar el cuerpo.
var send = _onFirstRead;
_onFirstRead = null;
await send();
}
return await ReadBodyAsync(buffer, ct);
}
protected abstract ValueTask<int> ReadBodyAsync(Memory<byte> buffer, CancellationToken ct);
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) =>
ReadAsync(buffer.AsMemory(offset, count), ct).AsTask();
public override int Read(byte[] buffer, int offset, int count) =>
ReadAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult();
/// <summary>Descarta lo que quede (hasta un limite) para poder leer la siguiente peticion.</summary>
public async Task<bool> DrainAsync(long limit, CancellationToken ct)
{
var buffer = new byte[16384];
long total = 0;
while (!Completed)
{
int n = await ReadBodyAsync(buffer, ct);
if (n == 0)
return Completed;
total += n;
if (total > limit)
return false;
}
return true;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
/// <summary>Cuerpo con Content-Length.</summary>
internal sealed class ContentLengthReadStream(HttpRequestReader reader, long length, Func<Task>? onFirstRead)
: RequestBodyStream(reader, onFirstRead)
{
private long _remaining = length;
public override bool Completed => _remaining == 0;
protected override async ValueTask<int> ReadBodyAsync(Memory<byte> buffer, CancellationToken ct)
{
if (_remaining == 0)
return 0;
if (buffer.Length > _remaining)
buffer = buffer[..(int)_remaining];
int n = await Reader.ReadRawAsync(buffer, ct);
if (n == 0)
throw new IOException("Connection closed in body");
_remaining -= n;
return n;
}
}
/// <summary>Cuerpo con Transfer-Encoding: chunked.</summary>
internal sealed class ChunkedReadStream(HttpRequestReader reader, Func<Task>? onFirstRead)
: RequestBodyStream(reader, onFirstRead)
{
private long _chunkRemaining;
private bool _done;
public override bool Completed => _done;
protected override async ValueTask<int> ReadBodyAsync(Memory<byte> buffer, CancellationToken ct)
{
if (_done)
return 0;
if (_chunkRemaining == 0)
{
string line = await Reader.ReadLineAsync(ct, countAsHeader: false) ?? throw new IOException("Connection closed in chunk");
int semi = line.IndexOf(';');
string size = (semi < 0 ? line : line[..semi]).Trim();
if (!long.TryParse(size, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out _chunkRemaining) || _chunkRemaining < 0)
throw new HttpParseException(400, "Bad chunk size");
if (_chunkRemaining == 0)
{
// Trailers hasta la linea vacia.
while (!string.IsNullOrEmpty(await Reader.ReadLineAsync(ct, countAsHeader: false))) { }
_done = true;
return 0;
}
}
if (buffer.Length > _chunkRemaining)
buffer = buffer[..(int)_chunkRemaining];
int n = await Reader.ReadRawAsync(buffer, ct);
if (n == 0)
throw new IOException("Connection closed in chunk");
_chunkRemaining -= n;
if (_chunkRemaining == 0)
await Reader.ReadLineAsync(ct, countAsHeader: false); // CRLF tras los datos del trozo
return n;
}
}