最近跟朋友聊到 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: Upgrade 及 Upgrade: 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 意見:
張貼意見