using System.Text;
using XFESystem.Browser;
namespace XFESystem.NativeRuntime.Application.Web;
// Bounded HTTP framing shared in semantics with the legacy tested reader.
// This implementation depends on a supplied Stream read, never host sockets.
internal sealed record NativeHttpResponse(int StatusCode, string ContentType, byte[] Body,
string? Location, string? ContentSecurityPolicy, IReadOnlyDictionary<string,string> Headers, string StatusLine);
/// <summary>Bounded HTTP framing over actual stream bytes, retaining parsed response headers.</summary>
internal static class NativeHttpProtocol
{
private const int MaximumHeaderBytes = 16_384;
private const int MaximumLineBytes = 4_096;
public static NativeHttpResponse ReadResponse(Func<byte[], int, int, int> receive, int maximumBodyBytes)
{
ArgumentNullException.ThrowIfNull(receive);
ArgumentOutOfRangeException.ThrowIfLessThan(maximumBodyBytes, 1);
ArgumentOutOfRangeException.ThrowIfGreaterThan(maximumBodyBytes, 33_554_432);
var reader = new WireReader(receive);
Dictionary<string, string> headers;
var totalHeaderBytes = 0;
var interim = 0;
int status; string statusLine;
do
{
statusLine = reader.Line(ref totalHeaderBytes);
if (statusLine.Length < 12 || !(statusLine.StartsWith("HTTP/1.1 ", StringComparison.Ordinal) ||
statusLine.StartsWith("HTTP/1.0 ", StringComparison.Ordinal)) ||
statusLine[9] is < '1' or > '5' || statusLine[10] is < '0' or > '9' || statusLine[11] is < '0' or > '9' ||
statusLine.Length > 12 && statusLine[12] != ' ')
throw new IOException("The HTTP status line is invalid.");
status = (statusLine[9] - '0') * 100 + (statusLine[10] - '0') * 10 + statusLine[11] - '0';
headers = ReadHeaders(reader, ref totalHeaderBytes);
if (status == 101) throw new NotSupportedException("HTTP protocol upgrades are not supported.");
if (status < 200 && ++interim > 4) throw new IOException("Too many interim HTTP responses.");
} while (status < 200);
headers.TryGetValue("Content-Type", out var contentType);
headers.TryGetValue("Location", out var location);
headers.TryGetValue("Content-Security-Policy", out var csp);
if (headers.TryGetValue("Content-Encoding", out var contentEncoding) &&
!contentEncoding.Equals("identity", StringComparison.OrdinalIgnoreCase))
throw new NotSupportedException("Compressed native HTTP bodies are not supported.");
var hasLength = headers.TryGetValue("Content-Length", out var lengthText);
var hasTransfer = headers.TryGetValue("Transfer-Encoding", out var transfer);
if (hasLength && hasTransfer) throw new IOException("Ambiguous HTTP Content-Length and Transfer-Encoding.");
if (hasTransfer && !transfer!.Equals("chunked", StringComparison.OrdinalIgnoreCase))
throw new NotSupportedException("Only a single chunked HTTP transfer coding is supported.");
var body = new ByteBody(maximumBodyBytes);
if (status is 204 or 304 or 301 or 302 or 303 or 307 or 308)
return new(status, contentType ?? "text/plain; charset=utf-8", [], location, csp, headers, statusLine);
if (hasTransfer)
{
var trailerBytes = 0;
while (true)
{
var lineBytes = 0;
var chunkLine = reader.Line(ref lineBytes);
var semicolon = chunkLine.IndexOf(';');
var size = ParseUnsigned(semicolon < 0 ? chunkLine : chunkLine[..semicolon], 16, maximumBodyBytes);
if (size == 0)
{
var trailers = ReadHeaders(reader, ref trailerBytes);
if (trailers.ContainsKey("Content-Length") || trailers.ContainsKey("Transfer-Encoding") ||
trailers.ContainsKey("Host"))
throw new IOException("HTTP trailers contain forbidden framing fields.");
break;
}
body.ReadExactly(reader, size);
if (reader.Byte() != '\r' || reader.Byte() != '\n') throw new IOException("A chunk has no CRLF terminator.");
}
}
else if (hasLength)
{
var size = ParseUnsigned(lengthText!, 10, maximumBodyBytes);
body.ReadExactly(reader, size);
}
else
{
var buffer = new byte[4_096];
int count;
while ((count = reader.Read(buffer, 0, buffer.Length)) != 0) body.Append(buffer, count);
}
return new(status, contentType ?? "text/plain; charset=utf-8", body.ToArray(), location, csp, headers, statusLine);
}
public static string DecodeBody(NativeHttpResponse response)
{
var charset = "utf-8";
foreach (var parameter in response.ContentType.Split(';').Skip(1))
{
var pair = parameter.Trim().Split('=', 2);
if (pair.Length == 2 && pair[0].Equals("charset", StringComparison.OrdinalIgnoreCase))
charset = pair[1].Trim().Trim('"', '\'').ToLowerInvariant();
}
var encoding = charset switch
{
"utf-8" or "utf8" => Encoding.UTF8,
"us-ascii" or "ascii" => Encoding.ASCII,
"iso-8859-1" or "latin1" => Encoding.Latin1,
_ => throw new NotSupportedException("The native HTTP charset is not supported: " + charset)
};
return encoding.GetString(response.Body);
}
private static Dictionary<string, string> ReadHeaders(WireReader reader, ref int totalBytes)
{
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
while (true)
{
var line = reader.Line(ref totalBytes);
if (line.Length == 0) return headers;
if (headers.Count == 100) throw new IOException("The HTTP header count limit was reached.");
var colon = line.IndexOf(':');
if (colon <= 0 || line[..colon].Any(character => !IsToken(character)))
throw new IOException("The HTTP header name is invalid.");
var name = line[..colon];
var value = line[(colon + 1)..].Trim(' ', '\t');
if (headers.TryGetValue(name, out var previous))
{
if (name.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) ||
name.Equals("Transfer-Encoding", StringComparison.OrdinalIgnoreCase) ||
name.Equals("Location", StringComparison.OrdinalIgnoreCase))
throw new IOException("Duplicate HTTP framing or redirect header.");
headers[name] = previous + "; " + value;
}
else headers.Add(name, value);
}
}
private static bool IsToken(char value) => value is >= '0' and <= '9' or >= 'A' and <= 'Z' or >= 'a' and <= 'z' ||
"!#$%&'*+-.^_`|~".Contains(value);
private static int ParseUnsigned(string text, int radix, int maximum)
{
if (text.Length is 0 or > 16) throw new IOException("The HTTP body length is invalid.");
var result = 0;
foreach (var character in text)
{
var digit = character is >= '0' and <= '9' ? character - '0' :
character is >= 'a' and <= 'f' ? character - 'a' + 10 :
character is >= 'A' and <= 'F' ? character - 'A' + 10 : -1;
if (digit < 0 || digit >= radix) throw new IOException("The HTTP body length is invalid.");
if (result > (maximum - digit) / radix || digit > maximum)
throw new BrowserLimitException("The native HTTP body exceeds its byte limit.");
result = result * radix + digit;
}
return result;
}
private sealed class WireReader(Func<byte[], int, int, int> receive)
{
private readonly byte[] buffer = new byte[4_096];
private int position;
private int length;
public int Byte()
{
if (position == length)
{
length = receive(buffer, 0, buffer.Length);
position = 0;
if (length < 0 || length > buffer.Length) throw new IOException("Invalid native TCP byte count.");
if (length == 0) return -1;
}
return buffer[position++];
}
public int Read(byte[] target, int offset, int count)
{
if (position != length)
{
var copied = Math.Min(count, length - position);
Array.Copy(buffer, position, target, offset, copied);
position += copied;
return copied;
}
var read = receive(target, offset, count);
if (read < 0 || read > count) throw new IOException("Invalid native TCP byte count.");
return read;
}
public string Line(ref int totalBytes)
{
var line = new StringBuilder();
while (true)
{
var value = Byte();
if (value < 0) throw new IOException("The HTTP headers ended unexpectedly.");
if (++totalBytes > MaximumHeaderBytes) throw new IOException("The HTTP header byte limit was reached.");
if (value == '\r')
{
if (Byte() != '\n') throw new IOException("HTTP lines require CRLF.");
if (++totalBytes > MaximumHeaderBytes) throw new IOException("The HTTP header byte limit was reached.");
return line.ToString();
}
if (value != '\t' && value < 32 || value > 126)
throw new IOException("HTTP headers contain invalid control or non-ASCII bytes.");
if (line.Length == MaximumLineBytes) throw new IOException("The HTTP line length limit was reached.");
line.Append((char)value);
}
}
}
private sealed class ByteBody(int maximum)
{
private byte[] bytes = new byte[Math.Min(maximum, 4_096)];
private int length;
public void ReadExactly(WireReader reader, int count)
{
EnsureCapacity(count);
var end = length + count;
while (length < end)
{
var read = reader.Read(bytes, length, end - length);
if (read == 0) throw new IOException("The HTTP body ended before its declared length.");
length += read;
}
}
public void Append(byte[] buffer, int count)
{
EnsureCapacity(count);
Array.Copy(buffer, 0, bytes, length, count);
length += count;
}
private void EnsureCapacity(int count)
{
if (count > maximum - length) throw new BrowserLimitException("The native HTTP body exceeds its byte limit.");
if (bytes.Length >= length + count) return;
var expanded = new byte[Math.Min(maximum, Math.Max(length + count, bytes.Length * 2))];
Array.Copy(bytes, expanded, length);
bytes = expanded;
}
public byte[] ToArray()
{
var result = new byte[length];
Array.Copy(bytes, result, length);
return result;
}
}
}
using System.Text;
using XFESystem.Browser;
namespace XFESystem.NativeRuntime.Application.Web;
// Bounded HTTP framing shared in semantics with the legacy tested reader.
// This implementation depends on a supplied Stream read, never host sockets.
internal sealed record NativeHttpResponse(int StatusCode, string ContentType, byte[] Body,
string? Location, string? ContentSecurityPolicy, IReadOnlyDictionary<string,string> Headers, string StatusLine);
/// <summary>Bounded HTTP framing over actual stream bytes, retaining parsed response headers.</summary>
internal static class NativeHttpProtocol
{
private const int MaximumHeaderBytes = 16_384;
private const int MaximumLineBytes = 4_096;
public static NativeHttpResponse ReadResponse(Func<byte[], int, int, int> receive, int maximumBodyBytes)
{
ArgumentNullException.ThrowIfNull(receive);
ArgumentOutOfRangeException.ThrowIfLessThan(maximumBodyBytes, 1);
ArgumentOutOfRangeException.ThrowIfGreaterThan(maximumBodyBytes, 33_554_432);
var reader = new WireReader(receive);
Dictionary<string, string> headers;
var totalHeaderBytes = 0;
var interim = 0;
int status; string statusLine;
do
{
statusLine = reader.Line(ref totalHeaderBytes);
if (statusLine.Length < 12 || !(statusLine.StartsWith("HTTP/1.1 ", StringComparison.Ordinal) ||
statusLine.StartsWith("HTTP/1.0 ", StringComparison.Ordinal)) ||
statusLine[9] is < '1' or > '5' || statusLine[10] is < '0' or > '9' || statusLine[11] is < '0' or > '9' ||
statusLine.Length > 12 && statusLine[12] != ' ')
throw new IOException("The HTTP status line is invalid.");
status = (statusLine[9] - '0') * 100 + (statusLine[10] - '0') * 10 + statusLine[11] - '0';
headers = ReadHeaders(reader, ref totalHeaderBytes);
if (status == 101) throw new NotSupportedException("HTTP protocol upgrades are not supported.");
if (status < 200 && ++interim > 4) throw new IOException("Too many interim HTTP responses.");
} while (status < 200);
headers.TryGetValue("Content-Type", out var contentType);
headers.TryGetValue("Location", out var location);
headers.TryGetValue("Content-Security-Policy", out var csp);
if (headers.TryGetValue("Content-Encoding", out var contentEncoding) &&
!contentEncoding.Equals("identity", StringComparison.OrdinalIgnoreCase))
throw new NotSupportedException("Compressed native HTTP bodies are not supported.");
var hasLength = headers.TryGetValue("Content-Length", out var lengthText);
var hasTransfer = headers.TryGetValue("Transfer-Encoding", out var transfer);
if (hasLength && hasTransfer) throw new IOException("Ambiguous HTTP Content-Length and Transfer-Encoding.");
if (hasTransfer && !transfer!.Equals("chunked", StringComparison.OrdinalIgnoreCase))
throw new NotSupportedException("Only a single chunked HTTP transfer coding is supported.");
var body = new ByteBody(maximumBodyBytes);
if (status is 204 or 304 or 301 or 302 or 303 or 307 or 308)
return new(status, contentType ?? "text/plain; charset=utf-8", [], location, csp, headers, statusLine);
if (hasTransfer)
{
var trailerBytes = 0;
while (true)
{
var lineBytes = 0;
var chunkLine = reader.Line(ref lineBytes);
var semicolon = chunkLine.IndexOf(';');
var size = ParseUnsigned(semicolon < 0 ? chunkLine : chunkLine[..semicolon], 16, maximumBodyBytes);
if (size == 0)
{
var trailers = ReadHeaders(reader, ref trailerBytes);
if (trailers.ContainsKey("Content-Length") || trailers.ContainsKey("Transfer-Encoding") ||
trailers.ContainsKey("Host"))
throw new IOException("HTTP trailers contain forbidden framing fields.");
break;
}
body.ReadExactly(reader, size);
if (reader.Byte() != '\r' || reader.Byte() != '\n') throw new IOException("A chunk has no CRLF terminator.");
}
}
else if (hasLength)
{
var size = ParseUnsigned(lengthText!, 10, maximumBodyBytes);
body.ReadExactly(reader, size);
}
else
{
var buffer = new byte[4_096];
int count;
while ((count = reader.Read(buffer, 0, buffer.Length)) != 0) body.Append(buffer, count);
}
return new(status, contentType ?? "text/plain; charset=utf-8", body.ToArray(), location, csp, headers, statusLine);
}
public static string DecodeBody(NativeHttpResponse response)
{
var charset = "utf-8";
foreach (var parameter in response.ContentType.Split(';').Skip(1))
{
var pair = parameter.Trim().Split('=', 2);
if (pair.Length == 2 && pair[0].Equals("charset", StringComparison.OrdinalIgnoreCase))
charset = pair[1].Trim().Trim('"', '\'').ToLowerInvariant();
}
var encoding = charset switch
{
"utf-8" or "utf8" => Encoding.UTF8,
"us-ascii" or "ascii" => Encoding.ASCII,
"iso-8859-1" or "latin1" => Encoding.Latin1,
_ => throw new NotSupportedException("The native HTTP charset is not supported: " + charset)
};
return encoding.GetString(response.Body);
}
private static Dictionary<string, string> ReadHeaders(WireReader reader, ref int totalBytes)
{
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
while (true)
{
var line = reader.Line(ref totalBytes);
if (line.Length == 0) return headers;
if (headers.Count == 100) throw new IOException("The HTTP header count limit was reached.");
var colon = line.IndexOf(':');
if (colon <= 0 || line[..colon].Any(character => !IsToken(character)))
throw new IOException("The HTTP header name is invalid.");
var name = line[..colon];
var value = line[(colon + 1)..].Trim(' ', '\t');
if (headers.TryGetValue(name, out var previous))
{
if (name.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) ||
name.Equals("Transfer-Encoding", StringComparison.OrdinalIgnoreCase) ||
name.Equals("Location", StringComparison.OrdinalIgnoreCase))
throw new IOException("Duplicate HTTP framing or redirect header.");
headers[name] = previous + "; " + value;
}
else headers.Add(name, value);
}
}
private static bool IsToken(char value) => value is >= '0' and <= '9' or >= 'A' and <= 'Z' or >= 'a' and <= 'z' ||
"!#$%&'*+-.^_`|~".Contains(value);
private static int ParseUnsigned(string text, int radix, int maximum)
{
if (text.Length is 0 or > 16) throw new IOException("The HTTP body length is invalid.");
var result = 0;
foreach (var character in text)
{
var digit = character is >= '0' and <= '9' ? character - '0' :
character is >= 'a' and <= 'f' ? character - 'a' + 10 :
character is >= 'A' and <= 'F' ? character - 'A' + 10 : -1;
if (digit < 0 || digit >= radix) throw new IOException("The HTTP body length is invalid.");
if (result > (maximum - digit) / radix || digit > maximum)
throw new BrowserLimitException("The native HTTP body exceeds its byte limit.");
result = result * radix + digit;
}
return result;
}
private sealed class WireReader(Func<byte[], int, int, int> receive)
{
private readonly byte[] buffer = new byte[4_096];
private int position;
private int length;
public int Byte()
{
if (position == length)
{
length = receive(buffer, 0, buffer.Length);
position = 0;
if (length < 0 || length > buffer.Length) throw new IOException("Invalid native TCP byte count.");
if (length == 0) return -1;
}
return buffer[position++];
}
public int Read(byte[] target, int offset, int count)
{
if (position != length)
{
var copied = Math.Min(count, length - position);
Array.Copy(buffer, position, target, offset, copied);
position += copied;
return copied;
}
var read = receive(target, offset, count);
if (read < 0 || read > count) throw new IOException("Invalid native TCP byte count.");
return read;
}
public string Line(ref int totalBytes)
{
var line = new StringBuilder();
while (true)
{
var value = Byte();
if (value < 0) throw new IOException("The HTTP headers ended unexpectedly.");
if (++totalBytes > MaximumHeaderBytes) throw new IOException("The HTTP header byte limit was reached.");
if (value == '\r')
{
if (Byte() != '\n') throw new IOException("HTTP lines require CRLF.");
if (++totalBytes > MaximumHeaderBytes) throw new IOException("The HTTP header byte limit was reached.");
return line.ToString();
}
if (value != '\t' && value < 32 || value > 126)
throw new IOException("HTTP headers contain invalid control or non-ASCII bytes.");
if (line.Length == MaximumLineBytes) throw new IOException("The HTTP line length limit was reached.");
line.Append((char)value);
}
}
}
private sealed class ByteBody(int maximum)
{
private byte[] bytes = new byte[Math.Min(maximum, 4_096)];
private int length;
public void ReadExactly(WireReader reader, int count)
{
EnsureCapacity(count);
var end = length + count;
while (length < end)
{
var read = reader.Read(bytes, length, end - length);
if (read == 0) throw new IOException("The HTTP body ended before its declared length.");
length += read;
}
}
public void Append(byte[] buffer, int count)
{
EnsureCapacity(count);
Array.Copy(buffer, 0, bytes, length, count);
length += count;
}
private void EnsureCapacity(int count)
{
if (count > maximum - length) throw new BrowserLimitException("The native HTTP body exceeds its byte limit.");
if (bytes.Length >= length + count) return;
var expanded = new byte[Math.Min(maximum, Math.Max(length + count, bytes.Length * 2))];
Array.Copy(bytes, expanded, length);
bytes = expanded;
}
public byte[] ToArray()
{
var result = new byte[length];
Array.Copy(bytes, result, length);
return result;
}
}
}