PageRenderTime 27ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/src/message/connection-endpoint.js

https://gitlab.com/jasonparser/deepstream.io
JavaScript | 387 lines | 185 code | 41 blank | 161 comment | 32 complexity | cc97c425e548583d68910e4e4c2cfc1e MD5 | raw file
  1. var C = require( '../constants/constants' ),
  2. messageParser = require( './message-parser' ),
  3. SocketWrapper = require( './socket-wrapper' ),
  4. engine = require('engine.io'),
  5. TcpEndpoint = require( '../tcp/tcp-endpoint' ),
  6. events = require( 'events' ),
  7. util = require( 'util' ),
  8. https = require('https'),
  9. http = require('http'),
  10. ENGINE_IO = 0,
  11. TCP_ENDPOINT = 1,
  12. READY_STATE_CLOSED = 'closed';
  13. /**
  14. * This is the frontmost class of deepstream's message pipeline. It receives
  15. * connections and authentication requests, authenticates sockets and
  16. * forwards messages it receives from authenticated sockets.
  17. *
  18. * @constructor
  19. *
  20. * @extends events.EventEmitter
  21. *
  22. * @param {Object} options the extended default options
  23. * @param {Function} readyCallback will be invoked once both the engineIo and the tcp connection are established
  24. */
  25. var ConnectionEndpoint = function( options, readyCallback ) {
  26. this._options = options;
  27. this._readyCallback = readyCallback;
  28. // Initialise engine.io's server - a combined http and websocket server for browser connections
  29. this._engineIoReady = false;
  30. this._engineIoServerClosed = false;
  31. this._server = null;
  32. if( this._isHttpsServer() ) {
  33. var httpsOptions = {
  34. key: this._options.sslKey,
  35. cert: this._options.sslCert
  36. };
  37. if (this._options.sslCa) {
  38. httpsOptions.ca = this._options.sslCa;
  39. }
  40. this._server = https.createServer(httpsOptions);
  41. }
  42. else {
  43. this._server = http.createServer();
  44. }
  45. this._server.listen( this._options.port, this._options.host, this._checkReady.bind( this, ENGINE_IO ) );
  46. this._engineIo = engine.attach( this._server );
  47. this._engineIo.on( 'error', this._onError.bind( this ) );
  48. this._engineIo.on( 'connection', this._onConnection.bind( this, ENGINE_IO ) );
  49. // Initialise a tcp server to facilitate fast and compatible communication with backend systems
  50. this._tcpEndpointReady = false;
  51. this._tcpEndpoint = new TcpEndpoint( options, this._checkReady.bind( this, TCP_ENDPOINT ) );
  52. this._tcpEndpoint.on( 'error', this._onError.bind( this ) );
  53. this._tcpEndpoint.on( 'connection', this._onConnection.bind( this, TCP_ENDPOINT ) );
  54. this._timeout = null;
  55. this._msgNum = 0;
  56. this._authenticatedSockets = [];
  57. };
  58. util.inherits( ConnectionEndpoint, events.EventEmitter );
  59. /**
  60. * Called for every message that's received
  61. * from an authenticated socket
  62. *
  63. * This method will be overridden by an external class and is used instead
  64. * of an event emitter to improve the performance of the messaging pipeline
  65. *
  66. * @param {SocketWrapper} socketWrapper
  67. * @param {String} message the raw message as sent by the client
  68. *
  69. * @public
  70. *
  71. * @returns {void}
  72. */
  73. ConnectionEndpoint.prototype.onMessage = function( socketWrapper, message ) {};
  74. /**
  75. * Closes both the engine.io connection and the tcp connection. The ConnectionEndpoint
  76. * will emit a close event once both are succesfully shut down
  77. *
  78. * @public
  79. * @returns {void}
  80. */
  81. ConnectionEndpoint.prototype.close = function() {
  82. this._engineIo.removeAllListeners( 'connection' );
  83. this._tcpEndpoint.removeAllListeners( 'connection' );
  84. // Close the engine.io server
  85. for( var i = 0; i < this._engineIo.clients.length; i++ ) {
  86. if( this._engineIo.clients[ i ].readyState !== READY_STATE_CLOSED ) {
  87. this._engineIo.clients[ i ].once( 'close', this._checkClosed.bind( this ) );
  88. }
  89. }
  90. this._engineIo.close();
  91. this._server.close(function(){ this._engineIoServerClosed = true; }.bind( this ));
  92. // Close the tcp server
  93. this._tcpEndpoint.on( 'close', this._checkClosed.bind( this ) );
  94. this._tcpEndpoint.close();
  95. };
  96. /**
  97. * Called whenever either the tcp server itself or one of its sockets
  98. * is closed. Once everything is closed it will emit a close event
  99. *
  100. * @private
  101. * @returns {void}
  102. */
  103. ConnectionEndpoint.prototype._checkClosed = function() {
  104. if( this._engineIoServerClosed === false ) {
  105. return;
  106. }
  107. if( this._tcpEndpoint.isClosed === false ) {
  108. return;
  109. }
  110. for( var i = 0; i < this._engineIo.clients.length; i++ ) {
  111. if( this._engineIo.clients[ i ].readyState !== READY_STATE_CLOSED ) {
  112. return;
  113. }
  114. }
  115. this.emit( 'close' );
  116. };
  117. /**
  118. * Callback for 'connection' event. Receives
  119. * a connected socket, wraps it in a SocketWrapper and
  120. * subscribes to authentication messages
  121. *
  122. * @param {Number} endpoint
  123. * @param {TCPSocket|Engine.io} socket
  124. *
  125. * @private
  126. * @returns {void}
  127. */
  128. ConnectionEndpoint.prototype._onConnection = function( endpoint, socket ) {
  129. var socketWrapper = new SocketWrapper( socket, this._options ),
  130. handshakeData = socketWrapper.getHandshakeData(),
  131. logMsg;
  132. if( endpoint === ENGINE_IO ) {
  133. logMsg = 'from ' + handshakeData.referer + ' (' + handshakeData.remoteAddress + ')' + ' via engine.io';
  134. } else {
  135. logMsg = 'from ' + handshakeData.remoteAddress + ' via tcp';
  136. }
  137. this._options.logger.log( C.LOG_LEVEL.INFO, C.EVENT.INCOMING_CONNECTION, logMsg );
  138. socketWrapper.authCallBack = this._authenticateConnection.bind( this, socketWrapper );
  139. socket.on( 'message', socketWrapper.authCallBack );
  140. };
  141. /**
  142. * Callback for the first message that's received from the socket.
  143. * This is expected to be an auth-message. This method makes sure that's
  144. * the case and - if so - forwards it to the permission handler for authentication
  145. *
  146. * @param {SocketWrapper} socketWrapper
  147. * @param {String} authMsg
  148. *
  149. * @private
  150. *
  151. * @returns {void}
  152. */
  153. ConnectionEndpoint.prototype._authenticateConnection = function( socketWrapper, authMsg ) {
  154. var msg = messageParser.parse( authMsg )[ 0 ],
  155. logMsg,
  156. authData,
  157. errorMsg;
  158. /**
  159. * Log the authentication attempt
  160. */
  161. logMsg = socketWrapper.getHandshakeData().remoteAddress + ': ' + authMsg;
  162. this._options.logger.log( C.LOG_LEVEL.DEBUG, C.EVENT.AUTH_ATTEMPT, logMsg );
  163. /**
  164. * Ensure the message is a valid authentication message
  165. */
  166. if( !msg || msg.topic !== C.TOPIC.AUTH || msg.action !== C.ACTIONS.REQUEST || msg.data.length !== 1 ) {
  167. errorMsg = this._options.logInvalidAuthData === true ? authMsg : '';
  168. this._sendInvalidAuthMsg( socketWrapper, errorMsg );
  169. return;
  170. }
  171. /**
  172. * Ensure the authentication data is valid JSON
  173. */
  174. try{
  175. authData = JSON.parse( msg.data[ 0 ] );
  176. } catch( e ) {
  177. errorMsg = 'Error parsing auth message';
  178. if( this._options.logInvalidAuthData === true ) {
  179. errorMsg += ' "' + authMsg + '": ' + e.toString();
  180. }
  181. this._sendInvalidAuthMsg( socketWrapper, errorMsg );
  182. return;
  183. }
  184. /**
  185. * Forward for authentication
  186. */
  187. this._options.permissionHandler.isValidUser(
  188. socketWrapper.getHandshakeData(),
  189. authData,
  190. this._processAuthResult.bind( this, authData, socketWrapper )
  191. );
  192. };
  193. /**
  194. * Will be called for syntactically incorrect auth messages. Logs
  195. * the message, sends an error to the client and closes the socket
  196. *
  197. * @param {SocketWrapper} socketWrapper
  198. * @param {String} msg the raw message as sent by the client
  199. *
  200. * @private
  201. *
  202. * @returns {void}
  203. */
  204. ConnectionEndpoint.prototype._sendInvalidAuthMsg = function( socketWrapper, msg ) {
  205. this._options.logger.log( C.LOG_LEVEL.WARN, C.EVENT.INVALID_AUTH_MSG, this._options.logInvalidAuthData ? msg : '' );
  206. socketWrapper.sendError( C.TOPIC.AUTH, C.EVENT.INVALID_AUTH_MSG, 'invalid authentication message' );
  207. socketWrapper.destroy();
  208. };
  209. /**
  210. * Callback for succesfully validated sockets. Removes
  211. * all authentication specific logic and registeres the
  212. * socket with the authenticated sockets
  213. *
  214. * @param {SocketWrapper} socketWrapper
  215. * @param {String} username
  216. *
  217. * @private
  218. *
  219. * @returns {void}
  220. */
  221. ConnectionEndpoint.prototype._registerAuthenticatedSocket = function( socketWrapper, username ) {
  222. socketWrapper.socket.removeListener( 'message', socketWrapper.authCallBack );
  223. socketWrapper.socket.once( 'close', this._onSocketClose.bind( this, socketWrapper ) );
  224. socketWrapper.socket.on( 'message', function( msg ){ this.onMessage( socketWrapper, msg ); }.bind( this ));
  225. socketWrapper.user = username;
  226. socketWrapper.sendMessage( C.TOPIC.AUTH, C.ACTIONS.ACK );
  227. this._authenticatedSockets.push( socketWrapper );
  228. this._options.logger.log( C.LOG_LEVEL.INFO, C.EVENT.AUTH_SUCCESSFUL, username );
  229. };
  230. /**
  231. * Callback for invalid credentials. Will notify the client
  232. * of the invalid auth attempt. If the number of invalid attempts
  233. * exceed the threshold specified in options.maxAuthAttempts
  234. * the client will be notified and the socket destroyed.
  235. *
  236. * @param {Object} authData the (invalid) auth data
  237. * @param {SocketWrapper} socketWrapper
  238. *
  239. * @private
  240. *
  241. * @returns {void}
  242. */
  243. ConnectionEndpoint.prototype._processInvalidAuth = function( authError, authData, socketWrapper ) {
  244. var logMsg = 'invalid authentication data';
  245. if( this._options.logInvalidAuthData === true ) {
  246. logMsg += ': ' + JSON.stringify( authData );
  247. }
  248. this._options.logger.log( C.LOG_LEVEL.INFO, C.EVENT.INVALID_AUTH_DATA, logMsg );
  249. socketWrapper.sendError( C.TOPIC.AUTH, C.EVENT.INVALID_AUTH_DATA, authError || 'invalid authentication data' );
  250. socketWrapper.authAttempts++;
  251. if( socketWrapper.authAttempts >= this._options.maxAuthAttempts ) {
  252. this._options.logger.log( C.LOG_LEVEL.INFO, C.EVENT.TOO_MANY_AUTH_ATTEMPTS, 'too many authentication attempts' );
  253. socketWrapper.sendError( C.TOPIC.AUTH, C.EVENT.TOO_MANY_AUTH_ATTEMPTS, 'too many authentication attempts' );
  254. socketWrapper.destroy();
  255. }
  256. };
  257. /**
  258. * Callback for the results returned by the permissionHandler
  259. *
  260. * @param {Object} authData
  261. * @param {SocketWrapper} socketWrapper
  262. * @param {String} authError String or null if auth succesfull
  263. * @param {String} username
  264. *
  265. * @private
  266. *
  267. * @returns {void}
  268. */
  269. ConnectionEndpoint.prototype._processAuthResult = function( authData, socketWrapper, authError, username ) {
  270. if( authError === null ) {
  271. this._registerAuthenticatedSocket( socketWrapper, username );
  272. } else {
  273. this._processInvalidAuth( authError, authData, socketWrapper );
  274. }
  275. };
  276. /**
  277. * Called for the ready events of both the engine.io server and the tcp server.
  278. *
  279. * @param {String} endpoint An endpoint constant
  280. *
  281. * @private
  282. * @returns {void}
  283. */
  284. ConnectionEndpoint.prototype._checkReady = function( endpoint ) {
  285. var msg;
  286. if( endpoint === ENGINE_IO ) {
  287. msg = 'Listening for browser connections on ' + this._options.host + ':' + this._options.port;
  288. this._engineIoReady = true;
  289. }
  290. if( endpoint === TCP_ENDPOINT ) {
  291. msg = 'Listening for tcp connections on ' + this._options.tcpHost + ':' + this._options.tcpPort;
  292. this._tcpEndpointReady = true;
  293. }
  294. this._options.logger.log( C.LOG_LEVEL.INFO, C.EVENT.INFO, msg );
  295. if( this._tcpEndpointReady === true && this._engineIoReady === true ) {
  296. this._readyCallback();
  297. }
  298. };
  299. /**
  300. * Generic callback for connection errors. This will most often be called
  301. * if the configured port number isn't available
  302. *
  303. * @param {String} error
  304. *
  305. * @private
  306. * @returns {void}
  307. */
  308. ConnectionEndpoint.prototype._onError = function( error ) {
  309. this._options.logger.log( C.LOG_LEVEL.ERROR, C.EVENT.CONNECTION_ERROR, error );
  310. };
  311. /**
  312. * Notifies the (optional) onClientDisconnect method of the permissionHandler
  313. * that the specified client has disconnected
  314. *
  315. * @param {SocketWrapper} socketWrapper
  316. *
  317. * @private
  318. * @returns {void}
  319. */
  320. ConnectionEndpoint.prototype._onSocketClose = function( socketWrapper ) {
  321. if( this._options.permissionHandler.onClientDisconnect ) {
  322. this._options.permissionHandler.onClientDisconnect( socketWrapper.user );
  323. }
  324. };
  325. /**
  326. * Returns whether or not sslKey and sslCert have been set to start a https server.
  327. *
  328. * @throws Will throw an error if only sslKey or sslCert have been specified
  329. *
  330. * @private
  331. * @returns {boolean}
  332. */
  333. ConnectionEndpoint.prototype._isHttpsServer = function( ) {
  334. var isHttps = false;
  335. if( this._options.sslKey || this._options.sslCert ) {
  336. if( !this._options.sslKey ) {
  337. throw new Error( 'Must also include sslKey in order to use HTTPS' );
  338. }
  339. if( !this._options.sslCert ) {
  340. throw new Error( 'Must also include sslCert in order to use HTTPS' );
  341. }
  342. isHttps = true;
  343. }
  344. return isHttps;
  345. };
  346. module.exports = ConnectionEndpoint;