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;

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<string,string> Headers, string StatusLine);

/// <summary>Bounded HTTP framing over actual stream bytes, retaining parsed response headers.</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; 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<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;
        }
    }
}