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 Headers, string StatusLine); /// Bounded HTTP framing over actual stream bytes, retaining parsed response headers. internal static class NativeHttpProtocol { private const int MaximumHeaderBytes = 16_384; private const int MaximumLineBytes = 4_096; public static NativeHttpResponse ReadResponse(Func receive, int maximumBodyBytes) { ArgumentNullException.ThrowIfNull(receive); ArgumentOutOfRangeException.ThrowIfLessThan(maximumBodyBytes, 1); ArgumentOutOfRangeException.ThrowIfGreaterThan(maximumBodyBytes, 33_554_432); var reader = new WireReader(receive); Dictionary 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 ReadHeaders(WireReader reader, ref int totalBytes) { var headers = new Dictionary(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 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; } } }