/server/socket.js

https://github.com/boyers/socket.js · JavaScript · 283 lines · 208 code · 33 blank · 42 comment · 54 complexity · 87b2b372569ebaf75b437fe72c70ff6a MD5 · raw file

  1. 'use strict';
  2. var crypto = require('crypto');
  3. // messages are converted to JSON before being sent down the wire
  4. // this function is used to validate that an object can be converted to JSON
  5. function jsonConvertible(x) {
  6. try {
  7. if (typeof JSON.stringify(x) !== 'string') {
  8. return false;
  9. }
  10. } catch (e) {
  11. return false;
  12. }
  13. return true;
  14. }
  15. // this function registers a callback to receive the connection
  16. module.exports = function(httpServer, handler) {
  17. // this event is fired whenever the client attempts to initiate a connection upgrade
  18. httpServer.on('upgrade', function(req, socket, head) {
  19. // make sure the upgrade is for the WebSockets protocol
  20. if (req.headers['upgrade'].toLowerCase() === 'websocket') {
  21. // we only support version 13 of the protocol, which
  22. // is the latest at the time of this writing
  23. var version = req.headers['sec-websocket-version'];
  24. if (version === '13') {
  25. // we have to send back this magic to the client to finish the handshake
  26. var key = req.headers['sec-websocket-key'];
  27. socket.write('HTTP/1.1 101 Switching Protocols\r\n' +
  28. 'Upgrade: websocket\r\n' +
  29. 'Connection: Upgrade\r\n' +
  30. 'Sec-WebSocket-Accept: ' + crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64') + '\r\n' +
  31. '\r\n');
  32. var started = false;
  33. var closed = false;
  34. var closeHandler = null;
  35. var messageHandlers = {};
  36. var dataReceived = new Buffer(0);
  37. var payloadReceived = new Buffer(0);
  38. // send a message to the client
  39. var sendMessage = function(message) {
  40. if (!closed) {
  41. // convert to JSON for sending down the wire
  42. var data = JSON.stringify(message);
  43. // FIN, RSV1-3, and opcode
  44. socket.write(new Buffer([129]));
  45. // funky variable-width encoding of the payload length
  46. var payloadLengthBuffer;
  47. if (data.length < 126) {
  48. payloadLengthBuffer = new Buffer([data.length]);
  49. } else if (data.length < 65536) {
  50. payloadLengthBuffer = new Buffer(3);
  51. payloadLengthBuffer.writeUInt8(126, 0);
  52. payloadLengthBuffer.writeUInt16BE(data.length, 1);
  53. } else {
  54. payloadLengthBuffer = new Buffer(9);
  55. payloadLengthBuffer.writeUInt8(127, 0);
  56. payloadLengthBuffer.writeUInt16BE(0, 1);
  57. payloadLengthBuffer.writeUInt16BE(data.length, 5);
  58. }
  59. socket.write(payloadLengthBuffer);
  60. // send the actual payload
  61. socket.write(data);
  62. }
  63. };
  64. // call this when the connection is closed
  65. // or when we want to close the connection
  66. var close = function(needToCloseSocket) {
  67. if (!closed) {
  68. // ask the client to close the socket if necessary
  69. if (needToCloseSocket) {
  70. sendMessage({
  71. type: 'close'
  72. });
  73. }
  74. // mark the connection as closed and clean up
  75. closed = true;
  76. dataReceived = new Buffer(0);
  77. // notify the application
  78. if (closeHandler !== null) {
  79. closeHandler();
  80. }
  81. }
  82. };
  83. socket.on('data', function(data) {
  84. if (closed) {
  85. return;
  86. }
  87. // collect all the unprocessed data received so far
  88. dataReceived = Buffer.concat([dataReceived, data], dataReceived.length + data.length);
  89. // eat as much data as possible, one frame at a time
  90. while (true) {
  91. // read the FIN bit
  92. var nextByteIndex = 0;
  93. if (dataReceived.length < nextByteIndex + 1) {
  94. return;
  95. }
  96. var fin = (dataReceived.readUInt8(0) >> 7) === 1;
  97. // read the opcode
  98. var opcode = dataReceived.readUInt8(0) & 15;
  99. if (opcode === 8) {
  100. // client closed the connection
  101. close(true);
  102. return;
  103. }
  104. nextByteIndex += 1;
  105. // read the mask bit
  106. if (dataReceived.length < nextByteIndex + 1) {
  107. return;
  108. }
  109. var mask = (dataReceived.readUInt8(1) >> 7) === 1;
  110. // read the payload length (it's a variable-width encoding)
  111. var payloadLength;
  112. if ((dataReceived.readUInt8(1) & 127) < 126) {
  113. payloadLength = dataReceived.readUInt8(1) & 127;
  114. nextByteIndex += 1;
  115. } else if ((dataReceived.readUInt8(1) & 127) === 126) {
  116. if (dataReceived.length < nextByteIndex + 3) {
  117. return;
  118. }
  119. payloadLength = dataReceived.readUInt16BE(2);
  120. nextByteIndex += 3;
  121. } else {
  122. if (dataReceived.length < nextByteIndex + 9) {
  123. return;
  124. }
  125. payloadLength = (dataReceived.readUInt32BE(2) << 32) + dataReceived.readUInt32BE(6);
  126. nextByteIndex += 9;
  127. }
  128. // read the masking key for decrypting the message, if there is one
  129. var maskingKey;
  130. if (mask) {
  131. if (dataReceived.length < nextByteIndex + 4) {
  132. return;
  133. }
  134. maskingKey = dataReceived.slice(nextByteIndex, nextByteIndex + 4);
  135. nextByteIndex += 4;
  136. }
  137. // check if we got the whole frame yet
  138. if (dataReceived.length < nextByteIndex + payloadLength) {
  139. return;
  140. }
  141. // decrypt the message if necessary
  142. if (mask) {
  143. for (var i = 0; i < payloadLength; i += 1) {
  144. dataReceived.writeUInt8(dataReceived.readUInt8(nextByteIndex + i) ^ maskingKey.readUInt8(i % 4), nextByteIndex + i);
  145. }
  146. }
  147. // read the payload
  148. payloadReceived = Buffer.concat([payloadReceived, dataReceived.slice(nextByteIndex, nextByteIndex + payloadLength)], payloadReceived.length + payloadLength);
  149. nextByteIndex += payloadLength;
  150. // if the message fits in one frame, we got it all
  151. if (fin) {
  152. if (opcode === 1) {
  153. var messageData;
  154. try {
  155. // try to parse the message
  156. messageData = JSON.parse(payloadReceived.toString());
  157. } catch (e) {
  158. close(true);
  159. return;
  160. }
  161. if (messageData.type === 'connect') {
  162. // the client is connecting for the first time
  163. if (!started) {
  164. started = true;
  165. start(null);
  166. }
  167. } else if (messageData.type === 'reconnect') {
  168. // the client is reconnecting
  169. if (!started) {
  170. started = true;
  171. start(messageData.reconnectData);
  172. }
  173. } else if (messageData.type === 'message') {
  174. // send the message to the application
  175. if (messageHandlers[messageData.messageType] !== undefined) {
  176. messageHandlers[messageData.messageType](messageData.message);
  177. }
  178. }
  179. }
  180. payloadReceived = new Buffer(0);
  181. }
  182. // free the data for this frame
  183. if (dataReceived.length === nextByteIndex) {
  184. dataReceived = new Buffer(0);
  185. } else {
  186. dataReceived = dataReceived.slice(nextByteIndex);
  187. }
  188. }
  189. });
  190. // when the socket is closed, we're done here
  191. socket.on('close', function(data) {
  192. close(false);
  193. });
  194. // this is called once the client tells us that
  195. // a) this is a new connection, or
  196. // b) we are reconnecting
  197. var start = function(reconnectData) {
  198. handler({
  199. // send a message to the client
  200. send: function(type, message) {
  201. if (typeof type !== 'string') {
  202. throw 'Invalid parameter: type';
  203. }
  204. if (!jsonConvertible(message)) {
  205. throw 'Invalid parameter: message';
  206. }
  207. if (closed) {
  208. throw 'Attempted to transmit after the connection has been closed';
  209. }
  210. sendMessage({
  211. type: 'message',
  212. messageType: type,
  213. message: message
  214. });
  215. },
  216. // register a callback to receive messages from the client
  217. receive: function(type, handler) {
  218. if (typeof type !== 'string') {
  219. throw 'Invalid parameter: type';
  220. }
  221. if (handler !== null && typeof handler !== 'function') {
  222. throw 'Invalid parameter: handler';
  223. }
  224. if (handler === null) {
  225. delete messageHandlers[type];
  226. } else {
  227. messageHandlers[type] = handler;
  228. }
  229. },
  230. // close the connection or register a callback to be notified when the connection is closed
  231. close: function(handler) {
  232. if (handler !== undefined && handler !== null && typeof handler !== 'function') {
  233. throw 'Invalid parameter: handler';
  234. }
  235. if (handler === undefined) {
  236. close(true);
  237. } else {
  238. closeHandler = handler;
  239. }
  240. }
  241. }, reconnectData);
  242. };
  243. } else {
  244. socket.end();
  245. }
  246. } else {
  247. socket.end();
  248. }
  249. });
  250. };