PageRenderTime 571ms CodeModel.GetById 121ms app.highlight 249ms RepoModel.GetById 193ms app.codeStats 1ms

/flash-src/src/net/gimite/websocket/WebSocket.as

http://github.com/gimite/web-socket-js
ActionScript | 613 lines | 528 code | 61 blank | 24 comment | 149 complexity | 682118586ca5e88e6db9c59fa040f4e1 MD5 | raw file
  1// Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
  2// License: New BSD License
  3// Reference: http://dev.w3.org/html5/websockets/
  4// Reference: http://tools.ietf.org/html/rfc6455
  5
  6package net.gimite.websocket {
  7
  8import com.adobe.net.proxies.RFC2817Socket;
  9import com.gsolo.encryption.SHA1;
 10import com.hurlant.crypto.tls.TLSConfig;
 11import com.hurlant.crypto.tls.TLSEngine;
 12import com.hurlant.crypto.tls.TLSSecurityParameters;
 13import com.hurlant.crypto.tls.TLSSocket;
 14
 15import flash.display.*;
 16import flash.errors.*;
 17import flash.events.*;
 18import flash.external.*;
 19import flash.net.*;
 20import flash.system.*;
 21import flash.utils.*;
 22
 23import mx.controls.*;
 24import mx.core.*;
 25import mx.events.*;
 26import mx.utils.*;
 27
 28public class WebSocket extends EventDispatcher {
 29  
 30  private static const WEB_SOCKET_GUID:String = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
 31  
 32  private static const CONNECTING:int = 0;
 33  private static const OPEN:int = 1;
 34  private static const CLOSING:int = 2;
 35  private static const CLOSED:int = 3;
 36  
 37  private static const OPCODE_CONTINUATION:int = 0x00;
 38  private static const OPCODE_TEXT:int = 0x01;
 39  private static const OPCODE_BINARY:int = 0x02;
 40  private static const OPCODE_CLOSE:int = 0x08;
 41  private static const OPCODE_PING:int = 0x09;
 42  private static const OPCODE_PONG:int = 0x0a;
 43  
 44  private static const STATUS_NORMAL_CLOSURE:int = 1000;
 45  private static const STATUS_NO_CODE:int = 1005;
 46  private static const STATUS_CLOSED_ABNORMALLY:int = 1006;
 47  private static const STATUS_CONNECTION_ERROR:int = 5000;
 48  
 49  private var id:int;
 50  private var url:String;
 51  private var scheme:String;
 52  private var host:String;
 53  private var port:uint;
 54  private var path:String;
 55  private var origin:String;
 56  private var requestedProtocols:Array;
 57  private var cookie:String;
 58  private var headers:String;
 59  
 60  private var rawSocket:Socket;
 61  private var tlsSocket:TLSSocket;
 62  private var tlsConfig:TLSConfig;
 63  private var socket:Socket;
 64  
 65  private var acceptedProtocol:String;
 66  private var expectedDigest:String;
 67  
 68  private var buffer:ByteArray = new ByteArray();
 69  private var fragmentsBuffer:ByteArray = null;
 70  private var headerState:int = 0;
 71  private var readyState:int = CONNECTING;
 72  
 73  private var logger:IWebSocketLogger;
 74  private var base64Encoder:Base64Encoder = new Base64Encoder();
 75  
 76  public function WebSocket(
 77      id:int, url:String, protocols:Array, origin:String,
 78      proxyHost:String, proxyPort:int,
 79      cookie:String, headers:String,
 80      logger:IWebSocketLogger) {
 81    this.logger = logger;
 82    this.id = id;
 83    this.url = url;
 84    var m:Array = url.match(/^(\w+):\/\/([^\/:]+)(:(\d+))?(\/.*)?(\?.*)?$/);
 85    if (!m) fatal("SYNTAX_ERR: invalid url: " + url);
 86    this.scheme = m[1];
 87    this.host = m[2];
 88    var defaultPort:int = scheme == "wss" ? 443 : 80;
 89    this.port = parseInt(m[4]) || defaultPort;
 90    this.path = (m[5] || "/") + (m[6] || "");
 91    this.origin = origin;
 92    this.requestedProtocols = protocols;
 93    this.cookie = cookie;
 94    // if present and not the empty string, headers MUST end with \r\n
 95    // headers should be zero or more complete lines, for example
 96    // "Header1: xxx\r\nHeader2: yyyy\r\n"
 97    this.headers = headers;
 98    
 99    if (proxyHost != null && proxyPort != 0){
100      if (scheme == "wss") {
101        fatal("wss with proxy is not supported");
102      }
103      var proxySocket:RFC2817Socket = new RFC2817Socket();
104      proxySocket.setProxyInfo(proxyHost, proxyPort);
105      proxySocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
106      rawSocket = socket = proxySocket;
107    } else {
108      rawSocket = new Socket();
109      if (scheme == "wss") {
110        tlsConfig= new TLSConfig(TLSEngine.CLIENT,
111            null, null, null, null, null,
112            TLSSecurityParameters.PROTOCOL_VERSION);
113        tlsConfig.trustAllCertificates = true;
114        tlsConfig.ignoreCommonNameMismatch = true;
115        tlsSocket = new TLSSocket();
116        tlsSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
117        socket = tlsSocket;
118      } else {
119        rawSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
120        socket = rawSocket;
121      }
122    }
123    rawSocket.addEventListener(Event.CLOSE, onSocketClose);
124    rawSocket.addEventListener(Event.CONNECT, onSocketConnect);
125    rawSocket.addEventListener(IOErrorEvent.IO_ERROR, onSocketIoError);
126    rawSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSocketSecurityError);
127    rawSocket.connect(host, port);
128  }
129  
130  /**
131   * @return  This WebSocket's ID.
132   */
133  public function getId():int {
134    return this.id;
135  }
136  
137  /**
138   * @return this WebSocket's readyState.
139   */
140  public function getReadyState():int {
141    return this.readyState;
142  }
143
144  public function getAcceptedProtocol():String {
145    return this.acceptedProtocol;
146  }
147  
148  public function send(encData:String):int {
149    var data:String;
150    try {
151      data = decodeURIComponent(encData);
152    } catch (ex:URIError) {
153      logger.error("SYNTAX_ERR: URIError in send()");
154      return 0;
155    }
156    logger.log("send: " + data);
157    var dataBytes:ByteArray = new ByteArray();
158    dataBytes.writeUTFBytes(data);
159    if (readyState == OPEN) {
160      var frame:WebSocketFrame = new WebSocketFrame();
161      frame.opcode = OPCODE_TEXT;
162      frame.payload = dataBytes;
163      if (sendFrame(frame)) {
164        return -1;
165      } else {
166        return dataBytes.length;
167      }
168    } else if (readyState == CLOSING || readyState == CLOSED) {
169      return dataBytes.length;
170    } else {
171      fatal("invalid state");
172      return 0;
173    }
174  }
175  
176  public function close(
177      code:int = STATUS_NO_CODE, reason:String = "", origin:String = "client"):void {
178    if (code != STATUS_NORMAL_CLOSURE &&
179        code != STATUS_NO_CODE &&
180        code != STATUS_CONNECTION_ERROR) {
181      logger.error(StringUtil.substitute(
182          "Fail connection by {0}: code={1} reason={2}", origin, code, reason));
183    }
184    var closeConnection:Boolean =
185        code == STATUS_CONNECTION_ERROR || origin == "server";
186    try {
187      if (readyState == OPEN && code != STATUS_CONNECTION_ERROR) {
188        var frame:WebSocketFrame = new WebSocketFrame();
189        frame.opcode = OPCODE_CLOSE;
190        frame.payload = new ByteArray();
191        if (origin == "client" && code != STATUS_NO_CODE) {
192          frame.payload.writeShort(code);
193          frame.payload.writeUTFBytes(reason);
194        }
195        sendFrame(frame);
196      }
197      if (closeConnection) {
198        socket.close();
199      }
200    } catch (ex:Error) {
201      logger.error("Error: " + ex.message);
202    }
203    if (closeConnection) {
204      logger.log("closed");
205      var fireErrorEvent:Boolean = readyState != CONNECTING && code == STATUS_CONNECTION_ERROR;
206      readyState = CLOSED;
207      if (fireErrorEvent) {
208        dispatchEvent(new WebSocketEvent("error"));
209      }
210      var wasClean:Boolean = code != STATUS_CLOSED_ABNORMALLY && code != STATUS_CONNECTION_ERROR;
211      var eventCode:int = code == STATUS_CONNECTION_ERROR ? STATUS_CLOSED_ABNORMALLY : code;
212      dispatchCloseEvent(wasClean, eventCode, reason);
213    } else {
214      logger.log("closing");
215      readyState = CLOSING;
216    }
217  }
218  
219  private function onSocketConnect(event:Event):void {
220    logger.log("connected");
221
222    if (scheme == "wss") {
223      logger.log("starting SSL/TLS");
224      tlsSocket.startTLS(rawSocket, host, tlsConfig);
225    }
226    
227    var defaultPort:int = scheme == "wss" ? 443 : 80;
228    var hostValue:String = host + (port == defaultPort ? "" : ":" + port);
229    var key:String = generateKey();
230
231    SHA1.b64pad = "=";
232    expectedDigest = SHA1.b64_sha1(key + WEB_SOCKET_GUID);
233
234    var opt:String = "";
235    if (requestedProtocols.length > 0) {
236      opt += "Sec-WebSocket-Protocol: " + requestedProtocols.join(",") + "\r\n";
237    }
238    // if caller passes additional headers they must end with "\r\n"
239    if (headers) opt += headers;
240    
241    var req:String = StringUtil.substitute(
242      "GET {0} HTTP/1.1\r\n" +
243      "Host: {1}\r\n" +
244      "Upgrade: websocket\r\n" +
245      "Connection: Upgrade\r\n" +
246      "Sec-WebSocket-Key: {2}\r\n" +
247      "Origin: {3}\r\n" +
248      "Sec-WebSocket-Version: 13\r\n" +
249      (cookie == "" ? "" : "Cookie: {4}\r\n") +
250      "{5}" +
251      "\r\n",
252      path, hostValue, key, origin, cookie, opt);
253    logger.log("request header:\n" + req);
254    socket.writeUTFBytes(req);
255    socket.flush();
256  }
257
258  private function onSocketClose(event:Event):void {
259    logger.log("closed");
260    readyState = CLOSED;
261    dispatchCloseEvent(false, STATUS_CLOSED_ABNORMALLY, "");
262  }
263
264  private function onSocketIoError(event:IOErrorEvent):void {
265    var message:String;
266    if (readyState == CONNECTING) {
267      message = "cannot connect to Web Socket server at " + url + " (IoError: " + event.text + ")";
268    } else {
269      message =
270          "error communicating with Web Socket server at " + url +
271          " (IoError: " + event.text + ")";
272    }
273    onConnectionError(message);
274  }
275
276  private function onSocketSecurityError(event:SecurityErrorEvent):void {
277    var message:String;
278    if (readyState == CONNECTING) {
279      message =
280          "cannot connect to Web Socket server at " + url + " (SecurityError: " + event.text + ")\n" +
281          "make sure the server is running and Flash socket policy file is correctly placed";
282    } else {
283      message =
284          "error communicating with Web Socket server at " + url +
285          " (SecurityError: " + event.text + ")";
286    }
287    onConnectionError(message);
288  }
289  
290  private function onConnectionError(message:String):void {
291    if (readyState == CLOSED) return;
292    logger.error(message);
293    close(STATUS_CONNECTION_ERROR);
294  }
295
296  private function onSocketData(event:ProgressEvent):void {
297    var pos:int = buffer.length;
298    socket.readBytes(buffer, pos);
299    for (; pos < buffer.length; ++pos) {
300      if (headerState < 4) {
301        // try to find "\r\n\r\n"
302        if ((headerState == 0 || headerState == 2) && buffer[pos] == 0x0d) {
303          ++headerState;
304        } else if ((headerState == 1 || headerState == 3) && buffer[pos] == 0x0a) {
305          ++headerState;
306        } else {
307          headerState = 0;
308        }
309        if (headerState == 4) {
310          var headerStr:String = readUTFBytes(buffer, 0, pos + 1);
311          logger.log("response header:\n" + headerStr);
312          if (!validateHandshake(headerStr)) return;
313          removeBufferBefore(pos + 1);
314          pos = -1;
315          readyState = OPEN;
316          this.dispatchEvent(new WebSocketEvent("open"));
317        }
318      } else {
319        var frame:WebSocketFrame = parseFrame();
320        if (frame) {
321          removeBufferBefore(frame.length);
322          pos = -1;
323          if (frame.rsv != 0) {
324            close(1002, "RSV must be 0.");
325          } else if (frame.mask) {
326            close(1002, "Frame from server must not be masked.");
327          } else if (frame.opcode >= 0x08 && frame.opcode <= 0x0f && frame.payload.length >= 126) {
328            close(1004, "Payload of control frame must be less than 126 bytes.");
329          } else {
330            switch (frame.opcode) {
331              case OPCODE_CONTINUATION:
332                if (fragmentsBuffer == null) {
333                  close(1002, "Unexpected continuation frame");
334                } else {
335                  fragmentsBuffer.writeBytes(frame.payload);
336                  if (frame.fin) {
337                    data = readUTFBytes(fragmentsBuffer, 0, fragmentsBuffer.length);
338                    try {
339                      this.dispatchEvent(new WebSocketEvent("message", encodeURIComponent(data)));
340                    } catch (ex:URIError) {
341                      close(1007, "URIError while encoding the received data.");
342                    }
343                    fragmentsBuffer = null;
344                  }
345                }
346                break;
347              case OPCODE_TEXT:
348                if (frame.fin) {
349                var data:String = readUTFBytes(frame.payload, 0, frame.payload.length);
350                try {
351                  this.dispatchEvent(new WebSocketEvent("message", encodeURIComponent(data)));
352                } catch (ex:URIError) {
353                  close(1007, "URIError while encoding the received data.");
354                }
355                } else {
356                  fragmentsBuffer = new ByteArray();
357                  fragmentsBuffer.writeBytes(frame.payload);
358                }
359                break;
360              case OPCODE_BINARY:
361                // See https://github.com/gimite/web-socket-js/pull/89
362                // for discussion about supporting binary data.
363                close(1003, "Received binary data, which is not supported.");
364                break;
365              case OPCODE_CLOSE:
366                // Extracts code and reason string.
367                var code:int = STATUS_NO_CODE;
368                var reason:String = "";
369                if (frame.payload.length >= 2) {
370                  frame.payload.endian = Endian.BIG_ENDIAN;
371                  frame.payload.position = 0;
372                  code = frame.payload.readUnsignedShort();
373                  reason = readUTFBytes(frame.payload, 2, frame.payload.length - 2);
374                }
375                logger.log("received closing frame");
376                close(code, reason, "server");
377                break;
378              case OPCODE_PING:
379                sendPong(frame.payload);
380                break;
381              case OPCODE_PONG:
382                break;
383              default:
384                close(1002, "Received unknown opcode: " + frame.opcode);
385                break;
386            }
387          }
388        }
389      }
390    }
391  }
392  
393  private function validateHandshake(headerStr:String):Boolean {
394    var lines:Array = headerStr.split(/\r\n/);
395    if (!lines[0].match(/^HTTP\/1.1 101 /)) {
396      onConnectionError("bad response: " + lines[0]);
397      return false;
398    }
399    var header:Object = {};
400    var lowerHeader:Object = {};
401    for (var i:int = 1; i < lines.length; ++i) {
402      if (lines[i].length == 0) continue;
403      var m:Array = lines[i].match(/^(\S+):(.*)$/);
404      if (!m) {
405        onConnectionError("failed to parse response header line: " + lines[i]);
406        return false;
407      }
408      var key:String = m[1].toLowerCase();
409      var value:String = StringUtil.trim(m[2]);
410      header[key] = value;
411      lowerHeader[key] = value.toLowerCase();
412    }
413    if (lowerHeader["upgrade"] != "websocket") {
414      onConnectionError("invalid Upgrade: " + header["Upgrade"]);
415      return false;
416    }
417    if (lowerHeader["connection"] != "upgrade") {
418      onConnectionError("invalid Connection: " + header["Connection"]);
419      return false;
420    }
421    if (!lowerHeader["sec-websocket-accept"]) {
422      onConnectionError(
423        "The WebSocket server speaks old WebSocket protocol, " +
424        "which is not supported by web-socket-js. " +
425        "It requires WebSocket protocol HyBi 10. " +
426        "Try newer version of the server if available.");
427      return false;
428    }
429    var replyDigest:String = header["sec-websocket-accept"]
430    if (replyDigest != expectedDigest) {
431      onConnectionError("digest doesn't match: " + replyDigest + " != " + expectedDigest);
432      return false;
433    }
434    if (requestedProtocols.length > 0) {
435      acceptedProtocol = header["sec-websocket-protocol"];
436      if (requestedProtocols.indexOf(acceptedProtocol) < 0) {
437        onConnectionError("protocol doesn't match: '" +
438          acceptedProtocol + "' not in '" + requestedProtocols.join(",") + "'");
439        return false;
440      }
441    }
442    return true;
443  }
444
445  private function sendPong(payload:ByteArray):Boolean {
446    var frame:WebSocketFrame = new WebSocketFrame();
447    frame.opcode = OPCODE_PONG;
448    frame.payload = payload;
449    return sendFrame(frame);
450  }
451  
452  private function sendFrame(frame:WebSocketFrame):Boolean {
453    
454    var plength:uint = frame.payload.length;
455    
456    // Generates a mask.
457    var mask:ByteArray = new ByteArray();
458    for (var i:int = 0; i < 4; i++) {
459      mask.writeByte(randomInt(0, 255));
460    }
461    
462    var header:ByteArray = new ByteArray();
463    // FIN + RSV + opcode
464    header.writeByte((frame.fin ? 0x80 : 0x00) | (frame.rsv << 4) | frame.opcode);
465    if (plength <= 125) {
466      header.writeByte(0x80 | plength);  // Masked + length
467    } else if (plength > 125 && plength < 65536) {
468      header.writeByte(0x80 | 126);  // Masked + 126
469      header.writeShort(plength);
470    } else if (plength >= 65536 && plength < 4294967296) {
471      header.writeByte(0x80 | 127);  // Masked + 127
472      header.writeUnsignedInt(0);  // zero high order bits
473      header.writeUnsignedInt(plength);
474    } else {
475      fatal("Send frame size too large");
476    }
477    header.writeBytes(mask);
478    
479    var maskedPayload:ByteArray = new ByteArray();
480    maskedPayload.length = frame.payload.length;
481    for (i = 0; i < frame.payload.length; i++) {
482      maskedPayload[i] = mask[i % 4] ^ frame.payload[i];
483    }
484
485    try {
486      socket.writeBytes(header);
487      socket.writeBytes(maskedPayload);
488      socket.flush();
489    } catch (ex:Error) {
490      logger.error("Error while sending frame: " + ex.message);
491      setTimeout(function():void {
492        if (readyState != CLOSED) {
493          close(STATUS_CONNECTION_ERROR);
494        }
495      }, 0);
496      return false;
497    }
498    return true;
499    
500  }
501
502  private function parseFrame():WebSocketFrame {
503    
504    var frame:WebSocketFrame = new WebSocketFrame();
505    var hlength:uint = 0;
506    var plength:uint = 0;
507    
508    hlength = 2;
509    if (buffer.length < hlength) {
510      return null;
511    }
512
513    frame.fin = (buffer[0] & 0x80) != 0;
514    frame.rsv = (buffer[0] & 0x70) >> 4;
515    frame.opcode  = buffer[0] & 0x0f;
516    // Payload unmasking is not implemented because masking frames from server
517    // is not allowed. This field is used only for error checking.
518    frame.mask = (buffer[1] & 0x80) != 0;
519    plength = buffer[1] & 0x7f;
520
521    if (plength == 126) {
522      
523      hlength = 4;
524      if (buffer.length < hlength) {
525        return null;
526      }
527      buffer.endian = Endian.BIG_ENDIAN;
528      buffer.position = 2;
529      plength = buffer.readUnsignedShort();
530      
531    } else if (plength == 127) {
532      
533      hlength = 10;
534      if (buffer.length < hlength) {
535        return null;
536      }
537      buffer.endian = Endian.BIG_ENDIAN;
538      buffer.position = 2;
539      // Protocol allows 64-bit length, but we only handle 32-bit
540      var big:uint = buffer.readUnsignedInt(); // Skip high 32-bits
541      plength = buffer.readUnsignedInt(); // Low 32-bits
542      if (big != 0) {
543        fatal("Frame length exceeds 4294967295. Bailing out!");
544        return null;
545      }
546      
547    }
548
549    if (buffer.length < hlength + plength) {
550      return null;
551    }
552    
553    frame.length = hlength + plength;
554    frame.payload = new ByteArray();
555    buffer.position = hlength;
556    buffer.readBytes(frame.payload, 0, plength);
557    return frame;
558    
559  }
560  
561  private function dispatchCloseEvent(wasClean:Boolean, code:int, reason:String):void {
562    var event:WebSocketEvent = new WebSocketEvent("close");
563    event.wasClean = wasClean;
564    event.code = code;
565    event.reason = reason;
566    dispatchEvent(event);
567  }
568  
569  private function removeBufferBefore(pos:int):void {
570    if (pos == 0) return;
571    var nextBuffer:ByteArray = new ByteArray();
572    buffer.position = pos;
573    buffer.readBytes(nextBuffer);
574    buffer = nextBuffer;
575  }
576  
577  private function generateKey():String {
578    var vals:ByteArray = new ByteArray();
579    vals.length = 16;
580    for (var i:int = 0; i < vals.length; ++i) {
581        vals[i] = randomInt(0, 127);
582    }
583    base64Encoder.reset();
584    base64Encoder.encodeBytes(vals);
585    return base64Encoder.toString();
586  }
587  
588  private function readUTFBytes(buffer:ByteArray, start:int, numBytes:int):String {
589    buffer.position = start;
590    var data:String = "";
591    for(var i:int = start; i < start + numBytes; ++i) {
592      // Workaround of a bug of ByteArray#readUTFBytes() that bytes after "\x00" is discarded.
593      if (buffer[i] == 0x00) {
594        data += buffer.readUTFBytes(i - buffer.position) + "\x00";
595        buffer.position = i + 1;
596      }
597    }
598    data += buffer.readUTFBytes(start + numBytes - buffer.position);
599    return data;
600  }
601  
602  private function randomInt(min:uint, max:uint):uint {
603    return min + Math.floor(Math.random() * (Number(max) - min + 1));
604  }
605  
606  private function fatal(message:String):void {
607    logger.error(message);
608    throw message;
609  }
610
611}
612
613}