3

我用 Java 写了一个 WebSocket 帧解码器:

private byte[] decodeFrame(byte[] _rawIn) {
        int maskIndex = 2;
        byte[] maskBytes = new byte[4];

        if ((_rawIn[1] & (byte) 127) == 126) {
            maskIndex = 4;
        } else if ((_rawIn[1] & (byte) 127) == 127) {
            maskIndex = 10;
        }

        System.arraycopy(_rawIn, maskIndex, maskBytes, 0, 4);

        byte[] message = new byte[_rawIn.length - maskIndex - 4];

        for (int i = maskIndex + 4; i < _rawIn.length; i++) {
            message[i - maskIndex - 4] = (byte) (_rawIn[i] ^ maskBytes[(i - maskIndex - 4) % 4]);
        }

        return message;
    }

它有效,但我不知道如何验证帧以确保它仅解码有效帧。

不幸的是,协议描述http://tools.ietf.org/html/rfc6455并没有说明帧验证。

4

4 回答 4

17

解析原始 websocket 框架很容易。但是您必须一次检查一个字节的标头。

这是一个粗略的例子:

我留下了一些 TODO 供您自己解决(当然是在阅读 RFC-6455 规范之后)

您可以验证的事情:

基本帧协议:RFC-6455 - 第 5.2 节

  • 是否找到了规范中定义的有效操作码之一?
  • RSV 位是否使用不当?

客户端到服务器屏蔽:RFC 6455 - 第 5.3 节

  • 如果帧是由客户端发送的,帧是否被屏蔽?
  • 掩码是从帧到帧随机的吗?
  • 不允许 [0x00, 0x00, 0x00, 0x00] 作为掩码。

分段:RFC 6455 - 第 5.4 节

  • 它是一个零散的控制框架吗?
  • 由多个帧组成的大消息的碎片是否乱序?
  • 一条新消息在前一条消息完成之前是否已开始并带有 FIN 标志?

控制帧:RFC 6455 - 第 5.5 节

  • 控制帧的有效载荷长度是否超过 125 字节?
  • 有效载荷是否碎片化?

关闭帧:RFC 6455 - 第 5.5.1 节

  • 如果有效载荷中提供了状态代码,该状态代码是否符合第 7.4.1 节中声明的状态代码之一?不要忘记检查在 RFC 最终确定后添加的 websocket 状态代码的 IANA 注册表)
  • 状态码是否允许在帧中通过网络发送?(例如,参见代码 1005 和 1006)
  • 如果框架中提供了 /reason/,它是否符合 UTF-8 编码规则?
  • 在关闭帧之后,您是否收到任何类型的帧?(这是一个禁忌)

数据帧:RFC 6455 - 第 5.6 节

  • 如果您收到 TEXT 有效负载数据(来自 TEXT + CONTINUATION 帧),有效负载数据是否符合 UTF-8 编码规则?

虽然您可以在单个帧级别进行验证,但您会发现上面的一些验证是对多个帧之间的状态和行为的验证。您可以在发送和接收数据中找到更多此类验证:RFC 6455 - 第 6 节

但是,如果您在混合中有扩展,那么您还需要从协商的扩展堆栈的角度处理帧。使用扩展时,上述某些测试似乎无效。

示例:您有Compression Extension (RFC-7692)(例如permessage-deflate)正在使用,则无法通过网络外的原始帧验证 TEXT 有效负载,因为您必须首先通过扩展传递帧。请注意,扩展可以更改碎片以满足其需求,这也可能会扰乱您的验证。

package websocket;

import java.nio.ByteBuffer;
import java.nio.charset.Charset;

public class RawParse
{
    public static class Frame
    {
        byte opcode;
        boolean fin;
        byte payload[];
    }

    public static Frame parse(byte raw[])
    {
        // easier to do this via ByteBuffer
        ByteBuffer buf = ByteBuffer.wrap(raw);

        // Fin + RSV + OpCode byte
        Frame frame = new Frame();
        byte b = buf.get();
        frame.fin = ((b & 0x80) != 0);
        boolean rsv1 = ((b & 0x40) != 0);
        boolean rsv2 = ((b & 0x20) != 0);
        boolean rsv3 = ((b & 0x10) != 0);
        frame.opcode = (byte)(b & 0x0F);

        // TODO: add control frame fin validation here
        // TODO: add frame RSV validation here

        // Masked + Payload Length
        b = buf.get();
        boolean masked = ((b & 0x80) != 0);
        int payloadLength = (byte)(0x7F & b);
        int byteCount = 0;
        if (payloadLength == 0x7F)
        {
            // 8 byte extended payload length
            byteCount = 8;
        }
        else if (payloadLength == 0x7E)
        {
            // 2 bytes extended payload length
            byteCount = 2;
        }

        // Decode Payload Length
        while (--byteCount > 0)
        {
            b = buf.get();
            payloadLength |= (b & 0xFF) << (8 * byteCount);
        }
        
        // TODO: add control frame payload length validation here

        byte maskingKey[] = null;
        if (masked)
        {
            // Masking Key
            maskingKey = new byte[4];
            buf.get(maskingKey,0,4);
        }
        
        // TODO: add masked + maskingkey validation here

        // Payload itself
        frame.payload = new byte[payloadLength];
        buf.get(frame.payload,0,payloadLength);

        // Demask (if needed)
        if (masked)
        {
            for (int i = 0; i < frame.payload.length; i++)
            {
                frame.payload[i] ^= maskingKey[i % 4];
            }
        }

        return frame;
    }

    public static void main(String[] args)
    {
        Charset UTF8 = Charset.forName("UTF-8");

        Frame closeFrame = parse(hexToByteArray("8800"));
        System.out.printf("closeFrame.opcode = %d%n",closeFrame.opcode);
        System.out.printf("closeFrame.payload.length = %d%n",closeFrame.payload.length);

        // Examples from https://www.rfc-editor.org/rfc/rfc6455#section-5.7
        Frame unmaskedTextFrame = parse(hexToByteArray("810548656c6c6f"));
        System.out.printf("unmaskedTextFrame.opcode = %d%n",unmaskedTextFrame.opcode);
        System.out.printf("unmaskedTextFrame.payload.length = %d%n",unmaskedTextFrame.payload.length);
        System.out.printf("unmaskedTextFrame.payload = \"%s\"%n",new String(unmaskedTextFrame.payload,UTF8));

        Frame maskedTextFrame = parse(hexToByteArray("818537fa213d7f9f4d5158"));
        System.out.printf("maskedTextFrame.opcode = %d%n",maskedTextFrame.opcode);
        System.out.printf("maskedTextFrame.payload.length = %d%n",maskedTextFrame.payload.length);
        System.out.printf("maskedTextFrame.payload = \"%s\"%n",new String(maskedTextFrame.payload,UTF8));
    }

    public static byte[] hexToByteArray(String hstr)
    {
        if ((hstr.length() < 0) || ((hstr.length() % 2) != 0))
        {
            throw new IllegalArgumentException(String.format("Invalid string length of <%d>",hstr.length()));
        }

        int size = hstr.length() / 2;
        byte buf[] = new byte[size];
        byte hex;
        int len = hstr.length();

        int idx = (int)Math.floor(((size * 2) - (double)len) / 2);
        for (int i = 0; i < len; i++)
        {
            hex = 0;
            if (i >= 0)
            {
                hex = (byte)(Character.digit(hstr.charAt(i),16) << 4);
            }
            i++;
            hex += (byte)(Character.digit(hstr.charAt(i),16));

            buf[idx] = hex;
            idx++;
        }

        return buf;
    }
}
于 2013-08-22T03:04:47.043 回答
0

websocket 协议不包括任何类型的校验和,如果这是您正在寻找的。如果数据帧中存在错误,您将知道的唯一方法是因为数据出现错误或因为后续帧出现“有趣”(意外的操作码,比预期的更长或更短等)。

于 2013-08-21T21:47:27.260 回答
0

防止应用程序连接到它不是为它设计的 websocket 服务器的第一个保护措施是 HTTP websocket 握手。如果它不包含Upgrade: websocketSec-WebSocket-Key或者Sec-WebSocket-Version: 13它甚至不是 RFC6455 websocket 客户端并且必须被拒绝。

第二种保护措施适用于使用 websocket 的客户端,但它们是为不同的应用程序设计的。这是Sec-WebSocket-Protocol: something标题。此标头是可选的,但应该是标识客户端要使用的应用程序的字符串。当该值与服务器期望的应用程序不匹配时,客户端应该被拒绝。

对于认为他们说 websocket 并连接到正确的服务器但实际上在他们的 websocket 协议实现中存在错误的客户端的最后一个保护措施是保留位。

掩码键或长度没有非法值。错误的长度将导致下一帧在解释为有效负载的数据不足或过多后开始,但这很难检测到。发生这种情况的唯一迹象是,当一个所谓的帧的第一个字节没有意义时。

帧的第 2、第 3 和第 4 位被保留,并且根据 RFC“必须为 0,除非协商扩展 [...] 如果接收到非零值 [...] 接收端点必须使 WebSocket 失败连接。”。目前还没有使用这些位的扩展,当有扩展时,您将不得不做一些事情来打开它。因此,当这些位中的一个不为零时,就会出现问题。

如果需要,您可以在协议级别添加进一步的保护措施,例如每条消息必须以特定的神奇字节值开始和/或结束(请记住,存在多片段消息,浏览器可以在它感觉就像这样做)。我目前开发的应用程序使用 JSON 有效负载,因此当消息不是以 开头{和结尾的有效 JSON 字符串时},我知道客户端已损坏(或者我的服务器帧解码方法很可能)。

于 2013-08-21T22:06:29.283 回答
-1

尽管我是 websockets 的新手,但我目前对 websockets 的兴趣使我可以帮助解决这个问题。

https://www.rfc-editor.org/rfc/rfc6455#section-5.2 提供了数据框的高级视图。您将测试第一个字节的最后四个,因此 raw_in[0]<<<4。这会给你最后四个我对位操作不太好,所以我不确定如何让最后 4 位表示 0000 1111-0000 0000 vs 1111 0000-0000 0000。所以你可以看到 0001操作码是文本帧,0010 操作码是二进制帧,依此类推。因此,如果您只想排除文本帧,只需测试第一个字节的最后四位是否为 0001。

于 2013-08-21T21:52:12.733 回答