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