WebSocket協議:5分鐘從入門到精通

內容概覽

WebSocket的出現,使得瀏覽器具備了實時雙向通信的能力。

本文由淺入深,介紹了WebSocket如何建立連接、交換數據的細節,以及數據幀的格式。

此外,還簡要介紹了針對WebSocket的安全攻擊,以及協議是如何抵禦類似攻擊的。


什麼是WebSocket

HTML5開始提供的一種瀏覽器與服務器進行全雙工通訊的網絡技術,屬於應用層協議。它基於TCP傳輸協議,並複用HTTP的握手通道。

對大部分web開發者來說,上面這段描述有點枯燥,其實只要記住幾點:

【1】WebSocket可以在瀏覽器裡使用

【2】支持雙向通信

【3】使用很簡單


有哪些優點

說到優點,這裡的對比參照物是HTTP協議,概括地說就是:支持雙向通信,更靈活,更高效,可擴展性更好。

【1】支持雙向通信,實時性更強。

【2】更好的二進制支持。

【3】較少的控制開銷。連接創建後,ws客戶端、服務端進行數據交換時,協議控制的數據包頭部較小。在不包含頭部的情況下,服務端到客戶端的包頭只有2~10字節(取決於數據包長度),客戶端到服務端的的話,需要加上額外的4字節的掩碼。而HTTP協議每次通信都需要攜帶完整的頭部。

【4】支持擴展。ws協議定義了擴展,用戶可以擴展協議,或者實現自定義的子協議。(比如支持自定義壓縮算法等)

對於後面兩點,沒有研究過WebSocket協議規範的同學可能理解起來不夠直觀,但不影響對WebSocket的學習和使用。


需要學習哪些東西

對網絡應用層協議的學習來說,最重要的往往就是連接建立過程數據交換教程

當然,數據的格式是逃不掉的,因為它直接決定了協議本身的能力。好的數據格式能讓協議更高效、擴展性更好。

下文主要圍繞下面幾點展開:

【1】如何建立連接

【2】如何交換數據

【3】數據幀格式

【4】如何維持連接


入門例子

在正式介紹協議細節前,先來看一個簡單的例子,有個直觀感受。例子包括了WebSocket服務端、WebSocket客戶端(網頁端)

這裡服務端用了ws這個庫。相比大家熟悉的socket.io,ws實現更輕量,更適合學習的目的。

服務端

代碼如下,監聽8080端口。當有新的連接請求到達時,打印日誌,同時向客戶端發送消息。當收到到來自客戶端的消息時,同樣打印日誌。

WebSocket協議:5分鐘從入門到精通


客戶端

代碼如下,向8080端口發起WebSocket連接。

連接建立後,打印日誌,同時向服務端發送消息。接收到來自服務端的消息後,同樣打印日誌。

WebSocket協議:5分鐘從入門到精通


運行結果

可分別查看服務端、客戶端的日誌,這裡不展開。

服務端輸出

server: receive connection.
server: received hello

客戶端輸出

client: ws connection is open
client: received world

如何建立連接

前面提到,WebSocket複用了HTTP的握手通道。具體指的是,客戶端通過HTTP請求與WebSocket服務端協商升級協議。

協議升級完成後,後續的數據交換則遵照WebSocket的協議。

1、客戶端:申請協議升級

首先,客戶端發起協議升級請求。

可以看到,採用的是標準的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==

重點請求首部意義如下:

【1】Connection: Upgrade:表示要升級協議

【2】Upgrade: websocket:表示要升級到websocket協議。

【3】Sec-WebSocket-Version: 13:表示websocket的版本。如果服務端不支持該版本,需要返回一個Sec-WebSocket-Versionheader,裡面包含服務端支持的版本號。

【4】Sec-WebSocket-Key:與後面服務端響應首部的Sec-WebSocket-Accept是配套的,提供基本的防護,比如惡意的連接,或者無意的連接。

注意,上面請求省略了部分非重點請求首部。由於是標準的HTTP請求,類似Host、Origin、Cookie等請求首部會照常發送。在握手階段,可以通過相關請求首部進行 安全限制、權限校驗等。

2、服務端:相應協議升級

服務端返回內容如下,狀態代碼101表示協議切換。

到此完成協議升級,後續的數據交互都按照新的協議來。

HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
備註:每個header都以\r\n結尾,並且最後一行加上一個額外的空行\r\n。此外,服務端回應的HTTP狀態碼只能在握手階段使用。過了握手階段後,就只能採用特定的錯誤碼。

3、Sec-WebSocket-Accept的計算

Sec-WebSocket-Accept根據客戶端請求首部的Sec-WebSocket-Key計算出來。

計算公式為:

【1】將Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。

【2】通過SHA1計算出摘要,並轉成base64字符串。

偽代碼如下:

>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )

驗證下前面的返回結果:

WebSocket協議:5分鐘從入門到精通


數據幀格式

客戶端、服務端數據的交換,離不開數據幀格式的定義。因此,在實際講解數據交換之前,我們先來看下WebSocket的數據幀格式。

WebSocket客戶端、服務端通信的最小單位是幀(frame),由1個或多個幀組成一條完整的消息(message)。

【1】發送端:將消息切割成多個幀,併發送給服務端;

【2】接收端:接收消息幀,並將關聯的幀重新組裝成完整的消息;

本節的重點,就是講解數據幀的格式。

1、數據幀格式概覽

下面給出了WebSocket數據幀的統一格式。熟悉TCP/IP協議的同學對這樣的圖應該不陌生。

【1】從左到右,單位是比特。比如FIN、RSV1各佔據1比特,opcode佔據4比特。

【2】內容包括了標識、操作代碼、掩碼、數據、數據長度等。

WebSocket協議:5分鐘從入門到精通

2、數據幀格式詳解

針對前面的格式概覽圖,這裡逐個字段進行講解,如有不清楚之處,可參考協議規範,或留言交流。

FIN:1個比特。

如果是1,表示這是消息(message)的最後一個分片(fragment),如果是0,表示不是是消息(message)的最後一個分片(fragment)。

RSV1, RSV2, RSV3:各佔1個比特。

一般情況下全為0。當客戶端、服務端協商採用WebSocket擴展時,這三個標誌位可以非0,且值的含義由擴展進行定義。如果出現非零的值,且並沒有採用WebSocket擴展,連接出錯。

Opcode: 4個比特。

操作代碼,Opcode的值決定了應該如何解析後續的數據載荷(data payload)。如果操作代碼是不認識的,那麼接收端應該斷開連接(fail the connection)。可選的操作代碼如下:

%x0:表示一個延續幀。當Opcode為0時,表示本次數據傳輸採用了數據分片,當前收到的數據幀為其中一個數據分片。
%x1:表示這是一個文本幀(frame)
%x2:表示這是一個二進制幀(frame)
%x3-7:保留的操作代碼,用於後續定義的非控制幀。
%x8:表示連接斷開。
%x9:表示這是一個ping操作。
%xA:表示這是一個pong操作。
%xB-F:保留的操作代碼,用於後續定義的控制幀。

Mask: 1個比特。

表示是否要對數據載荷進行掩碼操作。從客戶端向服務端發送數據時,需要對數據進行掩碼操作;從服務端向客戶端發送數據時,不需要對數據進行掩碼操作。

如果服務端接收到的數據沒有進行過掩碼操作,服務端需要斷開連接。

如果Mask是1,那麼在Masking-key中會定義一個掩碼鍵(masking key),並用這個掩碼鍵來對數據載荷進行反掩碼。所有客戶端發送到服務端的數據幀,Mask都是1。

掩碼的算法、用途在下一小節講解。

Payload length:數據載荷的長度,單位是字節。為7位,或7+16位,或1+64位。

假設數Payload length === x,如果

x為0~126:數據的長度為x字節。
x為126:後續2個字節代表一個16位的無符號整數,該無符號整數的值為數據的長度。
x為127:後續8個字節代表一個64位的無符號整數(最高位為0),該無符號整數的值為數據的長度。

此外,如果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字節。所有的擴展都必須聲明擴展數據的長度,或者可以如何計算出擴展數據的長度。此外,擴展如何使用必須在握手階段就協商好。如果擴展數據存在,那麼載荷數據長度必須將擴展數據的長度包含在內。

應用數據:任意的應用數據,在擴展數據之後(如果存在擴展數據),佔據了數據幀剩餘的位置。載荷數據長度 減去 擴展數據長度,就得到應用數據的長度。

3、掩碼算法

掩碼鍵(Masking-key)是由客戶端挑選出來的32位的隨機數。掩碼操作不會影響數據載荷的長度。掩碼、反掩碼操作都採用如下算法:

首先,假設:

【1】original-octet-i:為原始數據的第i字節。

【2】transformed-octet-i:為轉換後的數據的第i字節。

【3】j:為i mod 4的結果。

【4】masking-key-octet-j:為mask key第j字節。

算法描述為: 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表示數據交互。

1、數據分片

WebSocket的每條消息可能被切分成多個數據幀。當WebSocket的接收方收到一個數據幀時,會根據FIN的值來判斷,是否已經收到消息的最後一個數據幀。

FIN=1表示當前數據幀為消息的最後一個數據幀,此時接收方已經收到完整的消息,可以對消息進行處理。FIN=0,則接收方還需要繼續監聽接收其餘的數據幀。

此外,opcode在數據交換的場景下,表示的是數據的類型。0x01表示文本,0x02表示二進制。而0x00比較特殊,表示延續幀(continuation frame),顧名思義,就是完整消息對應的數據幀還沒接收完。

2、數據分片例子

直接看例子更形象些。下面例子來自MDN,可以很好地演示數據的分片。客戶端向服務端兩次發送消息,服務端收到消息後回應客戶端,這裡主要看客戶端往服務端發送的消息。

第一條消息

FIN=1, 表示是當前消息的最後一個數據幀。服務端收到當前數據幀後,可以處理消息。opcode=0x1,表示客戶端發送的是文本類型。

第二條消息

【1】FIN=0,opcode=0x1,表示發送的是文本類型,且消息還沒發送完成,還有後續的數據幀。

【2】FIN=0,opcode=0x0,表示消息還沒發送完成,還有後續的數據幀,當前的數據幀需要接在上一條數據幀之後。

【3】FIN=1,opcode=0x0,表示消息已經發送完成,沒有後續的數據幀,當前的數據幀需要接在上一條數據幀之後。服務端可以將關聯的數據幀組裝成完整的消息。


連接保持+心跳

WebSocket為了保持客戶端、服務端的實時雙向通信,需要確保客戶端、服務端之間的TCP通道保持連接沒有斷開。然而,對於長時間沒有數據往來的連接,如果依舊長時間保持著,可能會浪費包括的連接資源。

但不排除有些場景,客戶端、服務端雖然長時間沒有數據往來,但仍需要保持連接。這個時候,可以採用心跳來實現。

發送方->接收方:ping
接收方->發送方:pong

ping、pong的操作,對應的是WebSocket的兩個控制幀,opcode分別是0x9、0xA。

舉例,WebSocket服務端向客戶端發送ping,只需要如下代碼(採用ws模塊)

ws.ping('', false, true);

Sec-WebSocket-Key/Accept的作用

前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在於提供基礎的防護,減少惡意連接、意外連接。

作用大致歸納如下:

【1】避免服務端收到非法的websocket連接(比如http客戶端不小心請求連接websocket服務,此時服務端可以直接拒絕連接)

【2】確保服務端理解websocket連接。因為ws握手階段採用的是http協議,因此可能ws連接是被一個http服務器處理並返回的,此時客戶端可以通過Sec-WebSocket-Key來確保服務端認識ws協議。(並非百分百保險,比如總是存在那麼些無聊的http服務器,光處理Sec-WebSocket-Key,但並沒有實現ws協議。。。)

【3】用瀏覽器裡發起ajax請求,設置header時,Sec-WebSocket-Key以及其他相關的header是被禁止的。這樣可以避免客戶端發送ajax請求時,意外請求協議升級(websocket upgrade)

【4】可以防止反向代理(不理解ws協議)返回錯誤的數據。比如反向代理前後收到兩次ws連接的升級請求,反向代理把第一次請求的返回給cache住,然後第二次請求到來時直接把cache住的請求給返回(無意義的返回)。

【5】Sec-WebSocket-Key主要目的並不是確保數據的安全性,因為Sec-WebSocket-Key、Sec-WebSocket-Accept的轉換計算公式是公開的,而且非常簡單,最主要的作用是預防一些常見的意外情況(非故意的)。

強調:Sec-WebSocket-Key/Sec-WebSocket-Accept 的換算,只能帶來基本的保障,但連接是否安全、數據是否安全、客戶端/服務端是否合法的 ws客戶端、ws服務端,其實並沒有實際性的保證。 


數據掩碼的作用

WebSocket協議中,數據掩碼的作用是增強協議的安全性。但數據掩碼並不是為了保護數據本身,因為算法本身是公開的,運算也不復雜。除了加密通道本身,似乎沒有太多有效的保護通信安全的辦法。

那麼為什麼還要引入掩碼計算呢,除了增加計算機器的運算量外似乎並沒有太多的收益(這也是不少同學疑惑的點)。

答案還是兩個字:安全。但並不是為了防止數據洩密,而是為了防止早期版本的協議中存在的代理緩存汙染攻擊(proxy cache poisoning attacks)等問題。

1、代理緩存汙染攻擊

在正式描述攻擊步驟之前,我們假設有如下參與者:

【1】攻擊者、攻擊者自己控制的服務器(簡稱“邪惡服務器”)、攻擊者偽造的資源(簡稱“邪惡資源”)

【2】受害者、受害者想要訪問的資源(簡稱“正義資源”)

【3】受害者實際想要訪問的服務器(簡稱“正義服務器”)

【4】中間代理服務器

攻擊步驟一:

【1】攻擊者瀏覽器 向 邪惡服務器 發起WebSocket連接。根據前文,首先是一個協議升級請求。

【2】協議升級請求 實際到達 代理服務器

【3】代理服務器 將協議升級請求轉發到 邪惡服務器

【4】邪惡服務器 同意連接,代理服務器 將響應轉發給 攻擊者

由於 upgrade 的實現上有缺陷,

代理服務器 以為之前轉發的是普通的HTTP消息。因此,當協議服務器 同意連接,代理服務器以為本次會話已經結束。

攻擊步驟二:

【1】攻擊者 在之前建立的連接上,通過WebSocket的接口向 邪惡服務器 發送數據,且數據是精心構造的HTTP格式的文本。其中包含了 正義資源 的地址,以及一個偽造的host(指向正義服務器)。(見後面報文)

【2】請求到達 代理服務器 。雖然複用了之前的TCP連接,但 代理服務器 以為是新的HTTP請求。

【3】代理服務器邪惡服務器 請求

邪惡資源

【4】邪惡服務器 返回 邪惡資源代理服務器 緩存住 邪惡資源(url是對的,但host是 正義服務器 的地址)。

到這裡,受害者可以登場了:

【1】受害者 通過 代理服務器 訪問 正義服務器正義資源

【2】代理服務器 檢查該資源的url、host,發現本地有一份緩存(偽造的)。

【3】代理服務器邪惡資源 返回給

受害者

【4】受害者 卒。

2、當前解決方案

最初的提案是對數據進行加密處理。基於安全、效率的考慮,最終採用了折中的方案:對數據載荷進行掩碼處理。

需要注意的是,這裡只是限制了瀏覽器對數據載荷進行掩碼處理,但是壞人完全可以實現自己的WebSocket客戶端、服務端,不按規則來,攻擊可以照常進行。

但是對瀏覽器加上這個限制後,可以大大增加攻擊的難度,以及攻擊的影響範圍。如果沒有這個限制,只需要在網上放個釣魚網站騙人去訪問,一下子就可以在短時間內展開大範圍的攻擊。


總結

WebSocket可寫的東西還挺多,比如WebSocket擴展。客戶端、服務端之間是如何協商、使用擴展的。WebSocket擴展可以給協議本身增加很多能力和想象空間,比如數據的壓縮、加密,以及多路複用等。


分享到:


相關文章: