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; /// Guest e1000/TCP plus managed TLS. No host socket, proxy, cache or certificate bypass. 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("= 0 && (start = body.IndexOf('>', start)) >= 0) { int end = body.IndexOf(" 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(" 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(); } }