理解WebSocket

-- TOC --

WebSocket是一个基于TCP的协议,因此它与UDP完全无关。WebSocket协议使用HTTP协议进行握手,即协议升级。

kele_pashu.png

简介

The WebSocket Protocol enables two-way communication between a client running untrusted code in a controlled environment to a remote host that has opted-in to communications from that code. The security model used for this is the origin-based security model commonly used by web browsers. The protocol consists of an opening handshake followed by basic message framing, layered over TCP. The goal of this technology is to provide a mechanism for browser-based applications that need two-way communication with servers that does not rely on opening multiple HTTP connections (e.g., using XMLHttpRequest or <iframe>s and long polling).

WebSocket的目的是为了给基于浏览器的App一种双向通信的机制。

The WebSocket Protocol (RFC6455) is an independent TCP-based protocol. Its only relationship to HTTP is that its handshake is interpreted by HTTP servers as an Upgrade request. By default, the WebSocket Protocol uses port 80 for regular WebSocket connections and port 443 for WebSocket connections tunneled over Transport Layer Security (TLS) [RFC2818].

默认ws和wss的端口也是80和443。

下面是两个从网上找到的理解WebSocket很好的图:

websocket connect

websocket http

WebSocket属于应用层协议,它基于TCP传输协议,并复用HTTP的握手通道。

握手,协议升级

WebSocket利用HTTP协议进行握手,具体指的是,客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。

首先,客户端申请协议升级

GET /ws HTTP/1.1
Host: a.b.c.d:6789
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://a.b.c.d:6789
Sec-WebSocket-Protocol: tty
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: j3bgeirMjfXZr5e2rtj9Rw==
Authorization: Basic eGlubGluOmFkbWluNjc4OQ==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

这些协议消息内容,来自于ttyd交互。

关键是这些内容:

GET /ws HTTP/1.1
Connection: Upgrade  # 要升级
Upgrade: websocket   # 升级到websocket
Sec-WebSocket-Version: 13  # 版本
Sec-WebSocket-Key: j3bgeirMjfXZr5e2rtj9Rw==

然后,服务器响应协议升级

HTTP/1.1 101 Switching Protocols
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: C9ZSqHKnjZV7NQJGQHAtPF5Iciw=
Sec-WebSocket-Protocol: tty

服务器返回状态代码101,表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。

每个header都以\r\n结尾,并且最后一行加上一个额外的空行\r\n。此外,服务端回应的HTTP状态码只能在握手阶段使用。过了握手阶段后,就只能采用特定的错误码。

Sec-WebSocket-Key/Accept的计算和作用

用客户端的Sec-WebSocket-Key加上一串magic,258EAFA5-E914-47DA-95CA-C5AB0DC85B11,先做sha1的hash,然后将hash值转换成base64格式。

>>> import hashlib
>>> import base64
>>> key = 'j3bgeirMjfXZr5e2rtj9Rw=='
>>> magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
>>> sha1 = hashlib.sha1()
>>> sha1.update((key+magic).encode())
>>> base64.b64encode(sha1.digest())
b'C9ZSqHKnjZV7NQJGQHAtPF5Iciw='

网上看到有人总结的:

Sec-WebSocket-Key/Accept只能带来最基本的防护,由于这一切都是公开的,这个机制只能起到防止非故意和一些意外。

WebSocket Frame格式

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

FIN: 1个比特。

如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示这不是消息(message)的最后一个分片(fragment)。

RSV1, RSV2, RSV3:各占1个比特。

一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用WebSocket扩展,连接出错。

Opcode: 4个比特。

操作代码,Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection)。可选的操作代码如下:

Mask: 1个比特。

表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。

如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。

如果Mask是1,那么在Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1。

Payload length:数据载荷的长度,单位是字节。为7位,或7+16位,或1+64位。

假设数Payload length === x,如果:

此外,如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian)。

Masking-key:0或4字节

所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。

payload length,不包括mask key的长度。

Payload data:(x+y) 字节

载荷数据:包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。x在前,y在后。

扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。

应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。

掩码算法和作用

掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:

假设:

算法伪码:

j = i % 4
transformed-octet-i = original-octet-i ^ masking-key-octet-j

WebSocket协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。那么为什么还要引入掩码计算呢?

答案还是两个字:安全。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。

WebSocket最初的提案是对数据进行加密处理。基于安全、效率的考虑,最终采用了折中的方案:对数据载荷进行掩码处理。

需要注意的是,这里只是限制了浏览器对数据载荷进行掩码处理,但是坏人完全可以实现自己的WebSocket客户端、服务端,不按规则来,攻击可以照常进行。但是对客户端加上这个限制后,可以大大增加攻击的难度,以及攻击的影响范围。如果没有这个限制,只需要在网上放个钓鱼网站骗人去访问,一下子就可以在短时间内展开大范围的攻击。

本文链接:https://cs.pynote.net/net/202202231/

-- EOF --

-- MORE --