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

XFEExtension

【DLL】XFE各类拓展是一个C#的DLL库,旨在优化C#代码中常用语句的使用,并提供更简洁的访问方式,同时提供Xunit测试框架,快速搭建服务器/客户端,免费ChatGPTAPI接口,免费通讯服务器,XFE下载器,新增格式等

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Buffers;
using System.Globalization;
using System.Net;
using System.Text;

namespace XFEExtension.NetCore.CyberComm;

internal sealed class CyberCommHttpProtocolException(HttpStatusCode statusCode, string message) : Exception(message)
{
    public HttpStatusCode StatusCode { get; } = statusCode;
}

internal sealed record CyberCommParsedRequest(
    string Method,
    string Target,
    Uri RequestUri,
    IReadOnlyDictionary<string, IReadOnlyList<string>> Headers,
    IReadOnlyDictionary<string, IReadOnlyList<string>> Query,
    ReadOnlyMemory<byte> Body,
    bool KeepAlive,
    bool IsWebSocketRequest,
    string? WebSocketKey);

internal sealed class CyberCommHttpConnectionReader(Stream stream, CyberCommLimitOptions limits)
{
    private readonly byte[] _buffer = ArrayPool<byte>.Shared.Rent(16 * 1024);
    private int _start;
    private int _end;
    private bool _disposed;
    public bool RequestStarted { get; private set; }

    public async ValueTask<CyberCommParsedRequest?> ReadRequestAsync(string scheme, CancellationToken cancellationToken)
    {
        RequestStarted = _end > _start;
        var requestLine = await ReadLineAsync(limits.MaxRequestLineBytes, cancellationToken).ConfigureAwait(false);
        if (requestLine is null) return null;
        if (requestLine.Length == 0)
            throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Empty request line");

        var firstSpace = requestLine.IndexOf(' ');
        var secondSpace = firstSpace < 0 ? -1 : requestLine.IndexOf(' ', firstSpace + 1);
        if (firstSpace <= 0 || secondSpace <= firstSpace + 1 || requestLine.IndexOf(' ', secondSpace + 1) >= 0)
            throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Request line must use exactly two single spaces");
        var requestParts = new[] { requestLine[..firstSpace], requestLine[(firstSpace + 1)..secondSpace], requestLine[(secondSpace + 1)..] };
        if (requestParts[2] != "HTTP/1.1")
            throw new CyberCommHttpProtocolException(HttpStatusCode.HttpVersionNotSupported, "Only HTTP/1.1 is supported");
        if (!IsToken(requestParts[0]))
            throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Invalid HTTP method");
        if (!requestParts[1].StartsWith('/') || requestParts[1].Contains('#') ||
            requestParts[1].Any(character => char.IsWhiteSpace(character) || char.IsControl(character)) ||
            !HasValidPercentEncoding(requestParts[1]) || ContainsEncodedDelimiter(requestParts[1]))
            throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Invalid request target");

        var headers = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
        var headerBytes = 0;
        while (true)
        {
            var line = await ReadLineAsync(limits.MaxHeaderBytes, cancellationToken).ConfigureAwait(false)
                ?? throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Unexpected end of headers");
            headerBytes += Encoding.ASCII.GetByteCount(line) + 2;
            if (headerBytes > limits.MaxHeaderBytes)
                throw new CyberCommHttpProtocolException((HttpStatusCode)431, "Request headers are too large");
            if (line.Length == 0) break;
            if (char.IsWhiteSpace(line[0]))
                throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Folded headers are not supported");
            var separator = line.IndexOf(':');
            if (separator <= 0 || !IsToken(line.AsSpan(0, separator)))
                throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Invalid header");
            var name = line[..separator];
            var value = line[(separator + 1)..].Trim();
            if (value.Any(character => character is '\r' or '\n' or '\0'))
                throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Invalid header value");
            if (!headers.TryGetValue(name, out var values))
                headers.Add(name, values = []);
            values.Add(value);
        }

        if (!headers.TryGetValue("Host", out var hostValues) || hostValues.Count != 1 || string.IsNullOrWhiteSpace(hostValues[0]))
            throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "A single Host header is required");
        var host = hostValues[0];
        if (host.Any(character => char.IsWhiteSpace(character) || char.IsControl(character)) ||
            !Uri.TryCreate($"{scheme}://{host}", UriKind.Absolute, out var authorityUri) ||
            authorityUri.UserInfo.Length != 0 || authorityUri.AbsolutePath != "/" || authorityUri.Query.Length != 0 || authorityUri.Fragment.Length != 0 ||
            !Uri.TryCreate(authorityUri.GetLeftPart(UriPartial.Authority) + requestParts[1], UriKind.Absolute, out var requestUri))
            throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Invalid request URI");

        var contentLength = ParseContentLength(headers);
        var chunked = ParseTransferEncoding(headers);
        if (contentLength is not null && chunked)
            throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Content-Length and Transfer-Encoding cannot be combined");

        if (headers.TryGetValue("Expect", out var expectValues) &&
            expectValues.Any(value => value.Equals("100-continue", StringComparison.OrdinalIgnoreCase)) &&
            (contentLength > 0 || chunked))
        {
            await stream.WriteAsync("HTTP/1.1 100 Continue\r\n\r\n"u8.ToArray(), cancellationToken).ConfigureAwait(false);
            await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
        }

        byte[] body;
        if (chunked)
            body = await ReadChunkedBodyAsync(cancellationToken).ConfigureAwait(false);
        else if (contentLength is > 0)
        {
            if (contentLength > limits.MaxHttpBodyBytes)
                throw new CyberCommHttpProtocolException(HttpStatusCode.RequestEntityTooLarge, "Request body is too large");
            body = new byte[checked((int)contentLength.Value)];
            await ReadExactlyAsync(body, cancellationToken).ConfigureAwait(false);
        }
        else
            body = [];

        var connectionTokens = GetCommaSeparatedValues(headers, "Connection");
        var keepAlive = !connectionTokens.Contains("close", StringComparer.OrdinalIgnoreCase);
        var upgradeTokens = GetCommaSeparatedValues(headers, "Upgrade");
        var isWebSocket = requestParts[0] == "GET" &&
                          connectionTokens.Contains("upgrade", StringComparer.OrdinalIgnoreCase) &&
                          upgradeTokens.Contains("websocket", StringComparer.OrdinalIgnoreCase);
        var webSocketKey = headers.TryGetValue("Sec-WebSocket-Key", out var keyValues) && keyValues.Count == 1 ? keyValues[0] : null;

        return new(
            requestParts[0],
            requestParts[1],
            requestUri,
            CyberCommHttpCollections.Freeze(headers),
            ParseQuery(requestUri.Query),
            body,
            keepAlive,
            isWebSocket,
            webSocketKey);
    }

    public Stream DetachStream()
    {
        var prefix = _end > _start ? _buffer.AsMemory(_start, _end - _start).ToArray() : [];
        DisposeBuffer();
        return prefix.Length == 0 ? stream : new CyberCommPrebufferedStream(stream, prefix);
    }

    public void DisposeBuffer() => DisposeBufferCore();

    private void DisposeBufferCore()
    {
        if (_disposed) return;
        _disposed = true;
        ArrayPool<byte>.Shared.Return(_buffer);
    }

    private async ValueTask<string?> ReadLineAsync(int maxBytes, CancellationToken cancellationToken)
    {
        var writer = new ArrayBufferWriter<byte>();
        while (true)
        {
            for (var index = _start; index < _end; index++)
            {
                if (_buffer[index] != (byte)'\n') continue;
                var count = index - _start;
                if (count == 0 || _buffer[index - 1] != (byte)'\r')
                    throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "HTTP lines must end with CRLF");
                writer.Write(_buffer.AsSpan(_start, count - 1));
                _start = index + 1;
                if (writer.WrittenCount > maxBytes)
                    throw new CyberCommHttpProtocolException(HttpStatusCode.RequestUriTooLong, "HTTP line is too large");
                ValidateLineBytes(writer.WrittenSpan);
                return Encoding.ASCII.GetString(writer.WrittenSpan);
            }

            if (_end > _start)
            {
                writer.Write(_buffer.AsSpan(_start, _end - _start));
                _start = _end;
                if (writer.WrittenCount > maxBytes)
                    throw new CyberCommHttpProtocolException(HttpStatusCode.RequestUriTooLong, "HTTP line is too large");
            }

            _start = 0;
            _end = await stream.ReadAsync(_buffer, cancellationToken).ConfigureAwait(false);
            if (_end > 0) RequestStarted = true;
            if (_end == 0)
            {
                if (writer.WrittenCount == 0) return null;
                throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Unexpected end of HTTP line");
            }
        }
    }

    private async ValueTask ReadExactlyAsync(Memory<byte> destination, CancellationToken cancellationToken)
    {
        var written = 0;
        while (written < destination.Length)
        {
            if (_start < _end)
            {
                var count = Math.Min(destination.Length - written, _end - _start);
                _buffer.AsMemory(_start, count).CopyTo(destination[written..]);
                _start += count;
                written += count;
                continue;
            }
            _start = 0;
            _end = await stream.ReadAsync(_buffer, cancellationToken).ConfigureAwait(false);
            if (_end == 0)
                throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Unexpected end of request body");
        }
    }

    private async ValueTask<byte[]> ReadChunkedBodyAsync(CancellationToken cancellationToken)
    {
        using var output = new MemoryStream();
        while (true)
        {
            var sizeLine = await ReadLineAsync(128, cancellationToken).ConfigureAwait(false)
                ?? throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Unexpected end of chunked body");
            var extensionSeparator = sizeLine.IndexOf(';');
            var sizeText = extensionSeparator < 0 ? sizeLine : sizeLine[..extensionSeparator];
            if (!long.TryParse(sizeText.Trim(), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var chunkSize) || chunkSize < 0)
                throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Invalid chunk size");
            if (chunkSize > limits.MaxHttpChunkBytes || output.Length + chunkSize > limits.MaxHttpBodyBytes)
                throw new CyberCommHttpProtocolException(HttpStatusCode.RequestEntityTooLarge, "Chunked request body is too large");
            if (chunkSize == 0)
            {
                var trailerBytes = 0;
                while (true)
                {
                    var trailer = await ReadLineAsync(limits.MaxHeaderBytes, cancellationToken).ConfigureAwait(false)
                        ?? throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Unexpected end of trailers");
                    trailerBytes += trailer.Length + 2;
                    if (trailerBytes > limits.MaxHeaderBytes)
                        throw new CyberCommHttpProtocolException((HttpStatusCode)431, "Trailers are too large");
                    if (trailer.Length == 0) break;
                    if (trailer.IndexOf(':') <= 0)
                        throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Invalid trailer");
                }
                return output.ToArray();
            }

            var chunk = new byte[checked((int)chunkSize)];
            await ReadExactlyAsync(chunk, cancellationToken).ConfigureAwait(false);
            await output.WriteAsync(chunk, cancellationToken).ConfigureAwait(false);
            var terminator = new byte[2];
            await ReadExactlyAsync(terminator, cancellationToken).ConfigureAwait(false);
            if (terminator[0] != '\r' || terminator[1] != '\n')
                throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Invalid chunk terminator");
        }
    }

    private static long? ParseContentLength(Dictionary<string, List<string>> headers)
    {
        if (!headers.TryGetValue("Content-Length", out var values)) return null;
        var allValues = values.SelectMany(value => value.Split(',')).Select(value => value.Trim()).ToArray();
        if (allValues.Length == 0 || allValues.Any(value => !long.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out _)))
            throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Invalid Content-Length");
        var parsed = allValues.Select(value => long.Parse(value, CultureInfo.InvariantCulture)).ToArray();
        if (parsed.Any(value => value < 0) || parsed.Distinct().Count() != 1)
            throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "Conflicting Content-Length headers");
        return parsed[0];
    }

    private static bool ParseTransferEncoding(Dictionary<string, List<string>> headers)
    {
        var values = GetCommaSeparatedValues(headers, "Transfer-Encoding");
        if (values.Count == 0) return false;
        if (values.Count != 1 || !values[0].Equals("chunked", StringComparison.OrdinalIgnoreCase))
            throw new CyberCommHttpProtocolException(HttpStatusCode.NotImplemented, "Only chunked Transfer-Encoding is supported");
        return true;
    }

    private static List<string> GetCommaSeparatedValues(Dictionary<string, List<string>> headers, string name)
        => headers.TryGetValue(name, out var values)
            ? values.SelectMany(value => value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)).ToList()
            : [];

    private static IReadOnlyDictionary<string, IReadOnlyList<string>> ParseQuery(string query)
    {
        var result = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
        foreach (var part in query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries))
        {
            var separator = part.IndexOf('=');
            var key = Uri.UnescapeDataString(separator < 0 ? part : part[..separator]);
            var value = Uri.UnescapeDataString(separator < 0 ? string.Empty : part[(separator + 1)..]);
            if (!result.TryGetValue(key, out var values)) result.Add(key, values = []);
            values.Add(value);
        }
        return CyberCommHttpCollections.Freeze(result);
    }

    private static bool IsToken(string value) => IsToken(value.AsSpan());

    private static bool IsToken(ReadOnlySpan<char> value)
    {
        if (value.IsEmpty) return false;
        foreach (var character in value)
        {
            if (!(char.IsAsciiLetterOrDigit(character) || character is '!' or '#' or '$' or '%' or '&' or '\'' or '*' or '+' or '-' or '.' or '^' or '_' or '`' or '|' or '~'))
                return false;
        }
        return true;
    }

    private static void ValidateLineBytes(ReadOnlySpan<byte> bytes)
    {
        foreach (var value in bytes)
        {
            if (value > 0x7f || value < 0x20 && value != (byte)'\t' || value == 0x7f)
                throw new CyberCommHttpProtocolException(HttpStatusCode.BadRequest, "HTTP line contains invalid bytes");
        }
    }

    private static bool HasValidPercentEncoding(string value)
    {
        for (var index = 0; index < value.Length; index++)
        {
            if (value[index] != '%') continue;
            if (index + 2 >= value.Length || !Uri.IsHexDigit(value[index + 1]) || !Uri.IsHexDigit(value[index + 2])) return false;
            index += 2;
        }
        return true;
    }

    private static bool ContainsEncodedDelimiter(string value) =>
        value.Contains("%2f", StringComparison.OrdinalIgnoreCase) ||
        value.Contains("%5c", StringComparison.OrdinalIgnoreCase) ||
        value.Contains("%0d", StringComparison.OrdinalIgnoreCase) ||
        value.Contains("%0a", StringComparison.OrdinalIgnoreCase) ||
        value.Contains("%00", StringComparison.OrdinalIgnoreCase);
}

internal sealed class CyberCommPrebufferedStream(Stream inner, byte[] prefix) : Stream
{
    private int _position;
    public override bool CanRead => inner.CanRead;
    public override bool CanSeek => false;
    public override bool CanWrite => inner.CanWrite;
    public override long Length => throw new NotSupportedException();
    public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
    public override void Flush() => inner.Flush();
    public override Task FlushAsync(CancellationToken cancellationToken) => inner.FlushAsync(cancellationToken);
    public override int Read(byte[] buffer, int offset, int count)
    {
        if (_position < prefix.Length)
        {
            var copied = Math.Min(count, prefix.Length - _position);
            prefix.AsSpan(_position, copied).CopyTo(buffer.AsSpan(offset, copied));
            _position += copied;
            return copied;
        }
        return inner.Read(buffer, offset, count);
    }
    public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
    {
        if (_position < prefix.Length)
        {
            var copied = Math.Min(buffer.Length, prefix.Length - _position);
            prefix.AsMemory(_position, copied).CopyTo(buffer);
            _position += copied;
            return copied;
        }
        return await inner.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
    }
    public override void Write(byte[] buffer, int offset, int count) => inner.Write(buffer, offset, count);
    public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => inner.WriteAsync(buffer, cancellationToken);
    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
}