阿里CBU前端團隊招人,感興趣的小夥伴加我微信casperchen,加以前附錄本身的博客或者github地址,簡歷可直接發416394284@qq.com 👈javascript
WebSocket的出現,使得瀏覽器具有了實時雙向通訊的能力。本文由淺入深,介紹了WebSocket如何創建鏈接、交換數據的細節,以及數據幀的格式。此外,還簡要介紹了針對WebSocket的安全攻擊,以及協議是如何抵禦相似攻擊的。html
HTML5開始提供的一種瀏覽器與服務器進行全雙工通信的網絡技術,屬於應用層協議。它基於TCP傳輸協議,並複用HTTP的握手通道。前端
對大部分web開發者來講,上面這段描述有點枯燥,其實只要記住幾點:java
說到優勢,這裏的對比參照物是HTTP協議,歸納地說就是:支持雙向通訊,更靈活,更高效,可擴展性更好。git
對於後面兩點,沒有研究過WebSocket協議規範的同窗可能理解起來不夠直觀,但不影響對WebSocket的學習和使用。github
對網絡應用層協議的學習來講,最重要的每每就是鏈接創建過程、數據交換教程。固然,數據的格式是逃不掉的,由於它直接決定了協議自己的能力。好的數據格式能讓協議更高效、擴展性更好。web
下文主要圍繞下面幾點展開:ajax
在正式介紹協議細節前,先來看一個簡單的例子,有個直觀感覺。例子包括了WebSocket服務端、WebSocket客戶端(網頁端)。完整代碼能夠在 這裏 找到。算法
這裏服務端用了ws
這個庫。相比你們熟悉的socket.io
,ws
實現更輕量,更適合學習的目的。express
代碼以下,監聽8080端口。當有新的鏈接請求到達時,打印日誌,同時向客戶端發送消息。當收到到來自客戶端的消息時,一樣打印日誌。
var app = require('express')();
var server = require('http').Server(app);
var WebSocket = require('ws');
var wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
console.log('server: receive connection.');
ws.on('message', function incoming(message) {
console.log('server: received: %s', message);
});
ws.send('world');
});
app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});
app.listen(3000);
複製代碼
代碼以下,向8080端口發起WebSocket鏈接。鏈接創建後,打印日誌,同時向服務端發送消息。接收到來自服務端的消息後,一樣打印日誌。
<script> var ws = new WebSocket('ws://localhost:8080'); ws.onopen = function () { console.log('ws onopen'); ws.send('from client: hello'); }; ws.onmessage = function (e) { console.log('ws onmessage'); console.log('from server: ' + e.data); }; </script> 複製代碼
可分別查看服務端、客戶端的日誌,這裏不展開。
服務端輸出:
server: receive connection.
server: received hello
複製代碼
客戶端輸出:
client: ws connection is open
client: received world
複製代碼
前面提到,WebSocket複用了HTTP的握手通道。具體指的是,客戶端經過HTTP請求與WebSocket服務端協商升級協議。協議升級完成後,後續的數據交換則遵守WebSocket的協議。
首先,客戶端發起協議升級請求。能夠看到,採用的是標準的HTTP報文格式,且只支持GET
方法。
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
複製代碼
重點請求首部意義以下:
Connection: Upgrade
:表示要升級協議Upgrade: websocket
:表示要升級到websocket協議。Sec-WebSocket-Version: 13
:表示websocket的版本。若是服務端不支持該版本,須要返回一個Sec-WebSocket-Version
header,裏面包含服務端支持的版本號。Sec-WebSocket-Key
:與後面服務端響應首部的Sec-WebSocket-Accept
是配套的,提供基本的防禦,好比惡意的鏈接,或者無心的鏈接。注意,上面請求省略了部分非重點請求首部。因爲是標準的HTTP請求,相似Host、Origin、Cookie等請求首部會照常發送。在握手階段,能夠經過相關請求首部進行 安全限制、權限校驗等。
服務端返回內容以下,狀態代碼101
表示協議切換。到此完成協議升級,後續的數據交互都按照新的協議來。
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
複製代碼
備註:每一個header都以
\r\n
結尾,而且最後一行加上一個額外的空行\r\n
。此外,服務端迴應的HTTP狀態碼只能在握手階段使用。過了握手階段後,就只能採用特定的錯誤碼。
Sec-WebSocket-Accept
根據客戶端請求首部的Sec-WebSocket-Key
計算出來。
計算公式爲:
Sec-WebSocket-Key
跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11
拼接。僞代碼以下:
>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
複製代碼
驗證下前面的返回結果:
const crypto = require('crypto');
const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const secWebSocketKey = 'w4v7O6xFTi36lq3RNcgctw==';
let secWebSocketAccept = crypto.createHash('sha1')
.update(secWebSocketKey + magic)
.digest('base64');
console.log(secWebSocketAccept);
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=
複製代碼
客戶端、服務端數據的交換,離不開數據幀格式的定義。所以,在實際講解數據交換以前,咱們先來看下WebSocket的數據幀格式。
WebSocket客戶端、服務端通訊的最小單位是幀(frame),由1個或多個幀組成一條完整的消息(message)。
本節的重點,就是講解數據幀的格式。詳細定義可參考 RFC6455 5.2節 。
下面給出了WebSocket數據幀的統一格式。熟悉TCP/IP協議的同窗對這樣的圖應該不陌生。
FIN
、RSV1
各佔據1比特,opcode
佔據4比特。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字節(32位)
全部從客戶端傳送到服務端的數據幀,數據載荷都進行了掩碼操做,Mask爲1,且攜帶了4字節的Masking-key。若是Mask爲0,則沒有Masking-key。
備註:載荷數據的長度,不包括mask key的長度。
Payload data:(x+y) 字節
載荷數據:包括了擴展數據、應用數據。其中,擴展數據x字節,應用數據y字節。
擴展數據:若是沒有協商使用擴展的話,擴展數據數據爲0字節。全部的擴展都必須聲明擴展數據的長度,或者能夠如何計算出擴展數據的長度。此外,擴展如何使用必須在握手階段就協商好。若是擴展數據存在,那麼載荷數據長度必須將擴展數據的長度包含在內。
應用數據:任意的應用數據,在擴展數據以後(若是存在擴展數據),佔據了數據幀剩餘的位置。載荷數據長度 減去 擴展數據長度,就獲得應用數據的長度。
掩碼鍵(Masking-key)是由客戶端挑選出來的32位的隨機數。掩碼操做不會影響數據載荷的長度。掩碼、反掩碼操做都採用以下算法:
首先,假設:
i mod 4
的結果。算法描述爲: original-octet-i 與 masking-key-octet-j 異或後,獲得 transformed-octet-i。
j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j
一旦WebSocket客戶端、服務端創建鏈接後,後續的操做都是基於數據幀的傳遞。
WebSocket根據opcode
來區分操做的類型。好比0x8
表示斷開鏈接,0x0
-0x2
表示數據交互。
WebSocket的每條消息可能被切分紅多個數據幀。當WebSocket的接收方收到一個數據幀時,會根據FIN
的值來判斷,是否已經收到消息的最後一個數據幀。
FIN=1表示當前數據幀爲消息的最後一個數據幀,此時接收方已經收到完整的消息,能夠對消息進行處理。FIN=0,則接收方還須要繼續監聽接收其他的數據幀。
此外,opcode
在數據交換的場景下,表示的是數據的類型。0x01
表示文本,0x02
表示二進制。而0x00
比較特殊,表示延續幀(continuation frame),顧名思義,就是完整消息對應的數據幀還沒接收完。
直接看例子更形象些。下面例子來自MDN,能夠很好地演示數據的分片。客戶端向服務端兩次發送消息,服務端收到消息後迴應客戶端,這裏主要看客戶端往服務端發送的消息。
第一條消息
FIN=1, 表示是當前消息的最後一個數據幀。服務端收到當前數據幀後,能夠處理消息。opcode=0x1,表示客戶端發送的是文本類型。
第二條消息
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
複製代碼
WebSocket爲了保持客戶端、服務端的實時雙向通訊,須要確保客戶端、服務端之間的TCP通道保持鏈接沒有斷開。然而,對於長時間沒有數據往來的鏈接,若是依舊長時間保持着,可能會浪費包括的鏈接資源。
但不排除有些場景,客戶端、服務端雖然長時間沒有數據往來,但仍須要保持鏈接。這個時候,能夠採用心跳來實現。
ping、pong的操做,對應的是WebSocket的兩個控制幀,opcode
分別是0x9
、0xA
。
舉例,WebSocket服務端向客戶端發送ping,只須要以下代碼(採用ws
模塊)
ws.ping('', false, true);
複製代碼
前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept
在主要做用在於提供基礎的防禦,減小惡意鏈接、意外鏈接。
做用大體概括以下:
強調:Sec-WebSocket-Key/Sec-WebSocket-Accept 的換算,只能帶來基本的保障,但鏈接是否安全、數據是否安全、客戶端/服務端是否合法的 ws客戶端、ws服務端,其實並無實際性的保證。
WebSocket協議中,數據掩碼的做用是加強協議的安全性。但數據掩碼並非爲了保護數據自己,由於算法自己是公開的,運算也不復雜。除了加密通道自己,彷佛沒有太多有效的保護通訊安全的辦法。
那麼爲何還要引入掩碼計算呢,除了增長計算機器的運算量外彷佛並無太多的收益(這也是很多同窗疑惑的點)。
答案仍是兩個字:安全。但並非爲了防止數據泄密,而是爲了防止早期版本的協議中存在的代理緩存污染攻擊(proxy cache poisoning attacks)等問題。
下面摘自2010年關於安全的一段講話。其中提到了代理服務器在協議實現上的缺陷可能致使的安全問題。猛擊出處。
「We show, empirically, that the current version of the WebSocket consent mechanism is vulnerable to proxy cache poisoning attacks. Even though the WebSocket handshake is based on HTTP, which should be understood by most network intermediaries, the handshake uses the esoteric 「Upgrade」 mechanism of HTTP [5]. In our experiment, we find that many proxies do not implement the Upgrade mechanism properly, which causes the handshake to succeed even though subsequent traffic over the socket will be misinterpreted by the proxy.」
[TALKING] Huang, L-S., Chen, E., Barth, A., Rescorla, E., and C.
Jackson, "Talking to Yourself for Fun and Profit", 2010,
複製代碼
在正式描述攻擊步驟以前,咱們假設有以下參與者:
攻擊步驟一:
因爲 upgrade 的實現上有缺陷,代理服務器 覺得以前轉發的是普通的HTTP消息。所以,當協議服務器 贊成鏈接,代理服務器 覺得本次會話已經結束。
攻擊步驟二:
到這裏,受害者能夠登場了:
附:前面提到的精心構造的「HTTP請求報文」。
Client → Server:
POST /path/of/attackers/choice HTTP/1.1 Host: host-of-attackers-choice.com Sec-WebSocket-Key: <connection-key>
Server → Client:
HTTP/1.1 200 OK
Sec-WebSocket-Accept: <connection-key>
複製代碼
最初的提案是對數據進行加密處理。基於安全、效率的考慮,最終採用了折中的方案:對數據載荷進行掩碼處理。
須要注意的是,這裏只是限制了瀏覽器對數據載荷進行掩碼處理,可是壞人徹底能夠實現本身的WebSocket客戶端、服務端,不按規則來,攻擊能夠照常進行。
可是對瀏覽器加上這個限制後,能夠大大增長攻擊的難度,以及攻擊的影響範圍。若是沒有這個限制,只須要在網上放個釣魚網站騙人去訪問,一會兒就能夠在短期內展開大範圍的攻擊。
WebSocket可寫的東西還挺多,好比WebSocket擴展。客戶端、服務端之間是如何協商、使用擴展的。WebSocket擴展能夠給協議自己增長不少能力和想象空間,好比數據的壓縮、加密,以及多路複用等。
篇幅所限,這裏先不展開,感興趣的同窗能夠留言交流。文章若有錯漏,敬請指出。
Talking to Yourself for Fun and Profit(含有攻擊描述)
What is Sec-WebSocket-Key for?
10.3. Attacks On Infrastructure (Masking)
Talking to Yourself for Fun and Profit
How does websocket frame masking protect against cache poisoning?