XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
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;
        }
    }
}