Jen

HTML5 - WebSocket 出現 INVALID_STATE_ERR 錯誤處理

最近跟朋友聊到 Web Socket 跟 WebGL,突然就拿出去年寫的 Web Socket Server 來 Build,但是拋出了 Exception。









簡單的一段 js 程式碼如下, Exception 就這樣被拋出來。
var socket;
window.onload = function (e) {
	socket = new WebSocket('ws://localhost:7878/handsomejen');
	socket.onopen = function () {
		alert('handshake 成功!');
	};
};
function sendMsg() {
	try {
		socket.send('test');
	}
	catch (err) {
		alert(err);
	}
}

在 socket.send( message ) 出現 INVALID_STATE_ERR 錯誤。一個從來沒改過的程式碼,怎麼會出錯呢? 一定是大環境變動,我們沒跟著變,因為我們沒跟著變,所以就被淘汰了(物競天擇之適者生存)。上網看了一下 W3C 規範的文件,ctrl-f 剛好有提到 INVALID_STATE_ERR 這個錯誤,是因為你在連線還沒建立的時候就執行了 Send() 這個方法,原文如下。

The send(data) method transmits data using the connection. If the readyState attribute is CONNECTING, it must raise an INVALID_STATE_ERR exception.

WebSocket 跟 xmlHttpRequest 一樣, 定義了 readyState 屬性,來表示現在物件正處於哪一種狀態。
0 : CONNECTING
1 : OPEN  (這個狀態才是「建線已建立」)
2 : CLOSING
3 : CLOSED

那為什麼原本好端端的程式碼,會錯了呢? 我猜的是 hand shake 的 protocol 有改過才會這樣,因為 HTML 5 還算是個  draft,常常改也是正常的吧,而剛好我看到 Chromium Blog 這篇文章提到 Chrome 原本對 Web Socket 的實作是 base on draft-hixie-thewebsocketprotocol-75,而長期經過社群朋友們的討論及建議,後來更新成了  draft-ietf-hybi-thewebsocketprotocol-00  ( draft-hixie-thewebsocketprotocol-76 ),至於一些規範細節有興趣的可以看看Chromium Blo這篇文章,我是沒什麼興趣,也懶得看。

而 Web Socket 及 Socket Server 之間是怎麼做 hand shake的呢?
當你執行 new WebSocket("ws://localhost:7788/handsomejen");
client (Chrome) 會丟一個 http GET Request 到 Server ,readyState 會是 0 ( CONNECTING ),而那個 http GET Request 長這樣:













你會看到最重要的2個 header  就是 Connection: UpgradeUpgrade: WebSocket。(HTTP 1.1 中規範的 upgrade header)

Connection: Upgrade => 用來告訴 Server 這個連線需要 upgrade。
Upgrade: WebSocket => 用來告訴 Server 要把這個連線 Upgrade 成 Web Socket。

上面這些 header 在前一個版本就有了,而會造成原本的 Server 無法 handshake 成功的元兇就是圖中 Response 區段所看到的 (Challenge Response),這個也就是 W3C 為了增加 Web Socket 安全性所定的新規範,而原本的 WebSocket-Location、WebSocket-Origin...也都加上了 「Sec- 」等前置詞,跟舊的 protocol 不一樣,這就是一直無法 Hand Shake成功的原因。













而不知道Challenge Response怎麼產生的是要怎麼寫 Server,所以就去啃一下文件,ctrl-f 找「Server-side requirements」關鍵字,文件寫的非常清楚。Challenge Response 的產生方式如下:

首先你會從 Client 收到 下面這 3個 header:
Key_1 = Sec-WebSocket-Key1.
Key_2 = Sec-WebSocket-Key2.
Key_3 = 8 bit data in the end of request.

第 1 步:
key-number-1 : 拿 Key_1 所有數字組合而成。
key-number-2 : 拿 Key_2 所有數字組合而成。

第 2 步:
spaces_1 : key_1 空白符號的數量。
spaces_2 : key_2 空白符號的數量。

第 3 步:
part_1 =  key-number-1 / spaces_1
part_2 = key-number-2 / spaces_2

接著就是拿 part_1 、 part_2 及 原本的 Key_3 來做處理。

challenge :  把 part_1 、part_2 、 key_3 轉成一個 big-endian byte 陣列,並串聯起來。
response : 最後把 challenge MD5 起來,就是要回應的 Challenge Response。

實作的 C# Code 如下,
private static byte[] GetChallengeResponse(string secKey1, string secKey2, byte[] secKey3)
{
    Regex rgxUnInt = new Regex("[^0-9]");

    //把空白去除,把所有數字提出來
    Int64 iK1 = Int64.Parse(rgxUnInt.Replace(secKey1, string.Empty));
    Int64 iK2 = Int64.Parse(rgxUnInt.Replace(secKey2, string.Empty));

    //  (提出來的數字/空白數量)
    int k1Spaces = secKey1.Count(c => c == ' ');
    int k2Spaces = secKey2.Count(c => c == ' ');
    int k1FinalNum = (int)(iK1 / k1Spaces);
    int k2FinalNum = (int)(iK2 / k2Spaces);

    //文件中提到 「 expressed as a big-endian unsigned 32-bit integer」,
    //陣例是排法是 little-endian, 所以要 Reverse().
    byte[] bKey1 = BitConverter.GetBytes(k1FinalNum).Reverse().ToArray();
    byte[] bKey2 = BitConverter.GetBytes(k2FinalNum).Reverse().ToArray();
    byte[] bKey3 = secKey3;

    //concatenation of all.
    List<byte> listChallenge = new List<byte>();
    listChallenge.AddRange(bKey1);
    listChallenge.AddRange(bKey2);
    listChallenge.AddRange(bKey3);
    
    //把 Challenge MD5
    byte[] response = MD5.Create().ComputeHash(listChallenge.ToArray());
    return response;
}

最後寫一個 console ,拿剛剛 hand shake 成功的記錄(上面的圖)來測試,再來比對一下結果。
string sKey1 = "<oz&ragl10 9="">761 8  4 95";  
//同上方圖中的Sec-WebSocket-Key1

string sKey2 = "28v  355mP42 =_  360m"; 
//同上方圖中的Sec-WebSocket-Key2

byte[] sKey3 = new byte[] { 0xC9,0xB6,0xCF,0x38,0xBA,0x27,0x6E,0x45 }; 
//同上方圖中的 (Key3)

byte[] challengeResponse = GetChallengeResponse(sKey1, sKey2, sKey3);

string stra = BitConverter.ToString(challengeResponse);

Console.WriteLine(stra);


比對結果是一樣的。

把這個修改到 Web Socket Server  就可以 hand shake 成功了。不過,可能過幾個月這個 protocol 又會被更新。

但目前 Web Socket 並不廣泛被應用,有興趣的再看看就好。

(各家 Browser 對 Web Socket 的支援度, 來源 : http://caniuse.com/#search=web socket)

Related articles : 
http://people.mozilla.com/~bsterne/content-security-policy/origin-header-proposal.html
http://blog.makezine.com/archive/2007/08/dns-rebinding-how-an-attacker.html

0 意見: