using System.Globalization;
using System.Net;
using System.Text;
using Org.BouncyCastle.Crypto.Digests;
using XFESystem.Browser;
using XFESystem.NativeRuntime.Application.Network;
using XFESystem.NativeRuntime.Application.Security;
using XFESystem.Tls;
using WebResponse = XFESystem.Browser.WebResponse;
namespace XFESystem.NativeRuntime.Application.Web;
/// <summary>Guest e1000/TCP plus managed TLS. No host socket, proxy, cache or certificate bypass.</summary>
internal sealed class NativeWebTransport : IWebTransport, IWebTransportCapabilities
{
private static readonly object gate = new();
private static TlsCertificateValidator? certificates;
private static int requestId;
public bool IsNative => true;
public bool SupportsHttp => true;
public bool SupportsHttps => true;
internal NativeWebTransport() => NativeNetwork.ConfigureEntropy(NativeTlsSecurity.Random32);
private static TlsCertificateValidator Roots()
{
if (certificates is not null) return certificates;
long started = NativePlatform.Milliseconds();
NativePlatform.Write("XFE_NATIVE_TLS_TRUST_IMPORT_BEGIN\n");
using var stream = typeof(NativeWebTransport).Assembly.GetManifestResourceStream("Security/MozillaRoots.pem")
?? throw new IOException("The explicit trusted root snapshot is missing.");
using var reader = new StreamReader(stream, Encoding.ASCII);
certificates = TlsCertificateValidator.FromPem(reader.ReadToEnd());
NativePlatform.Write("XFE_NATIVE_TLS_PREREQUISITES|Clock|CMOS_UTC|EntropyCapabilities|" + NativeTlsSecurity.Capabilities +
"|RootSnapshot|Mozilla20260925|HostCertificateStore|0|ImportMilliseconds|" + (NativePlatform.Milliseconds() - started) + "\n");
return certificates;
}
public WebResponse Get(Uri address, BrowserLimits limits)
{
try { return GetCore(address, limits); }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
var details = new StringBuilder();
for (Exception? current = error; current is not null && details.Length < 8192; current = current.InnerException)
details.Append(current.GetType().FullName).Append(": ").Append(current.Message).Append('\n').Append(current.StackTrace).Append('\n');
NativePlatform.Write("XFE_NATIVE_WEB_ERROR_BEGIN\n" + details + "XFE_NATIVE_WEB_ERROR_END\n");
throw;
}
}
private static WebResponse GetCore(Uri address, BrowserLimits limits)
{
ArgumentNullException.ThrowIfNull(address); ArgumentNullException.ThrowIfNull(limits);
if (limits.NetworkTimeout <= TimeSpan.Zero || limits.NetworkTimeout > TimeSpan.FromMinutes(2))
throw new ArgumentOutOfRangeException(nameof(limits));
long deadline = checked(NativePlatform.Milliseconds() + (long)limits.NetworkTimeout.TotalMilliseconds);
if (!Monitor.TryEnter(gate, Remaining(deadline))) throw new TimeoutException("The native web transport ownership deadline expired.");
try
{
Uri target = address;
for (int redirects = 0; ; redirects++)
{
ValidateAddress(target);
int id = ++requestId;
string host = target.IdnHost;
string authority = target.IsDefaultPort ? host : host + ":" + target.Port.ToString(CultureInfo.InvariantCulture);
string headers = "GET " + target.PathAndQuery + " HTTP/1.1\r\nHost: " + authority +
"\r\nUser-Agent: Mozilla/5.0 (XFESystem; x86_64) XFEWeb/0.3\r\n" +
"Accept: text/html,application/xhtml+xml,text/css,application/javascript,text/plain,*/*;q=0.5\r\n" +
"Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n";
NativePlatform.Write("XFE_NATIVE_WEB_REQUEST|" + id + "|" + target.AbsoluteUri + "\n");
using Stream tcp = NativeNetwork.OpenTcp(host, target.Port, Remaining(deadline));
using var bounded = new DeadlineStream(tcp, deadline);
long handshakeStarted = NativePlatform.Milliseconds();
using TlsClientConnection? tls = target.Scheme == "https"
? new TlsClientConnection(bounded, host, Roots(), NativeTlsSecurity.Context) : null;
if (tls is not null) NativePlatform.Write("XFE_NATIVE_TLS_CONNECTED|Host|" + host + "|Verified|" +
(tls.CertificateValidated ? 1 : 0) + "|Protocol|" + tls.Protocol + "|InitializationAndHandshakeMilliseconds|" +
(NativePlatform.Milliseconds() - handshakeStarted) + "\n");
Stream wire = tls?.Stream ?? bounded;
byte[] request = Encoding.ASCII.GetBytes(headers);
wire.Write(request, 0, request.Length); wire.Flush();
var response = NativeHttpProtocol.ReadResponse((buffer, offset, count) =>
wire.Read(buffer, offset, count), checked(limits.MaximumHtmlCharacters * 4));
string body = NativeHttpProtocol.DecodeBody(response);
if (body.Length > limits.MaximumHtmlCharacters) throw new BrowserLimitException("The decoded response exceeds its character limit.");
bool verified = tls?.CertificateValidated == true;
EmitResponse(id, target, headers, response, body, verified, tls?.Protocol ?? "HTTP");
if (response.StatusCode is 301 or 302 or 303 or 307 or 308)
{
if (redirects >= limits.MaximumRedirects) throw new IOException("The HTTP redirect limit was reached.");
if (string.IsNullOrWhiteSpace(response.Location) || !Uri.TryCreate(target, response.Location, out var next))
throw new IOException("The redirect Location is invalid.");
ValidateAddress(next);
if (target.Scheme == "https" && next.Scheme != "https") throw new IOException("HTTPS redirects cannot downgrade to HTTP.");
target = next; continue;
}
return new(target, response.StatusCode, response.ContentType, body, verified, response.ContentSecurityPolicy, Hash(body));
}
}
finally { Monitor.Exit(gate); }
}
private static int Remaining(long deadline)
{
long remaining = deadline - NativePlatform.Milliseconds();
if (remaining <= 0) throw new TimeoutException("The native web request deadline expired.");
return (int)Math.Min(remaining, int.MaxValue);
}
private static void ValidateAddress(Uri address)
{
if (!address.IsAbsoluteUri || address.Scheme is not ("http" or "https") ||
string.IsNullOrEmpty(address.Host) || !string.IsNullOrEmpty(address.UserInfo))
throw new NotSupportedException("Only HTTP and HTTPS URLs without embedded credentials are supported.");
}
private static string Hash(string body) => Hash(Encoding.UTF8.GetBytes(body));
private static string Hash(byte[] bytes)
{
byte[] digest = new byte[32];
var sha = new Sha256Digest(); sha.BlockUpdate(bytes, 0, bytes.Length); sha.DoFinal(digest, 0);
return Convert.ToHexString(digest).ToLowerInvariant();
}
internal static string Quote(string? value)
{
if (value is null) return "null";
var builder = new StringBuilder("\"");
foreach (char c in value)
if (c is '\\' or '"') builder.Append('\\').Append(c);
else if (c < 32) builder.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
else builder.Append(c);
return builder.Append('"').ToString();
}
private static void EmitResponse(int id, Uri address, string requestHeaders, NativeHttpResponse response, string body, bool verified, string protocol)
{
string title = ""; int start = body.IndexOf("<title", StringComparison.OrdinalIgnoreCase);
if (start >= 0 && (start = body.IndexOf('>', start)) >= 0)
{ int end = body.IndexOf("</title", start, StringComparison.OrdinalIgnoreCase); if (end > start) title = WebUtility.HtmlDecode(body.Substring(start + 1, Math.Min(512, end - start - 1))); }
string responseHeaders = response.StatusLine + "\r\n" + string.Join("\r\n", response.Headers.Select(pair => pair.Key + ": " + pair.Value));
string json = "{\"Kind\":\"ActualGuestWebResponse\",\"RequestId\":" + id + ",\"RequestUri\":" + Quote(address.AbsoluteUri) +
",\"ResponseUri\":" + Quote(address.AbsoluteUri) + ",\"StatusCode\":" + response.StatusCode + ",\"ContentType\":" + Quote(response.ContentType) +
",\"CertificateValidated\":" + (verified ? "true" : "false") + ",\"NativeTransport\":true,\"Protocol\":" + Quote(protocol) +
",\"RequestHeaders\":" + Quote(requestHeaders) + ",\"ResponseHeaders\":" + Quote(responseHeaders) +
",\"BodyBytes\":" + response.Body.Length + ",\"BodySha256\":" + Quote(Hash(body)) +
",\"RawBodySha256\":" + Quote(Hash(response.Body)) +
",\"BodyHashMeaning\":\"UTF8 of decoded HTML/text\",\"HtmlTitle\":" + Quote(title) +
",\"HasHtml\":" + (body.Contains("<html", StringComparison.OrdinalIgnoreCase) ? "true" : "false") +
",\"HasHead\":" + (body.Contains("<head", StringComparison.OrdinalIgnoreCase) ? "true" : "false") +
",\"Location\":" + Quote(response.Location) + "}";
NativePlatform.Write("XFE_NATIVE_WEB_RESPONSE_BEGIN\n" + json + "\nXFE_NATIVE_WEB_RESPONSE_END\n");
}
private sealed class DeadlineStream(Stream inner, long deadline) : Stream
{
public override bool CanRead => true; public override bool CanWrite => true; public override bool CanSeek => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
private void Check()
{
int left = Remaining(deadline);
if (!inner.CanTimeout) throw new IOException("The native TCP stream cannot enforce bounded web I/O.");
inner.ReadTimeout = left; inner.WriteTimeout = left;
}
public override int Read(byte[] buffer, int offset, int count) { Check(); int read = inner.Read(buffer, offset, count); Check(); return read; }
public override void Write(byte[] buffer, int offset, int count) { Check(); inner.Write(buffer, offset, count); Check(); }
public override void Flush() { Check(); inner.Flush(); }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
}
}
using System.Globalization;
using System.Net;
using System.Text;
using Org.BouncyCastle.Crypto.Digests;
using XFESystem.Browser;
using XFESystem.NativeRuntime.Application.Network;
using XFESystem.NativeRuntime.Application.Security;
using XFESystem.Tls;
using WebResponse = XFESystem.Browser.WebResponse;
namespace XFESystem.NativeRuntime.Application.Web;
/// <summary>Guest e1000/TCP plus managed TLS. No host socket, proxy, cache or certificate bypass.</summary>
internal sealed class NativeWebTransport : IWebTransport, IWebTransportCapabilities
{
private static readonly object gate = new();
private static TlsCertificateValidator? certificates;
private static int requestId;
public bool IsNative => true;
public bool SupportsHttp => true;
public bool SupportsHttps => true;
internal NativeWebTransport() => NativeNetwork.ConfigureEntropy(NativeTlsSecurity.Random32);
private static TlsCertificateValidator Roots()
{
if (certificates is not null) return certificates;
long started = NativePlatform.Milliseconds();
NativePlatform.Write("XFE_NATIVE_TLS_TRUST_IMPORT_BEGIN\n");
using var stream = typeof(NativeWebTransport).Assembly.GetManifestResourceStream("Security/MozillaRoots.pem")
?? throw new IOException("The explicit trusted root snapshot is missing.");
using var reader = new StreamReader(stream, Encoding.ASCII);
certificates = TlsCertificateValidator.FromPem(reader.ReadToEnd());
NativePlatform.Write("XFE_NATIVE_TLS_PREREQUISITES|Clock|CMOS_UTC|EntropyCapabilities|" + NativeTlsSecurity.Capabilities +
"|RootSnapshot|Mozilla20260925|HostCertificateStore|0|ImportMilliseconds|" + (NativePlatform.Milliseconds() - started) + "\n");
return certificates;
}
public WebResponse Get(Uri address, BrowserLimits limits)
{
try { return GetCore(address, limits); }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
var details = new StringBuilder();
for (Exception? current = error; current is not null && details.Length < 8192; current = current.InnerException)
details.Append(current.GetType().FullName).Append(": ").Append(current.Message).Append('\n').Append(current.StackTrace).Append('\n');
NativePlatform.Write("XFE_NATIVE_WEB_ERROR_BEGIN\n" + details + "XFE_NATIVE_WEB_ERROR_END\n");
throw;
}
}
private static WebResponse GetCore(Uri address, BrowserLimits limits)
{
ArgumentNullException.ThrowIfNull(address); ArgumentNullException.ThrowIfNull(limits);
if (limits.NetworkTimeout <= TimeSpan.Zero || limits.NetworkTimeout > TimeSpan.FromMinutes(2))
throw new ArgumentOutOfRangeException(nameof(limits));
long deadline = checked(NativePlatform.Milliseconds() + (long)limits.NetworkTimeout.TotalMilliseconds);
if (!Monitor.TryEnter(gate, Remaining(deadline))) throw new TimeoutException("The native web transport ownership deadline expired.");
try
{
Uri target = address;
for (int redirects = 0; ; redirects++)
{
ValidateAddress(target);
int id = ++requestId;
string host = target.IdnHost;
string authority = target.IsDefaultPort ? host : host + ":" + target.Port.ToString(CultureInfo.InvariantCulture);
string headers = "GET " + target.PathAndQuery + " HTTP/1.1\r\nHost: " + authority +
"\r\nUser-Agent: Mozilla/5.0 (XFESystem; x86_64) XFEWeb/0.3\r\n" +
"Accept: text/html,application/xhtml+xml,text/css,application/javascript,text/plain,*/*;q=0.5\r\n" +
"Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n";
NativePlatform.Write("XFE_NATIVE_WEB_REQUEST|" + id + "|" + target.AbsoluteUri + "\n");
using Stream tcp = NativeNetwork.OpenTcp(host, target.Port, Remaining(deadline));
using var bounded = new DeadlineStream(tcp, deadline);
long handshakeStarted = NativePlatform.Milliseconds();
using TlsClientConnection? tls = target.Scheme == "https"
? new TlsClientConnection(bounded, host, Roots(), NativeTlsSecurity.Context) : null;
if (tls is not null) NativePlatform.Write("XFE_NATIVE_TLS_CONNECTED|Host|" + host + "|Verified|" +
(tls.CertificateValidated ? 1 : 0) + "|Protocol|" + tls.Protocol + "|InitializationAndHandshakeMilliseconds|" +
(NativePlatform.Milliseconds() - handshakeStarted) + "\n");
Stream wire = tls?.Stream ?? bounded;
byte[] request = Encoding.ASCII.GetBytes(headers);
wire.Write(request, 0, request.Length); wire.Flush();
var response = NativeHttpProtocol.ReadResponse((buffer, offset, count) =>
wire.Read(buffer, offset, count), checked(limits.MaximumHtmlCharacters * 4));
string body = NativeHttpProtocol.DecodeBody(response);
if (body.Length > limits.MaximumHtmlCharacters) throw new BrowserLimitException("The decoded response exceeds its character limit.");
bool verified = tls?.CertificateValidated == true;
EmitResponse(id, target, headers, response, body, verified, tls?.Protocol ?? "HTTP");
if (response.StatusCode is 301 or 302 or 303 or 307 or 308)
{
if (redirects >= limits.MaximumRedirects) throw new IOException("The HTTP redirect limit was reached.");
if (string.IsNullOrWhiteSpace(response.Location) || !Uri.TryCreate(target, response.Location, out var next))
throw new IOException("The redirect Location is invalid.");
ValidateAddress(next);
if (target.Scheme == "https" && next.Scheme != "https") throw new IOException("HTTPS redirects cannot downgrade to HTTP.");
target = next; continue;
}
return new(target, response.StatusCode, response.ContentType, body, verified, response.ContentSecurityPolicy, Hash(body));
}
}
finally { Monitor.Exit(gate); }
}
private static int Remaining(long deadline)
{
long remaining = deadline - NativePlatform.Milliseconds();
if (remaining <= 0) throw new TimeoutException("The native web request deadline expired.");
return (int)Math.Min(remaining, int.MaxValue);
}
private static void ValidateAddress(Uri address)
{
if (!address.IsAbsoluteUri || address.Scheme is not ("http" or "https") ||
string.IsNullOrEmpty(address.Host) || !string.IsNullOrEmpty(address.UserInfo))
throw new NotSupportedException("Only HTTP and HTTPS URLs without embedded credentials are supported.");
}
private static string Hash(string body) => Hash(Encoding.UTF8.GetBytes(body));
private static string Hash(byte[] bytes)
{
byte[] digest = new byte[32];
var sha = new Sha256Digest(); sha.BlockUpdate(bytes, 0, bytes.Length); sha.DoFinal(digest, 0);
return Convert.ToHexString(digest).ToLowerInvariant();
}
internal static string Quote(string? value)
{
if (value is null) return "null";
var builder = new StringBuilder("\"");
foreach (char c in value)
if (c is '\\' or '"') builder.Append('\\').Append(c);
else if (c < 32) builder.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
else builder.Append(c);
return builder.Append('"').ToString();
}
private static void EmitResponse(int id, Uri address, string requestHeaders, NativeHttpResponse response, string body, bool verified, string protocol)
{
string title = ""; int start = body.IndexOf("<title", StringComparison.OrdinalIgnoreCase);
if (start >= 0 && (start = body.IndexOf('>', start)) >= 0)
{ int end = body.IndexOf("</title", start, StringComparison.OrdinalIgnoreCase); if (end > start) title = WebUtility.HtmlDecode(body.Substring(start + 1, Math.Min(512, end - start - 1))); }
string responseHeaders = response.StatusLine + "\r\n" + string.Join("\r\n", response.Headers.Select(pair => pair.Key + ": " + pair.Value));
string json = "{\"Kind\":\"ActualGuestWebResponse\",\"RequestId\":" + id + ",\"RequestUri\":" + Quote(address.AbsoluteUri) +
",\"ResponseUri\":" + Quote(address.AbsoluteUri) + ",\"StatusCode\":" + response.StatusCode + ",\"ContentType\":" + Quote(response.ContentType) +
",\"CertificateValidated\":" + (verified ? "true" : "false") + ",\"NativeTransport\":true,\"Protocol\":" + Quote(protocol) +
",\"RequestHeaders\":" + Quote(requestHeaders) + ",\"ResponseHeaders\":" + Quote(responseHeaders) +
",\"BodyBytes\":" + response.Body.Length + ",\"BodySha256\":" + Quote(Hash(body)) +
",\"RawBodySha256\":" + Quote(Hash(response.Body)) +
",\"BodyHashMeaning\":\"UTF8 of decoded HTML/text\",\"HtmlTitle\":" + Quote(title) +
",\"HasHtml\":" + (body.Contains("<html", StringComparison.OrdinalIgnoreCase) ? "true" : "false") +
",\"HasHead\":" + (body.Contains("<head", StringComparison.OrdinalIgnoreCase) ? "true" : "false") +
",\"Location\":" + Quote(response.Location) + "}";
NativePlatform.Write("XFE_NATIVE_WEB_RESPONSE_BEGIN\n" + json + "\nXFE_NATIVE_WEB_RESPONSE_END\n");
}
private sealed class DeadlineStream(Stream inner, long deadline) : Stream
{
public override bool CanRead => true; public override bool CanWrite => true; public override bool CanSeek => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
private void Check()
{
int left = Remaining(deadline);
if (!inner.CanTimeout) throw new IOException("The native TCP stream cannot enforce bounded web I/O.");
inner.ReadTimeout = left; inner.WriteTimeout = left;
}
public override int Read(byte[] buffer, int offset, int count) { Check(); int read = inner.Read(buffer, offset, count); Check(); return read; }
public override void Write(byte[] buffer, int offset, int count) { Check(); inner.Write(buffer, offset, count); Check(); }
public override void Flush() { Check(); inner.Flush(); }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
}
}