/js/lib/Socket.IO-node/support/node-websocket-client/lib/websocket.js

http://github.com/onedayitwillmake/RealtimeMultiplayerNodeJs · JavaScript · 562 lines · 365 code · 108 blank · 89 comment · 59 complexity · 929a4add1a4a95b25259950ac0efef86 MD5 · raw file

  1. var assert = require('assert');
  2. var buffer = require('buffer');
  3. var crypto = require('crypto');
  4. var events = require('events');
  5. var http = require('http');
  6. var net = require('net');
  7. var urllib = require('url');
  8. var sys = require('sys');
  9. var FRAME_NO = 0;
  10. var FRAME_LO = 1;
  11. var FRAME_HI = 2;
  12. // Values for readyState as per the W3C spec
  13. var CONNECTING = 0;
  14. var OPEN = 1;
  15. var CLOSING = 2;
  16. var CLOSED = 3;
  17. var debugLevel = parseInt(process.env.NODE_DEBUG, 16);
  18. var debug = (debugLevel & 0x4) ?
  19. function() { sys.error.apply(this, arguments); } :
  20. function() { };
  21. // Generate a Sec-WebSocket-* value
  22. var createSecretKey = function() {
  23. // How many spaces will we be inserting?
  24. var numSpaces = 1 + Math.floor(Math.random() * 12);
  25. assert.ok(1 <= numSpaces && numSpaces <= 12);
  26. // What is the numerical value of our key?
  27. var keyVal = (Math.floor(
  28. Math.random() * (4294967295 / numSpaces)
  29. ) * numSpaces);
  30. // Our string starts with a string representation of our key
  31. var s = keyVal.toString();
  32. // Insert 'numChars' worth of noise in the character ranges
  33. // [0x21, 0x2f] (14 characters) and [0x3a, 0x7e] (68 characters)
  34. var numChars = 1 + Math.floor(Math.random() * 12);
  35. assert.ok(1 <= numChars && numChars <= 12);
  36. for (var i = 0; i < numChars; i++) {
  37. var pos = Math.floor(Math.random() * s.length + 1);
  38. var c = Math.floor(Math.random() * (14 + 68));
  39. c = (c <= 14) ?
  40. String.fromCharCode(c + 0x21) :
  41. String.fromCharCode((c - 14) + 0x3a);
  42. s = s.substring(0, pos) + c + s.substring(pos, s.length);
  43. }
  44. // We shoudln't have any spaces in our value until we insert them
  45. assert.equal(s.indexOf(' '), -1);
  46. // Insert 'numSpaces' worth of spaces
  47. for (var i = 0; i < numSpaces; i++) {
  48. var pos = Math.floor(Math.random() * (s.length - 1)) + 1;
  49. s = s.substring(0, pos) + ' ' + s.substring(pos, s.length);
  50. }
  51. assert.notEqual(s.charAt(0), ' ');
  52. assert.notEqual(s.charAt(s.length), ' ');
  53. return s;
  54. };
  55. // Generate a challenge sequence
  56. var createChallenge = function() {
  57. var c = '';
  58. for (var i = 0; i < 8; i++) {
  59. c += String.fromCharCode(Math.floor(Math.random() * 255));
  60. }
  61. return c;
  62. };
  63. // Get the value of a secret key string
  64. //
  65. // This strips non-digit values and divides the result by the number of
  66. // spaces found.
  67. var secretKeyValue = function(sk) {
  68. var ns = 0;
  69. var v = 0;
  70. for (var i = 0; i < sk.length; i++) {
  71. var cc = sk.charCodeAt(i);
  72. if (cc == 0x20) {
  73. ns++;
  74. } else if (0x30 <= cc && cc <= 0x39) {
  75. v = v * 10 + cc - 0x30;
  76. }
  77. }
  78. return Math.floor(v / ns);
  79. }
  80. // Get the to-be-hashed value of a secret key string
  81. //
  82. // This takes the result of secretKeyValue() and encodes it in a big-endian
  83. // byte string
  84. var secretKeyHashValue = function(sk) {
  85. var skv = secretKeyValue(sk);
  86. var hv = '';
  87. hv += String.fromCharCode((skv >> 24) & 0xff);
  88. hv += String.fromCharCode((skv >> 16) & 0xff);
  89. hv += String.fromCharCode((skv >> 8) & 0xff);
  90. hv += String.fromCharCode((skv >> 0) & 0xff);
  91. return hv;
  92. };
  93. // Compute the secret key signature based on two secret key strings and some
  94. // handshaking data.
  95. var computeSecretKeySignature = function(s1, s2, hs) {
  96. assert.equal(hs.length, 8);
  97. var hash = crypto.createHash('md5');
  98. hash.update(secretKeyHashValue(s1));
  99. hash.update(secretKeyHashValue(s2));
  100. hash.update(hs);
  101. return hash.digest('binary');
  102. };
  103. // Return a hex representation of the given binary string; used for debugging
  104. var str2hex = function(str) {
  105. var hexChars = [
  106. '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
  107. 'a', 'b', 'c', 'd', 'e', 'f'
  108. ];
  109. var out = '';
  110. for (var i = 0; i < str.length; i++) {
  111. var c = str.charCodeAt(i);
  112. out += hexChars[(c & 0xf0) >>> 4];
  113. out += hexChars[c & 0x0f];
  114. out += ' ';
  115. }
  116. return out.trim();
  117. };
  118. // Get the scheme for a URL, undefined if none is found
  119. var getUrlScheme = function(url) {
  120. var i = url.indexOf(':');
  121. if (i == -1) {
  122. return undefined;
  123. }
  124. return url.substring(0, i);
  125. };
  126. // Set a constant on the given object
  127. var setConstant = function(obj, name, value) {
  128. Object.defineProperty(obj, name, {
  129. get : function() {
  130. return value;
  131. }
  132. });
  133. };
  134. // WebSocket object
  135. //
  136. // This is intended to conform (mostly) to http://dev.w3.org/html5/websockets/
  137. //
  138. // N.B. Arguments are parsed in the anonymous function at the bottom of the
  139. // constructor.
  140. var WebSocket = function(url, proto, opts) {
  141. events.EventEmitter.call(this);
  142. // Retain a reference to our object
  143. var self = this;
  144. // State of the connection
  145. var readyState = CONNECTING;
  146. // Our underlying net.Stream instance
  147. var stream = undefined;
  148. opts = opts || {
  149. origin : 'http://www.example.com'
  150. };
  151. // Frame parsing functions
  152. //
  153. // These read data from the given buffer starting at the given offset look
  154. // for the end of the current frame. If found, the current frame is emitted
  155. // and the function returns. Only a single frame is processed at a time.
  156. //
  157. // The number of bytes of completed frames read is returned, which the
  158. // caller is to use to advance along its buffer. If 0 is returned, no
  159. // completed frame bytes were found, and the caller should probably enqueue
  160. // the buffer as a continuation of the current message. If a complete frame
  161. // is read, the function is responsible fro resting 'frameType'.
  162. // Framing data
  163. var frameType = FRAME_NO;
  164. var bufs = [];
  165. var bufsBytes = 0;
  166. // Frame-parsing functions
  167. var frameFuncs = [
  168. // FRAME_NO
  169. function(buf, off) {
  170. if (buf[off] & 0x80) {
  171. throw new Error('High-byte frames not yet supported');
  172. }
  173. frameType = FRAME_LO;
  174. return 1;
  175. },
  176. // FRAME_LO
  177. function(buf, off) {
  178. assert.ok(bufs.length > 0 || bufsBytes == 0);
  179. debug('frame_lo(' + sys.inspect(buf) + ', ' + off + ')');
  180. // Find the first instance of 0xff, our terminating byte
  181. for (var i = off; i < buf.length && buf[i] != 0xff; i++)
  182. ;
  183. // We didn't find a terminating byte
  184. if (i >= buf.length) {
  185. return 0;
  186. }
  187. // We found a terminating byte; collect all bytes into a single buffer
  188. // and emit it
  189. var mb = null;
  190. if (bufs.length == 0) {
  191. mb = buf.slice(off, i);
  192. } else {
  193. mb = new buffer.Buffer(bufsBytes + i);
  194. var mbOff = 0;
  195. bufs.forEach(function(b) {
  196. b.copy(mb, mbOff, 0, b.length);
  197. mbOff += b.length;
  198. });
  199. assert.equal(mbOff, bufsBytes);
  200. // Don't call Buffer.copy() if we're coping 0 bytes. Rather
  201. // than being a no-op, this will trigger a range violation on
  202. // the destination.
  203. if (i > 0) {
  204. buf.copy(mb, mbOff, off, i);
  205. }
  206. // We consumed all of the buffers that we'd been saving; clear
  207. // things out
  208. bufs = [];
  209. bufsBytes = 0;
  210. }
  211. process.nextTick(function() {
  212. var b = mb;
  213. return function() {
  214. var m = b.toString('utf8');
  215. self.emit('data', b);
  216. self.emit('message', m); // wss compat
  217. if (self.onmessage) {
  218. self.onmessage({data: m});
  219. }
  220. };
  221. }());
  222. frameType = FRAME_NO;
  223. return i - off + 1;
  224. },
  225. // FRAME_HI
  226. function(buf, off) {
  227. debug('High-byte framing not yet supported');
  228. frameType = FRAME_NO;
  229. return buf.length - off;
  230. }
  231. ];
  232. // Handle data coming from our socket
  233. var dataListener = function(buf) {
  234. if (buf.length <= 0) {
  235. return;
  236. }
  237. debug('dataListener(' + sys.inspect(buf) + ')');
  238. var off = 0;
  239. var consumed = 0;
  240. do {
  241. if (frameType < 0 || frameFuncs.length <= frameType) {
  242. throw new Error('Unexpected frame type: ' + frameType);
  243. }
  244. consumed = frameFuncs[frameType](buf, off);
  245. off += consumed;
  246. } while (consumed > 0 && off < buf.length);
  247. if (consumed == 0) {
  248. bufs.push(buf.slice(off, buf.length));
  249. bufsBytes += buf.length - off;
  250. }
  251. };
  252. // Handle incoming file descriptors
  253. var fdListener = function(fd) {
  254. self.emit('fd', fd);
  255. };
  256. // Handle errors from any source (HTTP client, stream, etc)
  257. var errorListener = function(e) {
  258. process.nextTick(function() {
  259. self.emit('wserror', e);
  260. if (self.onerror) {
  261. self.onerror(e);
  262. }
  263. });
  264. };
  265. // External API
  266. self.close = function() {
  267. var f = function() {
  268. readyState = CLOSED;
  269. if (stream) {
  270. stream.end();
  271. stream.destroy();
  272. stream = undefined;
  273. }
  274. process.nextTick(function() {
  275. self.emit('close');
  276. if (self.onclose) {
  277. self.onclose();
  278. }
  279. });
  280. };
  281. switch (readyState) {
  282. case CLOSED:
  283. case CLOSING:
  284. break;
  285. case CONNECTING:
  286. f();
  287. break;
  288. default:
  289. readyState = CLOSING;
  290. // XXX: Run f() on the next tick so that we conform a little
  291. // closer to the spirit of the API in that the caller
  292. // never sees us transition directly to CLOSED. Instead, we
  293. // just seem to have an infinitely fast closing handshake.
  294. if (stream.write('', 'binary')) {
  295. process.nextTick(f);
  296. } else {
  297. stream.addListener('drain', f);
  298. }
  299. }
  300. };
  301. self.send = function(str, fd) {
  302. if (readyState != OPEN) {
  303. throw new Error('Cannot write to non-open WebSocket client');
  304. }
  305. stream.write('\x00', 'binary');
  306. stream.write(str, 'utf8', fd);
  307. stream.write('\xff', 'binary');
  308. };
  309. // wss compat
  310. self.write = self.send;
  311. setConstant(self, 'url', url);
  312. Object.defineProperty(self, 'readyState', {
  313. get : function() {
  314. return readyState;
  315. }
  316. });
  317. // Connect and perform handshaking with the server
  318. (function() {
  319. // Parse constructor arguments
  320. if (!url) {
  321. throw new Error('Url and must be specified.');
  322. }
  323. // Secrets used for handshaking
  324. var key1 = createSecretKey();
  325. var key2 = createSecretKey();
  326. var challenge = createChallenge();
  327. debug(
  328. 'key1=\'' + str2hex(key1) + '\'; ' +
  329. 'key2=\'' + str2hex(key2) + '\'; ' +
  330. 'challenge=\'' + str2hex(challenge) + '\''
  331. );
  332. var httpHeaders = {
  333. 'Connection' : 'Upgrade',
  334. 'Upgrade' : 'WebSocket',
  335. 'Sec-WebSocket-Key1' : key1,
  336. 'Sec-WebSocket-Key2' : key2
  337. };
  338. if (opts.origin) {
  339. httpHeaders['Origin'] = opts.origin;
  340. }
  341. var httpPath = '/';
  342. if (proto) {
  343. httpHeaders['Sec-WebSocket-Protocol'] = proto;
  344. }
  345. // Create the HTTP client that we'll use for handshaking. We'll cannabalize
  346. // its socket via the 'upgrade' event and leave it to rot.
  347. //
  348. // XXX: The ws+unix:// scheme makes use of the implementation detail
  349. // that http.Client passes its constructor arguments through,
  350. // un-inspected to net.Stream.connect(). The latter accepts a
  351. // string as its first argument to connect to a UNIX socket.
  352. var httpClient = undefined;
  353. switch (getUrlScheme(url)) {
  354. case 'ws':
  355. var u = urllib.parse(url);
  356. httpClient = http.createClient(u.port || 80, u.hostname);
  357. httpPath = (u.pathname || '/') + (u.search || '');
  358. httpHeaders.Host = u.hostname;
  359. break;
  360. case 'ws+unix':
  361. var sockPath = url.substring('ws+unix://'.length, url.length);
  362. httpClient = http.createClient(sockPath);
  363. httpHeaders.Host = 'localhost';
  364. break;
  365. default:
  366. throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.');
  367. }
  368. httpClient.addListener('upgrade', (function() {
  369. var data = undefined;
  370. return function(req, s, head) {
  371. stream = s;
  372. stream.addListener('data', function(d) {
  373. if (!data) {
  374. data = d;
  375. } else {
  376. var data2 = new buffer.Buffer(data.length + d.length);
  377. if (data.length) {
  378. data.copy(data2, 0, 0, data.length);
  379. }
  380. if (data2.length) {
  381. d.copy(data2, data.length, 0, d.length);
  382. }
  383. data = data2;
  384. }
  385. if (data.length >= 16) {
  386. var expected = computeSecretKeySignature(key1, key2, challenge);
  387. var actual = data.slice(0, 16).toString('binary');
  388. // Handshaking fails; we're donezo
  389. if (actual != expected) {
  390. debug(
  391. 'expected=\'' + str2hex(expected) + '\'; ' +
  392. 'actual=\'' + str2hex(actual) + '\''
  393. );
  394. process.nextTick(function() {
  395. // XXX: Emit 'wserror' here, as 'error' is a reserved word in the
  396. // EventEmitter world, and gets thrown.
  397. self.emit(
  398. 'wserror',
  399. new Error('Invalid handshake from server:' +
  400. 'expected \'' + str2hex(expected) + '\', ' +
  401. 'actual \'' + str2hex(actual) + '\''
  402. )
  403. );
  404. if (self.onerror) {
  405. self.onerror();
  406. }
  407. self.close();
  408. });
  409. }
  410. // Un-register our data handler and add the one to be used
  411. // for the normal, non-handshaking case. If we have extra
  412. // data left over, manually fire off the handler on
  413. // whatever remains.
  414. //
  415. // XXX: This is lame. We should only remove the listeners
  416. // that we added.
  417. httpClient.removeAllListeners('upgrade');
  418. stream.removeAllListeners('data');
  419. stream.addListener('data', dataListener);
  420. // Fire the 'open' event
  421. process.nextTick(function() {
  422. self.emit('open');
  423. if (self.onopen) {
  424. self.onopen();
  425. }
  426. });
  427. readyState = OPEN;
  428. if (data.length > 16) {
  429. stream.emit('data', data.slice(16, data.length));
  430. }
  431. }
  432. });
  433. stream.addListener('fd', fdListener);
  434. stream.addListener('error', errorListener);
  435. stream.emit('data', head);
  436. };
  437. })());
  438. httpClient.addListener('error', function(e) {
  439. httpClient.end();
  440. errorListener(e);
  441. });
  442. var httpReq = httpClient.request(httpPath, httpHeaders);
  443. httpReq.write(challenge, 'binary');
  444. httpReq.end();
  445. })();
  446. };
  447. sys.inherits(WebSocket, events.EventEmitter);
  448. exports.WebSocket = WebSocket;
  449. // Add some constants to the WebSocket object
  450. setConstant(WebSocket.prototype, 'CONNECTING', CONNECTING);
  451. setConstant(WebSocket.prototype, 'OPEN', OPEN);
  452. setConstant(WebSocket.prototype, 'CLOSING', CLOSING);
  453. setConstant(WebSocket.prototype, 'CLOSED', CLOSED);
  454. // vim:ts=4 sw=4 et