summaryrefslogtreecommitdiff
path: root/flash-src/net/gimite/websocket/WebSocket.as
diff options
context:
space:
mode:
Diffstat (limited to 'flash-src/net/gimite/websocket/WebSocket.as')
-rw-r--r--flash-src/net/gimite/websocket/WebSocket.as257
1 files changed, 137 insertions, 120 deletions
diff --git a/flash-src/net/gimite/websocket/WebSocket.as b/flash-src/net/gimite/websocket/WebSocket.as
index f043b60..3bafd63 100644
--- a/flash-src/net/gimite/websocket/WebSocket.as
+++ b/flash-src/net/gimite/websocket/WebSocket.as
@@ -1,12 +1,13 @@
// Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
// License: New BSD License
// Reference: http://dev.w3.org/html5/websockets/
-// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
+// Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
package net.gimite.websocket {
import com.adobe.net.proxies.RFC2817Socket;
import com.gsolo.encryption.MD5;
+import com.gsolo.encryption.SHA1;
import com.hurlant.crypto.tls.TLSConfig;
import com.hurlant.crypto.tls.TLSEngine;
import com.hurlant.crypto.tls.TLSSecurityParameters;
@@ -30,6 +31,7 @@ public class WebSocket extends EventDispatcher {
private static var OPEN:int = 1;
private static var CLOSING:int = 2;
private static var CLOSED:int = 3;
+ private static var GUID:String = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
private var id:int;
private var rawSocket:Socket;
@@ -49,9 +51,14 @@ public class WebSocket extends EventDispatcher {
private var readyState:int = CONNECTING;
private var cookie:String;
private var headers:String;
- private var noiseChars:Array;
private var expectedDigest:String;
private var logger:IWebSocketLogger;
+ private var b64encoder:Base64Encoder = new Base64Encoder();
+
+ private var frame_fin:int = -1;
+ private var frame_opcode:int = -1;
+ private var frame_hlength:uint = 0;
+ private var frame_plength:uint = 0;
public function WebSocket(
id:int, url:String, protocols:Array, origin:String,
@@ -60,7 +67,6 @@ public class WebSocket extends EventDispatcher {
logger:IWebSocketLogger) {
this.logger = logger;
this.id = id;
- initNoiseChars();
this.url = url;
var m:Array = url.match(/^(\w+):\/\/([^\/:]+)(:(\d+))?(\/.*)?(\?.*)?$/);
if (!m) fatal("SYNTAX_ERR: invalid url: " + url);
@@ -127,30 +133,109 @@ public class WebSocket extends EventDispatcher {
}
public function send(encData:String):int {
- var data:String = decodeURIComponent(encData);
+ var raw_str:String = decodeURIComponent(encData);
+ var data:ByteArray = new ByteArray();
+ data.writeUTFBytes(raw_str);
+ var plength:uint = data.length;
+
if (readyState == OPEN) {
- socket.writeByte(0x00);
- socket.writeUTFBytes(data);
- socket.writeByte(0xff);
+ // TODO: binary API support
+ var header:ByteArray = new ByteArray();
+
+ header.writeByte(0x80 | 0x01); // FIN + text opcode
+
+ if (plength <= 125) {
+ header.writeByte(0x80 | plength); // Masked + length
+ } else if (plength > 125 && plength < 65536) {
+ header.writeByte(0x80 | 126); // Masked + 126
+ header.writeShort(plength);
+ } else if (plength >= 65536 && plength < 4294967296) {
+ header.writeByte(0x80 | 127); // Masked + 127
+ header.writeUnsignedInt(0); // zero high order bits
+ header.writeUnsignedInt(plength);
+ } else {
+ fatal("Send frame size too large");
+ return 0;
+ }
+
+ // Generate a mask
+ var mask:Array = new Array(4);
+ for (var i:int = 0; i < 4; i++) {
+ mask[i] = randomInt(0, 255);
+ header.writeByte(mask[i]);
+ }
+ for (i = 0; i < data.length; i++) {
+ data[i] = mask[i%4] ^ data[i];
+ }
+
+ socket.writeBytes(header);
+ socket.writeBytes(data);
socket.flush();
logger.log("sent: " + data);
return -1;
} else if (readyState == CLOSING || readyState == CLOSED) {
- var bytes:ByteArray = new ByteArray();
- bytes.writeUTFBytes(data);
- return bytes.length; // not sure whether it should include \x00 and \xff
+ return plength;
} else {
fatal("invalid state");
return 0;
}
}
+
+ public function parseFrame():int {
+ var cur_pos:int = buffer.position;
+
+ frame_hlength = 2;
+ if (buffer.length < frame_hlength) {
+ return -1;
+ }
+
+ frame_opcode = buffer[0] & 0x0f;
+ frame_fin = (buffer[0] & 0x80) >> 7;
+ frame_plength = buffer[1] & 0x7f;
+
+ if (frame_plength == 126) {
+ frame_hlength = 4;
+ if (buffer.length < frame_hlength) {
+ return -1;
+ }
+
+ buffer.endian = Endian.BIG_ENDIAN;
+ buffer.position = 2;
+ frame_plength = buffer.readUnsignedShort();
+ buffer.position = cur_pos;
+ } else if (frame_plength == 127) {
+ frame_hlength = 10;
+ if (buffer.length < frame_hlength) {
+ return -1;
+ }
+
+ buffer.endian = Endian.BIG_ENDIAN;
+ buffer.position = 2;
+ // Protocol allows 64-bit length, but we only handle 32-bit
+ var big:uint = buffer.readUnsignedInt(); // Skip high 32-bits
+ frame_plength = buffer.readUnsignedInt(); // Low 32-bits
+ buffer.position = cur_pos;
+ if (big != 0) {
+ onError("Frame length exceeds 4294967295. Bailing out!");
+ return -1;
+ }
+ }
+
+ if (buffer.length < frame_hlength + frame_plength) {
+ return -1;
+ }
+
+ return 1;
+ }
public function close(isError:Boolean = false):void {
logger.log("close");
try {
if (readyState == OPEN && !isError) {
- socket.writeByte(0xff);
- socket.writeByte(0x00);
+ // TODO: send code and reason
+ socket.writeByte(0x80 | 0x08); // FIN + close opcode
+ socket.writeByte(0x80 | 0x00); // Masked + no payload
+ socket.writeUnsignedInt(0x00); // Mask
socket.flush();
}
socket.close();
@@ -169,10 +254,11 @@ public class WebSocket extends EventDispatcher {
var defaultPort:int = scheme == "wss" ? 443 : 80;
var hostValue:String = host + (port == defaultPort ? "" : ":" + port);
- var key1:String = generateKey();
- var key2:String = generateKey();
- var key3:String = generateKey3();
- expectedDigest = getSecurityDigest(key1, key2, key3);
+ var key:String = generateKey();
+
+ SHA1.b64pad = "=";
+ expectedDigest = SHA1.b64_sha1(key + GUID);
+
var opt:String = "";
if (requestedProtocols.length > 0) {
opt += "Sec-WebSocket-Protocol: " + requestedProtocols.join(",") + "\r\n";
@@ -182,20 +268,18 @@ public class WebSocket extends EventDispatcher {
var req:String = StringUtil.substitute(
"GET {0} HTTP/1.1\r\n" +
- "Upgrade: WebSocket\r\n" +
- "Connection: Upgrade\r\n" +
"Host: {1}\r\n" +
- "Origin: {2}\r\n" +
- "Cookie: {3}\r\n" +
- "Sec-WebSocket-Key1: {4}\r\n" +
- "Sec-WebSocket-Key2: {5}\r\n" +
- "{6}" +
+ "Upgrade: websocket\r\n" +
+ "Connection: Upgrade\r\n" +
+ "Sec-WebSocket-Key: {2}\r\n" +
+ "Sec-WebSocket-Origin: {3}\r\n" +
+ "Sec-WebSocket-Version: 7\r\n" +
+ "Cookie: {4}\r\n" +
+ "{5}" +
"\r\n",
- path, hostValue, origin, cookie, key1, key2, opt);
+ path, hostValue, key, origin, cookie, opt);
logger.log("request header:\n" + req);
socket.writeUTFBytes(req);
- logger.log("sent key3: " + key3);
- writeBytes(key3);
socket.flush();
}
@@ -253,46 +337,30 @@ public class WebSocket extends EventDispatcher {
if (headerState == 4) {
var headerStr:String = readUTFBytes(buffer, 0, pos + 1);
logger.log("response header:\n" + headerStr);
- if (!validateHeader(headerStr)) return;
- removeBufferBefore(pos + 1);
- pos = -1;
- }
- } else if (headerState == 4) {
- if (pos == 15) {
- var replyDigest:String = readBytes(buffer, 0, 16);
- logger.log("reply digest: " + replyDigest);
- if (replyDigest != expectedDigest) {
- onError("digest doesn't match: " + replyDigest + " != " + expectedDigest);
- return;
- }
- headerState = 5;
+ if (!validateHandshake(headerStr)) return;
removeBufferBefore(pos + 1);
pos = -1;
readyState = OPEN;
this.dispatchEvent(new WebSocketEvent("open"));
}
} else {
- if (buffer[pos] == 0xff && pos > 0) {
- if (buffer[0] != 0x00) {
- onError("data must start with \\x00");
- return;
- }
- var data:String = readUTFBytes(buffer, 1, pos - 1);
- logger.log("received: " + data);
- this.dispatchEvent(new WebSocketEvent("message", encodeURIComponent(data)));
- removeBufferBefore(pos + 1);
+ if (parseFrame() == 1) {
+ var data:String = readUTFBytes(buffer, frame_hlength, frame_plength);
+ removeBufferBefore(frame_hlength + frame_plength);
pos = -1;
- } else if (pos == 1 && buffer[0] == 0xff && buffer[1] == 0x00) { // closing
- logger.log("received closing packet");
- removeBufferBefore(pos + 1);
- pos = -1;
- close();
+ if (frame_opcode == 0x01 || frame_opcode == 0x02) {
+ this.dispatchEvent(new WebSocketEvent("message", encodeURIComponent(data)));
+ } else if (frame_opcode == 0x08) {
+ // TODO: extract code and reason string
+ logger.log("received closing packet");
+ close();
+ }
}
}
}
}
- private function validateHeader(headerStr:String):Boolean {
+ private function validateHandshake(headerStr:String):Boolean {
var lines:Array = headerStr.split(/\r\n/);
if (!lines[0].match(/^HTTP\/1.1 101 /)) {
onError("bad response: " + lines[0]);
@@ -318,21 +386,17 @@ public class WebSocket extends EventDispatcher {
onError("invalid Connection: " + header["Connection"]);
return false;
}
- if (!lowerHeader["sec-websocket-origin"]) {
- if (lowerHeader["websocket-origin"]) {
- onError(
- "The WebSocket server speaks old WebSocket protocol, " +
- "which is not supported by web-socket-js. " +
- "It requires WebSocket protocol 76 or later. " +
- "Try newer version of the server if available.");
- } else {
- onError("header Sec-WebSocket-Origin is missing");
- }
+ if (!lowerHeader["sec-websocket-accept"]) {
+ onError(
+ "The WebSocket server speaks old WebSocket protocol, " +
+ "which is not supported by web-socket-js. " +
+ "It requires WebSocket protocol HyBi 7. " +
+ "Try newer version of the server if available.");
return false;
}
- var resOrigin:String = lowerHeader["sec-websocket-origin"];
- if (resOrigin != origin) {
- onError("origin doesn't match: '" + resOrigin + "' != '" + origin + "'");
+ var replyDigest:String = header["sec-websocket-accept"]
+ if (replyDigest != expectedDigest) {
+ onError("digest doesn't match: " + replyDigest + " != " + expectedDigest);
return false;
}
if (requestedProtocols.length > 0) {
@@ -354,61 +418,14 @@ public class WebSocket extends EventDispatcher {
buffer = nextBuffer;
}
- private function initNoiseChars():void {
- noiseChars = new Array();
- for (var i:int = 0x21; i <= 0x2f; ++i) {
- noiseChars.push(String.fromCharCode(i));
- }
- for (var j:int = 0x3a; j <= 0x7a; ++j) {
- noiseChars.push(String.fromCharCode(j));
- }
- }
-
private function generateKey():String {
- var spaces:uint = randomInt(1, 12);
- var max:uint = uint.MAX_VALUE / spaces;
- var number:uint = randomInt(0, max);
- var key:String = (number * spaces).toString();
- var noises:int = randomInt(1, 12);
- var pos:int;
- for (var i:int = 0; i < noises; ++i) {
- var char:String = noiseChars[randomInt(0, noiseChars.length - 1)];
- pos = randomInt(0, key.length);
- key = key.substr(0, pos) + char + key.substr(pos);
- }
- for (var j:int = 0; j < spaces; ++j) {
- pos = randomInt(1, key.length - 1);
- key = key.substr(0, pos) + " " + key.substr(pos);
- }
- return key;
- }
-
- private function generateKey3():String {
- var key3:String = "";
- for (var i:int = 0; i < 8; ++i) {
- key3 += String.fromCharCode(randomInt(0, 255));
- }
- return key3;
- }
-
- private function getSecurityDigest(key1:String, key2:String, key3:String):String {
- var bytes1:String = keyToBytes(key1);
- var bytes2:String = keyToBytes(key2);
- return MD5.rstr_md5(bytes1 + bytes2 + key3);
- }
-
- private function keyToBytes(key:String):String {
- var keyNum:uint = parseInt(key.replace(/[^\d]/g, ""));
- var spaces:uint = 0;
- for (var i:int = 0; i < key.length; ++i) {
- if (key.charAt(i) == " ") ++spaces;
+ var vals:String = "";
+ for (var i:int = 0; i < 16; i++) {
+ vals = vals + randomInt(0, 127).toString();
}
- var resultNum:uint = keyNum / spaces;
- var bytes:String = "";
- for (var j:int = 3; j >= 0; --j) {
- bytes += String.fromCharCode((resultNum >> (j * 8)) & 0xff);
- }
- return bytes;
+ b64encoder.reset();
+ b64encoder.encode(vals);
+ return b64encoder.toString();
}
// Writes byte sequence to socket.