內容概覽
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分鐘從入門到精通](http://p2.ttnews.xyz/loading.gif)
客戶端
代碼如下,向8080端口發起WebSocket連接。
連接建立後,打印日誌,同時向服務端發送消息。接收到來自服務端的消息後,同樣打印日誌。
![WebSocket協議:5分鐘從入門到精通](http://p2.ttnews.xyz/loading.gif)
運行結果
可分別查看服務端、客戶端的日誌,這裡不展開。
服務端輸出
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的數據幀格式。
WebSocket客戶端、服務端通信的最小單位是幀(frame),由1個或多個幀組成一條完整的消息(message)。
【1】發送端:將消息切割成多個幀,併發送給服務端;
【2】接收端:接收消息幀,並將關聯的幀重新組裝成完整的消息;
本節的重點,就是講解數據幀的格式。
1、數據幀格式概覽
下面給出了WebSocket數據幀的統一格式。熟悉TCP/IP協議的同學對這樣的圖應該不陌生。
【1】從左到右,單位是比特。比如FIN、RSV1各佔據1比特,opcode佔據4比特。
【2】內容包括了標識、操作代碼、掩碼、數據、數據長度等。
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擴展可以給協議本身增加很多能力和想象空間,比如數據的壓縮、加密,以及多路複用等。
閱讀更多 JS加加網 的文章