如何在Java中解析和验证WebSocket框架?

时间:2013-08-21 21:35:55

标签: java websocket frame

我用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 个答案:

答案 0 :(得分:15)

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

这是一个粗略的例子:

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

您可以验证的事项:

Base Framing Protocol: RFC-6455 - Section 5.2

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

Client-to-Server Masking: RFC 6455 - Section 5.3

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

Fragmentation: RFC 6455 - Section 5.4

  • 它是一个支离破碎的控制框架吗?
  • 包含多个帧的大型邮件的碎片是否按顺序排列?
  • 是否在之前使用FIN标志完成之前启动了新消息?

Control Frames: RFC 6455 - Section 5.5

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

Close Frames: RFC 6455 - Section 5.5.1

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

Data Frames: RFC 6455 - Section 5.6

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

虽然您可以在单个帧级别进行验证,但您会发现上面的一些验证是对多个帧之间的状态和行为的验证。您可以在Sending and Receiving Data: RFC 6455 - Section 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 http://tools.ietf.org/html/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;
    }
}

答案 1 :(得分:0)

websocket协议不包含任何类型的校验和,如果这是您正在寻找的内容。如果数据框中出现错误,您知道的唯一方法是因为数据出错或者因为后续帧出现而且有趣#34; (意外操作码,比预期更长或更短等)。

答案 2 :(得分:0)

针对连接到websocket服务器的应用程序的第一个安全措施是HTTP websocket握手。当它不包含Upgrade: websocketSec-WebSocket-KeySec-WebSocket-Version: 13时,它甚至不是RFC6455 websocket客户端,必须被拒绝。

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

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

屏蔽密钥或长度没有非法值。在解释了与有效载荷不足或太多的数据之后,错误的长度将导致下一帧开始,但这可能难以检测。发生这种情况的唯一迹象是,所谓的框架的第一个字节没有意义。

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

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

答案 3 :(得分:-1)

我目前对websockets的兴趣让我可以帮助解决这个问题,虽然我对websockets是全新的。

http://tools.ietf.org/html/rfc6455#section-5.2提供了数据框的高级视图。您将测试第一个字节的最后四个,因此raw_in [0]&lt;&lt;&lt;&lt; 4。这将给你最后四个我对位操作不太好所以我不知道如何得到最后4位代表0000 1111-0000 0000 vs 1111 0000-0000 0000.所以你可以看到0001 op代码是文本框架,0010 op代码是二进制框架等等。因此,如果您只想除文本框架外,只需测试第一个字节的最后四位是否为0001。