v9.10.2019.0101  全面巩固批量Insert/Update/Upsert,支持数据备份、恢复和同步,支持实体列表保存到文件以及加载
大石头 authored at 2019-01-01 13:38:33
12.10 KiB
X
using System.ComponentModel;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using NewLife;
using NewLife.Net;
using Xunit;

namespace XUnitTest.Net;

/// <summary>WebSocket 客户端接收侧协议行为测试。RFC 6455 §5.1/§5.5 要求客户端检出违规帧时失败连接(1002),
/// 且不得把违规帧交付业务、不得按违规内容回写;§5.5.1 要求收到 Close 时回写关闭帧并关闭连接。
/// 使用裸套接字服务端构造帧,不依赖自家服务端实现</summary>
[Collection("Net.D")]
public class WebSocketClientProtocolTests
{
    [Fact]
    [DisplayName("ws协议_服务端带掩码帧_客户端以1002失败连接且不交付消息")]
    public async Task ServerMaskedFrame_ClientFails1002()
    {
        using var listener = StartListener(out var port);

        // 服务端方向违规:带掩码的文本帧(RFC 6455 §5.1 服务端不得掩码)
        var key = new Byte[] { 0x11, 0x22, 0x33, 0x44 };
        var masked = new Byte[] { 0x81, 0x80 | 2, key[0], key[1], key[2], key[3], (Byte)('h' ^ key[0]), (Byte)('i' ^ key[1]) };
        var server = AcceptAndReplyAsync(listener, masked);

        using var client = new WebSocketClient($"ws://127.0.0.1:{port}/ws") { Timeout = 5_000 };
        Assert.True(await client.OpenAsync());

        var (opcode, status) = await server.WaitAsync(TimeSpan.FromSeconds(10));

        // 客户端应答的首帧必须是关闭帧 1002,而不是把掩码负载当业务数据
        Assert.Equal(0x88, opcode);                         // FIN=1 + Close
        Assert.Equal(1002, status);

        // 拉取 API 不得交付该违规帧:连接关闭后返回 null
        var msg = await client.ReceiveMessageAsync().WaitAsync(TimeSpan.FromSeconds(5));
        Assert.Null(msg);
    }

    [Fact]
    [DisplayName("ws协议_超大Ping控制帧_客户端不回Pong并以1002失败连接")]
    public async Task OversizedPing_ClientFails1002WithoutPong()
    {
        using var listener = StartListener(out var port);

        // 违规:Ping 负载 126 字节,超过控制帧上限 125(RFC 6455 §5.5)
        var payload = new Byte[126];
        var ping = new Byte[4 + payload.Length];
        ping[0] = 0x89;                                     // FIN=1 + Ping
        ping[1] = 126;                                      // 2 字节扩展长度
        ping[3] = 126;
        var server = AcceptAndReplyAsync(listener, ping);

        using var client = new WebSocketClient($"ws://127.0.0.1:{port}/ws") { Timeout = 5_000 };
        Assert.True(await client.OpenAsync());

        var (opcode, status) = await server.WaitAsync(TimeSpan.FromSeconds(10));

        // 首帧必须是关闭帧:若客户端按违规 Ping 回了 Pong,Pong 会先于关闭帧到达
        Assert.Equal(0x88, opcode);
        Assert.Equal(1002, status);
    }

    [Fact]
    [DisplayName("ws协议_分片Ping控制帧_客户端不回Pong并以1002失败连接")]
    public async Task FragmentedPing_ClientFails1002WithoutPong()
    {
        using var listener = StartListener(out var port);

        // 违规:Ping 帧 FIN=0(RFC 6455 §5.5 控制帧不得分片)
        var ping = new Byte[] { 0x09, 0x00 };
        var server = AcceptAndReplyAsync(listener, ping);

        using var client = new WebSocketClient($"ws://127.0.0.1:{port}/ws") { Timeout = 5_000 };
        Assert.True(await client.OpenAsync());

        var (opcode, status) = await server.WaitAsync(TimeSpan.FromSeconds(10));

        Assert.Equal(0x88, opcode);
        Assert.Equal(1002, status);
    }

    [Fact]
    [DisplayName("ws关闭_服务端发Close_客户端回显同码并关闭,拉取先得Close消息再得null")]
    public async Task ServerClose_ClientRepliesEchoAndCloses()
    {
        using var listener = StartListener(out var port);

        // 服务端 Close(1000, "bye"):FIN=1 + Close,长度 5(2 字节状态码 + 3 字节描述)
        var close = new Byte[] { 0x88, 0x05, 0x03, 0xE8, (Byte)'b', (Byte)'y', (Byte)'e' };
        var server = AcceptAndReplyAsync(listener, close);

        using var client = new WebSocketClient($"ws://127.0.0.1:{port}/ws") { Timeout = 5_000 };
        Assert.True(await client.OpenAsync());

        var (opcode, status) = await server.WaitAsync(TimeSpan.FromSeconds(10));

        // 客户端必须回写关闭帧并回显状态码(RFC 6455 §5.5.1)
        Assert.Equal(0x88, opcode);
        Assert.Equal(1000, status);

        // 拉取 API 仍能先观察到 Close 消息,随后连接关闭返回 null
        var msg = await client.ReceiveMessageAsync().WaitAsync(TimeSpan.FromSeconds(5));
        Assert.NotNull(msg);
        Assert.Equal(NewLife.Messaging.WebSocketMessageType.Close, msg!.Type);
        Assert.Equal(1000, msg.CloseStatus);

        Assert.Null(await client.ReceiveMessageAsync().WaitAsync(TimeSpan.FromSeconds(5)));
    }

    [Fact]
    [DisplayName("ws关闭_服务端空Close_客户端回空负载关闭帧")]
    public async Task ServerEmptyClose_ClientRepliesEmptyClose()
    {
        using var listener = StartListener(out var port);

        // 无状态码关闭帧:{FIN=1 + Close, 长度 0}
        var close = new Byte[] { 0x88, 0x00 };
        var server = AcceptAndReplyAsync(listener, close);

        using var client = new WebSocketClient($"ws://127.0.0.1:{port}/ws") { Timeout = 5_000 };
        Assert.True(await client.OpenAsync());

        var (opcode, status) = await server.WaitAsync(TimeSpan.FromSeconds(10));

        // 回写空负载关闭帧:不得把 1005 等保留值发到线上(RFC 6455 §7.4.1)
        Assert.Equal(0x88, opcode);
        Assert.Equal(0, status);
    }

    [Fact]
    [DisplayName("ws关闭_服务端保留码Close_客户端回1002")]
    public async Task ServerCloseWithReservedCode_ClientReplies1002()
    {
        using var listener = StartListener(out var port);

        // 保留状态码 1005(RFC 6455 §7.4.1 禁止出现在线路上):{FIN=1 + Close, 长度 2, 1005}
        var close = new Byte[] { 0x88, 0x02, 0x03, 0xED };
        var server = AcceptAndReplyAsync(listener, close);

        using var client = new WebSocketClient($"ws://127.0.0.1:{port}/ws") { Timeout = 5_000 };
        Assert.True(await client.OpenAsync());

        var (opcode, status) = await server.WaitAsync(TimeSpan.FromSeconds(10));

        Assert.Equal(0x88, opcode);
        Assert.Equal(1002, status);
    }

    [Fact]
    [DisplayName("ws关闭_应用主动Close_客户端只发一帧关闭帧并关闭")]
    public async Task AppInitiatedClose_SendsSingleCloseFrame()
    {
        using var listener = StartListener(out var port);

        // 服务端只握手:读取客户端主动发出的关闭帧,再确认连接随后关闭(无第二帧)
        var server = AcceptAndExpectSingleCloseAsync(listener);

        using var client = new WebSocketClient($"ws://127.0.0.1:{port}/ws") { Timeout = 5_000 };
        Assert.True(await client.OpenAsync());

        await client.CloseAsync(1000, "app");

        var (opcode, status, hasMore) = await server.WaitAsync(TimeSpan.FromSeconds(10));

        Assert.Equal(0x88, opcode);
        Assert.Equal(1000, status);
        Assert.False(hasMore, "会话关闭后不得再发送任何帧");
    }

    private static Socket StartListener(out Int32 port)
    {
        var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
        listener.Listen(1);
        port = ((IPEndPoint)listener.LocalEndPoint!).Port;

        return listener;
    }

    /// <summary>接受一个连接,完成握手、发送违规帧,并读取客户端回写的首帧(opcode 与关闭状态码)</summary>
    private static async Task<(Int32 Opcode, Int32 CloseStatus)> AcceptAndReplyAsync(Socket listener, Byte[] frame)
    {
        using var socket = await listener.AcceptWithinAsync();
        socket.NoDelay = true;
        using var ns = new NetworkStream(socket);

        await HandshakeAsync(ns);

        // 发送违规帧
        await ns.WriteAsync(frame);
        await ns.FlushAsync();

        // 读取客户端应答的首帧(客户端所有帧都带掩码)
        return await ReadFrameAsync(ns);
    }

    /// <summary>接受一个连接,完成握手,读取客户端主动发出的关闭帧,并确认随后连接关闭、无后续帧</summary>
    private static async Task<(Int32 Opcode, Int32 CloseStatus, Boolean HasMore)> AcceptAndExpectSingleCloseAsync(Socket listener)
    {
        using var socket = await listener.AcceptWithinAsync();
        socket.NoDelay = true;
        using var ns = new NetworkStream(socket);

        await HandshakeAsync(ns);

        var (opcode, status) = await ReadFrameAsync(ns);

        // 确认没有第二帧:连接关闭(EOF)或静默超时视为正常,读到任何字节视为多余发送
        var hasMore = false;
        try
        {
            using var cts = new CancellationTokenSource(1_000);
            var buffer = new Byte[16];
            var n = await ns.ReadAsync(buffer, cts.Token);
            hasMore = n > 0;
        }
        catch (OperationCanceledException) { }
        catch (IOException) { }

        return (opcode, status, hasMore);
    }

    /// <summary>完成 WebSocket 服务端握手(读取请求头、回写 101 响应)</summary>
    private static async Task HandshakeAsync(NetworkStream ns)
    {
        // 读握手请求直到空行
        var buf = new Byte[4096];
        var total = 0;
        while (true)
        {
            var n = await ns.ReadAsync(buf.AsMemory(total));
            if (n <= 0) throw new IOException("握手请求未读完连接即关闭");
            total += n;
            if (Encoding.ASCII.GetString(buf, 0, total).Contains("\r\n\r\n")) break;
            if (total >= buf.Length) throw new IOException("握手请求过长");
        }

        // 计算并回写 101 响应
        var key = "";
        foreach (var line in Encoding.ASCII.GetString(buf, 0, total).Split("\r\n"))
        {
            if (line.StartsWithIgnoreCase("Sec-WebSocket-Key:"))
            {
                key = line[(line.IndexOf(':') + 1)..].Trim();
                break;
            }
        }
        var accept = SHA1.Create().ComputeHash((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").GetBytes()).ToBase64();
        var head = $"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {accept}\r\n\r\n";
        await ns.WriteAsync(Encoding.ASCII.GetBytes(head));
        await ns.FlushAsync();
    }

    /// <summary>读取一帧(客户端帧带掩码),返回 opcode 与关闭状态码(非 Close 帧或无状态码时为 0)</summary>
    private static async Task<(Int32 Opcode, Int32 CloseStatus)> ReadFrameAsync(NetworkStream ns)
    {
        var header = await ReadExactlyAsync(ns, 2);
        var opcode = header[0];
        var masked = (header[1] & 0x80) != 0;
        var len = (Int64)(header[1] & 0x7F);
        if (len == 126)
        {
            var ext = await ReadExactlyAsync(ns, 2);
            len = (ext[0] << 8) | ext[1];
        }
        else if (len == 127)
        {
            var ext = await ReadExactlyAsync(ns, 8);
            len = 0;
            for (var i = 0; i < 8; i++) len = (len << 8) | ext[i];
        }

        var masks = masked ? await ReadExactlyAsync(ns, 4) : null;
        var payload = await ReadExactlyAsync(ns, (Int32)len);
        if (masks != null)
        {
            for (var i = 0; i < payload.Length; i++) payload[i] = (Byte)(payload[i] ^ masks[i & 3]);
        }

        var status = payload.Length >= 2 ? (payload[0] << 8) | payload[1] : 0;

        return (opcode, status);
    }

    /// <summary>按量收满指定字节数;连接提前关闭即抛异常</summary>
    private static async Task<Byte[]> ReadExactlyAsync(NetworkStream ns, Int32 count)
    {
        var buf = new Byte[count];
        var got = 0;
        while (got < count)
        {
            var n = await ns.ReadAsync(buf.AsMemory(got));
            if (n <= 0) throw new IOException($"连接提前关闭,仅读到 {got}/{count} 字节");
            got += n;
        }

        return buf;
    }
}