using System.Text;
using XFESystem.Browser;
using WebResponse = XFESystem.Browser.WebResponse;
#if !XFE_HTTP_PROTOCOL_HOST_TEST
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using AddressFamily = System.Net.Sockets.AddressFamily;
using Cosmos.Kernel.System.Network;
using Cosmos.Kernel.System.Timers;
#endif
namespace XFESystem.Kernel;
#if !XFE_HTTP_PROTOCOL_HOST_TEST
/// <summary>
/// Real Cosmos socket transport for bounded IPv4 HTTP/1.1. HTTPS is refused:
/// the pinned runtime has no validated native TLS implementation.
/// </summary>
public sealed class NativeWebTransport : IWebTransport, IWebTransportCapabilities
{
public bool IsNative => true;
public bool SupportsHttp => true;
public bool SupportsHttps => false;
// Cosmos 3.0.94 ConnectTcp has an internal fixed 5000 ms wait. Never enter
// it when the operation's remaining budget cannot cover that wait.
private const int ConnectBudgetMilliseconds = 5_100;
private string? fault;
public string? Fault => fault;
public WebResponse Get(Uri address, BrowserLimits limits)
{
ArgumentNullException.ThrowIfNull(address);
ArgumentNullException.ThrowIfNull(limits);
ValidateAddress(address);
if (fault is not null)
throw new InvalidOperationException("Native HTTP transport stopped after a socket failure: " + fault);
if (!NetworkManager.IsEnabled || NetworkManager.DeviceCount == 0 || !NetworkManager.Ready ||
!NetworkManager.LinkUp || NetworkManager.Primary.IPConfig is null)
throw new InvalidOperationException("No configured native IPv4 network adapter is available.");
if (!TimerManager.IsInitialized || Stopwatch.Frequency <= 0)
throw new InvalidOperationException("Native HTTP requires a reliable monotonic clock and timer.");
if (limits.NetworkTimeout < TimeSpan.FromMilliseconds(ConnectBudgetMilliseconds) ||
limits.NetworkTimeout > TimeSpan.FromMinutes(2))
throw new NotSupportedException("This native socket profile requires a network timeout between 5.1 and 120 seconds.");
if (limits.MaximumHtmlCharacters is < 1 or > 8_388_608 || limits.MaximumRedirects is < 0 or > 10)
throw new ArgumentOutOfRangeException(nameof(limits));
var watch = Stopwatch.StartNew();
var target = address;
for (var redirects = 0; ; redirects++)
{
ValidateAddress(target);
var remote = Resolve(target.IdnHost, watch, limits.NetworkTimeout);
if (Remaining(watch, limits.NetworkTimeout) < ConnectBudgetMilliseconds)
throw new TimeoutException("The remaining HTTP deadline cannot cover the native TCP connect wait.");
var request = BuildRequest(target);
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
var connected = false;
NativeHttpResponse response;
try
{
// 3.0.94 reaps SYN_SENT on Close(0), so a failed connection
// can be retried without the 3.0.88 permanent circuit break.
socket.Connect(remote, target.Port);
connected = true;
CheckDeadline(watch, limits.NetworkTimeout);
var sent = 0;
while (sent < request.Length)
{
CheckDeadline(watch, limits.NetworkTimeout);
var count = socket.Send(request, sent, Math.Min(512, request.Length - sent), SocketFlags.None);
if (count <= 0) throw new IOException("The native TCP send did not make progress.");
sent += count;
}
response = NativeHttpProtocol.ReadResponse((buffer, offset, count) =>
Receive(socket, buffer, offset, count, watch, limits.NetworkTimeout),
checked(limits.MaximumHtmlCharacters * 4));
// The request explicitly asks the server to close. Give its
// FIN a bounded grace interval; do not accumulate unbounded
// detached FIN_WAIT state when a peer ignores that request.
var closeStart = watch.ElapsedMilliseconds;
while (socket.Connected && watch.ElapsedMilliseconds - closeStart < 100 &&
Remaining(watch, limits.NetworkTimeout) > 0)
TimerManager.Wait(1);
if (socket.Connected)
fault = "The peer did not close its HTTP connection; this runtime cannot reap detached TCP state safely. Restart the network runtime before another request.";
// Cosmos 3.0.94 does not run finally clauses during exception
// unwinding. Release on both explicit paths instead.
socket.Close(0);
}
catch
{
if (connected && socket.Connected)
fault ??= "The HTTP exchange failed while TCP remained open. Restart the network runtime before retrying.";
// Use the explicit zero-linger overload; Dispose/Close() use
// the upstream five-second close wait and native handles.
try { socket.Close(0); }
catch (Exception cleanupError)
{
fault ??= "Native TCP cleanup failed: " + cleanupError.Message;
}
throw;
}
if (response.StatusCode is 301 or 302 or 303 or 307 or 308)
{
if (redirects >= limits.MaximumRedirects) throw new InvalidOperationException("The HTTP redirect limit was reached.");
if (string.IsNullOrWhiteSpace(response.Location) || !Uri.TryCreate(target, response.Location, out var next))
throw new InvalidOperationException("The HTTP redirect has no valid Location header.");
ValidateAddress(next);
if (fault is not null) throw new InvalidOperationException(fault);
target = next;
continue;
}
var body = NativeHttpProtocol.DecodeBody(response);
if (body.Length > limits.MaximumHtmlCharacters)
throw new BrowserLimitException("The decoded native HTTP body exceeds its character limit.");
return new(target, response.StatusCode, response.ContentType, body, false, response.ContentSecurityPolicy);
}
}
private static IPAddress Resolve(string host, Stopwatch watch, TimeSpan timeout)
{
if (TryIPv4(host, out var bytes)) return new IPAddress(bytes);
if (DnsConfig.Nameservers.Count == 0) throw new InvalidOperationException("No native DNS server is configured.");
if (DnsConfig.Nameservers[0] is not Address4)
throw new NotSupportedException("This native browser transport resolves DNS over IPv4 only.");
var milliseconds = Math.Min(5_000, Remaining(watch, timeout) - ConnectBudgetMilliseconds);
milliseconds = milliseconds / 10 * 10;
if (milliseconds <= 0) throw new TimeoutException("The HTTP deadline cannot cover DNS and TCP connect.");
var resolver = new DnsClient();
try
{
resolver.Connect(DnsConfig.Nameservers[0]);
resolver.SendQuery(host);
var address = resolver.Receive(milliseconds);
var result = address is Address4 ? new IPAddress(address.ToBytes().ToArray())
: throw new InvalidOperationException("Native DNS did not return an IPv4 address before its deadline.");
resolver.Close();
return result;
}
catch
{
resolver.Close();
throw;
}
}
private static int Receive(Socket socket, byte[] buffer, int offset, int count, Stopwatch watch, TimeSpan timeout)
{
while (true)
{
CheckDeadline(watch, timeout);
var available = socket.Available;
if (available > 0)
{
socket.ReceiveTimeout = Math.Max(1, Remaining(watch, timeout));
// Avoid the extra temporary copy allocated by the upstream
// Span overload; this adapter owns a reusable byte[] buffer.
return socket.Receive(buffer, offset, Math.Min(count, available), SocketFlags.None);
}
if (!socket.Connected)
{
// Recheck in case the final data and FIN arrived between the
// first Available read and the connection-state read.
if (socket.Available > 0) continue;
return 0;
}
// Poll outside Receive so the whole-operation deadline is checked
// between waits; 3.0.94 also supplies the receive timeout above.
TimerManager.Wait(1);
}
}
private static byte[] BuildRequest(Uri address)
{
var host = address.IsDefaultPort ? address.IdnHost : address.IdnHost + ":" + address.Port;
var path = string.IsNullOrEmpty(address.PathAndQuery) ? "/" : address.PathAndQuery;
var request = "GET " + path + " HTTP/1.1\r\nHost: " + host +
"\r\nUser-Agent: XFESystem/0.3-CSharp\r\nAccept: text/html,text/plain,text/markdown\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n";
if (request.Length > 4_096 || request.Any(character => character > 127 || character == '\0'))
throw new NotSupportedException("The native HTTP request exceeds its ASCII header limit.");
return Encoding.ASCII.GetBytes(request);
}
private static void ValidateAddress(Uri address)
{
if (!address.IsAbsoluteUri || !string.IsNullOrEmpty(address.UserInfo) || address.AbsoluteUri.Length > 2_048)
throw new NotSupportedException("Native HTTP requires an absolute URL without embedded credentials.");
if (address.Scheme == "https")
throw new NotSupportedException("HTTPS is unavailable: this native runtime has no verified TLS implementation. Certificate validation cannot be bypassed.");
if (address.Scheme != "http" || address.HostNameType is not (UriHostNameType.IPv4 or UriHostNameType.Dns))
throw new NotSupportedException("The native browser currently supports IPv4 HTTP URLs only.");
if (address.AbsoluteUri.Any(char.IsControl)) throw new ArgumentException("The URL contains control characters.");
}
private static bool TryIPv4(string host, out byte[] bytes)
{
bytes = new byte[4];
var fragments = host.Split('.');
if (fragments.Length != 4) return false;
for (var index = 0; index < 4; index++)
{
if (fragments[index].Length is 0 or > 3) return false;
var value = 0;
foreach (var character in fragments[index])
{
if (character is < '0' or > '9') return false;
value = value * 10 + character - '0';
}
if (value > 255) return false;
bytes[index] = (byte)value;
}
return true;
}
private static int Remaining(Stopwatch watch, TimeSpan timeout) =>
(int)Math.Max(0, timeout.TotalMilliseconds - watch.Elapsed.TotalMilliseconds);
private static void CheckDeadline(Stopwatch watch, TimeSpan timeout)
{
if (Remaining(watch, timeout) <= 0) throw new TimeoutException("The native HTTP operation timed out.");
}
}
#endif
internal sealed record NativeHttpResponse(int StatusCode, string ContentType, byte[] Body,
string? Location, string? ContentSecurityPolicy);
/// <summary>Bounded HTTP framing shared verbatim by the native adapter and host protocol tests.</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;
do
{
var 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);
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);
}
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;
using WebResponse = XFESystem.Browser.WebResponse;
#if !XFE_HTTP_PROTOCOL_HOST_TEST
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using AddressFamily = System.Net.Sockets.AddressFamily;
using Cosmos.Kernel.System.Network;
using Cosmos.Kernel.System.Timers;
#endif
namespace XFESystem.Kernel;
#if !XFE_HTTP_PROTOCOL_HOST_TEST
/// <summary>
/// Real Cosmos socket transport for bounded IPv4 HTTP/1.1. HTTPS is refused:
/// the pinned runtime has no validated native TLS implementation.
/// </summary>
public sealed class NativeWebTransport : IWebTransport, IWebTransportCapabilities
{
public bool IsNative => true;
public bool SupportsHttp => true;
public bool SupportsHttps => false;
// Cosmos 3.0.94 ConnectTcp has an internal fixed 5000 ms wait. Never enter
// it when the operation's remaining budget cannot cover that wait.
private const int ConnectBudgetMilliseconds = 5_100;
private string? fault;
public string? Fault => fault;
public WebResponse Get(Uri address, BrowserLimits limits)
{
ArgumentNullException.ThrowIfNull(address);
ArgumentNullException.ThrowIfNull(limits);
ValidateAddress(address);
if (fault is not null)
throw new InvalidOperationException("Native HTTP transport stopped after a socket failure: " + fault);
if (!NetworkManager.IsEnabled || NetworkManager.DeviceCount == 0 || !NetworkManager.Ready ||
!NetworkManager.LinkUp || NetworkManager.Primary.IPConfig is null)
throw new InvalidOperationException("No configured native IPv4 network adapter is available.");
if (!TimerManager.IsInitialized || Stopwatch.Frequency <= 0)
throw new InvalidOperationException("Native HTTP requires a reliable monotonic clock and timer.");
if (limits.NetworkTimeout < TimeSpan.FromMilliseconds(ConnectBudgetMilliseconds) ||
limits.NetworkTimeout > TimeSpan.FromMinutes(2))
throw new NotSupportedException("This native socket profile requires a network timeout between 5.1 and 120 seconds.");
if (limits.MaximumHtmlCharacters is < 1 or > 8_388_608 || limits.MaximumRedirects is < 0 or > 10)
throw new ArgumentOutOfRangeException(nameof(limits));
var watch = Stopwatch.StartNew();
var target = address;
for (var redirects = 0; ; redirects++)
{
ValidateAddress(target);
var remote = Resolve(target.IdnHost, watch, limits.NetworkTimeout);
if (Remaining(watch, limits.NetworkTimeout) < ConnectBudgetMilliseconds)
throw new TimeoutException("The remaining HTTP deadline cannot cover the native TCP connect wait.");
var request = BuildRequest(target);
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
var connected = false;
NativeHttpResponse response;
try
{
// 3.0.94 reaps SYN_SENT on Close(0), so a failed connection
// can be retried without the 3.0.88 permanent circuit break.
socket.Connect(remote, target.Port);
connected = true;
CheckDeadline(watch, limits.NetworkTimeout);
var sent = 0;
while (sent < request.Length)
{
CheckDeadline(watch, limits.NetworkTimeout);
var count = socket.Send(request, sent, Math.Min(512, request.Length - sent), SocketFlags.None);
if (count <= 0) throw new IOException("The native TCP send did not make progress.");
sent += count;
}
response = NativeHttpProtocol.ReadResponse((buffer, offset, count) =>
Receive(socket, buffer, offset, count, watch, limits.NetworkTimeout),
checked(limits.MaximumHtmlCharacters * 4));
// The request explicitly asks the server to close. Give its
// FIN a bounded grace interval; do not accumulate unbounded
// detached FIN_WAIT state when a peer ignores that request.
var closeStart = watch.ElapsedMilliseconds;
while (socket.Connected && watch.ElapsedMilliseconds - closeStart < 100 &&
Remaining(watch, limits.NetworkTimeout) > 0)
TimerManager.Wait(1);
if (socket.Connected)
fault = "The peer did not close its HTTP connection; this runtime cannot reap detached TCP state safely. Restart the network runtime before another request.";
// Cosmos 3.0.94 does not run finally clauses during exception
// unwinding. Release on both explicit paths instead.
socket.Close(0);
}
catch
{
if (connected && socket.Connected)
fault ??= "The HTTP exchange failed while TCP remained open. Restart the network runtime before retrying.";
// Use the explicit zero-linger overload; Dispose/Close() use
// the upstream five-second close wait and native handles.
try { socket.Close(0); }
catch (Exception cleanupError)
{
fault ??= "Native TCP cleanup failed: " + cleanupError.Message;
}
throw;
}
if (response.StatusCode is 301 or 302 or 303 or 307 or 308)
{
if (redirects >= limits.MaximumRedirects) throw new InvalidOperationException("The HTTP redirect limit was reached.");
if (string.IsNullOrWhiteSpace(response.Location) || !Uri.TryCreate(target, response.Location, out var next))
throw new InvalidOperationException("The HTTP redirect has no valid Location header.");
ValidateAddress(next);
if (fault is not null) throw new InvalidOperationException(fault);
target = next;
continue;
}
var body = NativeHttpProtocol.DecodeBody(response);
if (body.Length > limits.MaximumHtmlCharacters)
throw new BrowserLimitException("The decoded native HTTP body exceeds its character limit.");
return new(target, response.StatusCode, response.ContentType, body, false, response.ContentSecurityPolicy);
}
}
private static IPAddress Resolve(string host, Stopwatch watch, TimeSpan timeout)
{
if (TryIPv4(host, out var bytes)) return new IPAddress(bytes);
if (DnsConfig.Nameservers.Count == 0) throw new InvalidOperationException("No native DNS server is configured.");
if (DnsConfig.Nameservers[0] is not Address4)
throw new NotSupportedException("This native browser transport resolves DNS over IPv4 only.");
var milliseconds = Math.Min(5_000, Remaining(watch, timeout) - ConnectBudgetMilliseconds);
milliseconds = milliseconds / 10 * 10;
if (milliseconds <= 0) throw new TimeoutException("The HTTP deadline cannot cover DNS and TCP connect.");
var resolver = new DnsClient();
try
{
resolver.Connect(DnsConfig.Nameservers[0]);
resolver.SendQuery(host);
var address = resolver.Receive(milliseconds);
var result = address is Address4 ? new IPAddress(address.ToBytes().ToArray())
: throw new InvalidOperationException("Native DNS did not return an IPv4 address before its deadline.");
resolver.Close();
return result;
}
catch
{
resolver.Close();
throw;
}
}
private static int Receive(Socket socket, byte[] buffer, int offset, int count, Stopwatch watch, TimeSpan timeout)
{
while (true)
{
CheckDeadline(watch, timeout);
var available = socket.Available;
if (available > 0)
{
socket.ReceiveTimeout = Math.Max(1, Remaining(watch, timeout));
// Avoid the extra temporary copy allocated by the upstream
// Span overload; this adapter owns a reusable byte[] buffer.
return socket.Receive(buffer, offset, Math.Min(count, available), SocketFlags.None);
}
if (!socket.Connected)
{
// Recheck in case the final data and FIN arrived between the
// first Available read and the connection-state read.
if (socket.Available > 0) continue;
return 0;
}
// Poll outside Receive so the whole-operation deadline is checked
// between waits; 3.0.94 also supplies the receive timeout above.
TimerManager.Wait(1);
}
}
private static byte[] BuildRequest(Uri address)
{
var host = address.IsDefaultPort ? address.IdnHost : address.IdnHost + ":" + address.Port;
var path = string.IsNullOrEmpty(address.PathAndQuery) ? "/" : address.PathAndQuery;
var request = "GET " + path + " HTTP/1.1\r\nHost: " + host +
"\r\nUser-Agent: XFESystem/0.3-CSharp\r\nAccept: text/html,text/plain,text/markdown\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n";
if (request.Length > 4_096 || request.Any(character => character > 127 || character == '\0'))
throw new NotSupportedException("The native HTTP request exceeds its ASCII header limit.");
return Encoding.ASCII.GetBytes(request);
}
private static void ValidateAddress(Uri address)
{
if (!address.IsAbsoluteUri || !string.IsNullOrEmpty(address.UserInfo) || address.AbsoluteUri.Length > 2_048)
throw new NotSupportedException("Native HTTP requires an absolute URL without embedded credentials.");
if (address.Scheme == "https")
throw new NotSupportedException("HTTPS is unavailable: this native runtime has no verified TLS implementation. Certificate validation cannot be bypassed.");
if (address.Scheme != "http" || address.HostNameType is not (UriHostNameType.IPv4 or UriHostNameType.Dns))
throw new NotSupportedException("The native browser currently supports IPv4 HTTP URLs only.");
if (address.AbsoluteUri.Any(char.IsControl)) throw new ArgumentException("The URL contains control characters.");
}
private static bool TryIPv4(string host, out byte[] bytes)
{
bytes = new byte[4];
var fragments = host.Split('.');
if (fragments.Length != 4) return false;
for (var index = 0; index < 4; index++)
{
if (fragments[index].Length is 0 or > 3) return false;
var value = 0;
foreach (var character in fragments[index])
{
if (character is < '0' or > '9') return false;
value = value * 10 + character - '0';
}
if (value > 255) return false;
bytes[index] = (byte)value;
}
return true;
}
private static int Remaining(Stopwatch watch, TimeSpan timeout) =>
(int)Math.Max(0, timeout.TotalMilliseconds - watch.Elapsed.TotalMilliseconds);
private static void CheckDeadline(Stopwatch watch, TimeSpan timeout)
{
if (Remaining(watch, timeout) <= 0) throw new TimeoutException("The native HTTP operation timed out.");
}
}
#endif
internal sealed record NativeHttpResponse(int StatusCode, string ContentType, byte[] Body,
string? Location, string? ContentSecurityPolicy);
/// <summary>Bounded HTTP framing shared verbatim by the native adapter and host protocol tests.</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;
do
{
var 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);
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);
}
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;
}
}
}