From 91680015b934fe1a5aa3e2704182542aad135c0a Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Mon, 1 Feb 2016 17:49:38 -0600 Subject: [PATCH 1/6] Added "destroy" method to HTTP, used in .lower() for clean shutdown. --- lib/app/lower.js | 78 ++++++++++++++++++++++++++---------- lib/hooks/http/initialize.js | 19 +++++++++ 2 files changed, 75 insertions(+), 22 deletions(-) diff --git a/lib/app/lower.js b/lib/app/lower.js index fff9bf5cdb..98b06cb503 100644 --- a/lib/app/lower.js +++ b/lib/app/lower.js @@ -17,15 +17,25 @@ var _ = require('lodash'); * @api public */ -module.exports = function lower(cb) { +module.exports = function lower(options, cb) { var sails = this; sails.log.verbose('Lowering sails...'); + + // Options are optional + if ('function' == typeof options) { + cb = options; + options = null; + } + // Callback is optional cb = cb || function(err) { if (err) return sails.log.error(err); }; + options = options || {}; + options.delay = options.delay || 100; + // Flag `sails._exiting` as soon as the app has begun to shutdown. // This may be used by hooks and other parts of core. // (e.g. to stop handling HTTP requests and prevent ugly error msgs) @@ -62,24 +72,37 @@ module.exports = function lower(cb) { async.series([ function shutdownSockets(cb) { - if (!_.isObject(sails.hooks) || !sails.hooks.sockets) { + + // If the sockets hook is disabled, skip this. + // Also skip if the socket server is piggybacking on the main HTTP server, to avoid + // the onClose event possibly being called multiple times (because you can't tell + // socket.io to close without it trying to close the http server). If we're piggybacking + // we'll call sails.io.close in the main "shutdownHTTP" code below. + if (!_.isObject(sails.hooks) || !sails.hooks.sockets || (sails.io.httpServer && sails.hooks.http.server === sails.io.httpServer)) { return cb(); } try { sails.log.verbose('Shutting down socket server...'); - var timeOut = setTimeout(cb, 100); - sails.io.server.unref(); - sails.io.server.close(); - sails.io.server.on('close', function() { - sails.log.verbose('Socket server shut down successfully.'); - clearTimeout(timeOut); - cb(); - }); + timeOut = setTimeout(function() { + sails.io.httpServer.removeListener('close', onClose); + return cb(); + }, 100); + sails.io.httpServer.unref(); + sails.io.httpServer.once('close', onClose); + sails.io.close(); } catch (e) { + sails.log.verbose("Error occurred closing socket server: ", e); clearTimeout(timeOut); cb(); } + + function onClose() { + sails.log.verbose('Socket server shut down successfully.'); + clearTimeout(timeOut); + cb(); + } + }, function shutdownHTTP(cb) { @@ -92,25 +115,36 @@ module.exports = function lower(cb) { try { sails.log.verbose('Shutting down HTTP server...'); - // Give the server 100ms to close all existing connections - // and emit the "close" event. After that, unbind our - // "close" listener and continue (this prevents the cb - // from being called twice). - timeOut = setTimeout(function() { - sails.hooks.http.server.removeListener('close', onClose); - return cb(); - }, 100); - // Allow process to exit once this server is closed sails.hooks.http.server.unref(); - // Stop the server from accepting new connections - sails.hooks.http.server.close(); + // If we have a socket server and it's piggybacking on the main HTTP server, tell + // socket.io to close now. This may call `.close()` on the HTTP server, which will + // happen again below, but the second synchronous call to .close() will have no + // additional effect. Leaving this as-is in case future versions of socket.io + // DON'T automatically close the http server for you. + if (sails.io && sails.io.httpServer && sails.hooks.http.server === sails.io.httpServer) { + sails.io.close(); + } + + // If the "hard shutdown" option is on, destroy the server immediately, + // severing all connections + if (options.hardShutdown) { + sails.hooks.http.destroy(); + } + // Otherwise just stop the server from accepting new connections, + // and wait options.delay for the existing connections to close + // gracefully before destroying. + else { + timeOut = setTimeout(sails.hooks.http.destroy, options.delay); + sails.hooks.http.server.close(); + } // Wait for the existing connections to close - sails.hooks.http.server.on('close', onClose); + sails.hooks.http.server.once('close', onClose); } catch (e) { + sails.log.verbose("Error occurred closing HTTP server: ", e); clearTimeout(timeOut); cb(); } diff --git a/lib/hooks/http/initialize.js b/lib/hooks/http/initialize.js index 5a94c6f32a..4340c4195f 100644 --- a/lib/hooks/http/initialize.js +++ b/lib/hooks/http/initialize.js @@ -48,6 +48,25 @@ module.exports = function(sails) { sails.hooks.http.server = createServer(serverOptions, sails.hooks.http.app); } else sails.hooks.http.server = createServer(sails.hooks.http.app); + // Keep track of all connections that come in, so we can destroy + // them later if we want to. + var connections = {}; + + sails.hooks.http.server.on('connection', function(conn) { + var key = conn.remoteAddress + ':' + conn.remotePort; + connections[key] = conn; + conn.on('close', function() { + delete connections[key]; + }); + }); + + // Create a `destroy` method we can use to do a hard shutdown of the server. + sails.hooks.http.destroy = function(cb) { + sails.log.verbose("Destroying http server..."); + sails.hooks.http.server.close(cb); + for (var key in connections) + connections[key].destroy(); + }; // Configure views if hook enabled if (sails.hooks.views) { From c6385730260f4dcf41a2a7ede25121178a4184fd Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Mon, 1 Feb 2016 23:33:36 -0600 Subject: [PATCH 2/6] Update version of sails.io.js used in tests --- test/helpers/sails.io.js | 199 ++++++++++++++++++++------- test/integration/helpers/sails.io.js | 190 +++++++++++++++++++------ 2 files changed, 294 insertions(+), 95 deletions(-) diff --git a/test/helpers/sails.io.js b/test/helpers/sails.io.js index 90ae1714c3..f0ea6c70b1 100644 --- a/test/helpers/sails.io.js +++ b/test/helpers/sails.io.js @@ -1,12 +1,3 @@ -///////////////////////////////////////////////////////////////////// -// -// This copy of the v0.11.x sails.io.js client is a dumb copy/paste. -// It's here to avoid issues with requiring the wrong sails.io.js -// from NPM. -// -///////////////////////////////////////////////////////////////////// - - /** * sails.io.js * ------------------------------------------------------------------------ @@ -61,7 +52,7 @@ // Current version of this SDK (sailsDK?!?!) and other metadata // that will be sent along w/ the initial connection request. var SDK_INFO = { - version: '0.11.0', // TODO: pull this automatically from package.json during build. + version: '0.13.4', // <-- pulled automatically from package.json, do not change! platform: typeof module === 'undefined' ? 'browser' : 'node', language: 'javascript' }; @@ -177,13 +168,15 @@ // (e.g. Ember does this. See https://github.com/balderdashy/sails.io.js/pull/5) var isSafeToDereference = ({}).hasOwnProperty.call(queue, i); if (isSafeToDereference) { - // Emit the request. - _emitFrom(socket, queue[i]); + // Get the arguments that were originally made to the "request" method + var requestArgs = queue[i]; + // Call the request method again in the context of the socket, with the original args + socket.request.apply(socket, requestArgs); } } // Now empty the queue to remove it as a source of additional complexity. - queue = null; + socket.requestQueue = null; } @@ -305,6 +298,22 @@ // ``` + var SOCKET_OPTIONS = [ + 'useCORSRouteToGetCookie', + 'url', + 'multiplex', + 'transports', + 'query', + 'path', + 'headers', + 'initialConnectionHeaders', + 'reconnection', + 'reconnectionAttempts', + 'reconnectionDelay', + 'reconnectionDelayMax', + 'randomizationFactor', + 'timeout' + ]; /** * SailsSocket @@ -324,12 +333,39 @@ var self = this; opts = opts||{}; - // Absorb opts - self.useCORSRouteToGetCookie = opts.useCORSRouteToGetCookie; - self.url = opts.url; - self.multiplex = opts.multiplex; - self.transports = opts.transports; - self.query = opts.query; + // Set up connection options so that they can only be changed when socket is disconnected. + var _opts = {}; + SOCKET_OPTIONS.forEach(function(option) { + // Okay to change global headers while socket is connected + if (option == 'headers') {return;} + Object.defineProperty(self, option, { + get: function() { + if (option == 'url') { + return _opts[option] || (self._raw && self._raw.io && self._raw.io.uri); + } + return _opts[option]; + }, + set: function(value) { + // Don't allow value to be changed while socket is connected + if (self.isConnected() && io.sails.strict !== false && value != _opts[option]) { + throw new Error('Cannot change value of `' + option + '` while socket is connected.'); + } + // If socket is attempting to reconnect, stop it. + if (self._raw && self._raw.io && self._raw.io.reconnecting && !self._raw.io.skipReconnect) { + self._raw.io.skipReconnect = true; + consolog("Stopping reconnect; use .reconnect() to connect socket after changing options."); + } + _opts[option] = value; + } + }); + }); + + // Absorb opts into SailsSocket instance + // See https://sailsjs.org/reference/websockets/sails.io.js/SailsSocket/properties.md + // for description of options + SOCKET_OPTIONS.forEach(function(option) { + self[option] = opts[option]; + }); // Set up "eventQueue" to hold event handlers which have not been set on the actual raw socket yet. self.eventQueue = {}; @@ -359,12 +395,24 @@ SailsSocket.prototype._connect = function (){ var self = this; + self.isConnecting = true; + // Apply `io.sails` config as defaults // (now that at least one tick has elapsed) - self.useCORSRouteToGetCookie = self.useCORSRouteToGetCookie||io.sails.useCORSRouteToGetCookie; - self.url = self.url||io.sails.url; - self.transports = self.transports || io.sails.transports; - self.query = self.query || io.sails.query; + // See https://sailsjs.org/reference/websockets/sails.io.js/SailsSocket/properties.md + // for description of options and default values + SOCKET_OPTIONS.forEach(function(option) { + if ('undefined' == typeof self[option]) { + self[option] = io.sails[option]; + } + }); + + // Headers that will be sent with the initial request to /socket.io (Node.js only) + self.extraHeaders = self.initialConnectionHeaders || {}; + + if (!(typeof module === 'object' && typeof module.exports !== 'undefined') && self.initialConnectionHeaders) { + console.warn("initialConnectionHeaders option available in Node.js only!"); + } // Ensure URL has no trailing slash self.url = self.url ? self.url.replace(/(\/)$/, '') : undefined; @@ -475,6 +523,7 @@ var mikealsReq = require('request'); mikealsReq.get(xOriginCookieURL, function(err, httpResponse, body) { if (err) { + self.isConnecting = false; consolog( 'Failed to connect socket (failed to get cookie)', 'Error:', err @@ -499,7 +548,7 @@ * successfully. */ self.on('connect', function socketConnected() { - + self.isConnecting = false; consolog.noPrefix( '\n' + '\n' + @@ -508,7 +557,7 @@ // '\n'+ ' |> Now connected to Sails.' + '\n' + '\\___/ For help, see: http://bit.ly/1DmTvgK' + '\n' + - ' (using '+io.sails.sdk.platform+' SDK @v'+io.sails.sdk.version+')'+ '\n' + + ' (using sails.io.js '+io.sails.sdk.platform+' SDK @v'+io.sails.sdk.version+')'+ '\n' + '\n'+ '\n'+ // '\n'+ @@ -539,6 +588,9 @@ }); self.on('reconnect', function(transport, numAttempts) { + if (!self.isConnecting) { + self.on('connect', runRequestQueue.bind(self, self)); + } var msSinceConnectionLost = ((new Date()).getTime() - self.connectionLostTimestamp); var numSecsOffline = (msSinceConnectionLost / 1000); consolog( @@ -553,7 +605,7 @@ // (usually because of a failed authorization, which is in turn // usually due to a missing or invalid cookie) self.on('error', function failedToConnect(err) { - + self.isConnecting = false; // TODO: // handle failed connections due to failed authorization // in a smarter way (probably can listen for a different event) @@ -578,6 +630,20 @@ }; + /** + * Reconnect the underlying socket. + * + * @api public + */ + SailsSocket.prototype.reconnect = function (){ + if (this.isConnecting) { + throw new Error('Cannot connect- socket is already connecting'); + } + if (this.isConnected()) { + throw new Error('Cannot connect- socket is already connected'); + } + return this._connect(); + }; /** * Disconnect the underlying socket. @@ -585,7 +651,8 @@ * @api public */ SailsSocket.prototype.disconnect = function (){ - if (!this._raw) { + this.isConnecting = false; + if (!this.isConnected()) { throw new Error('Cannot disconnect- socket is already disconnected'); } return this._raw.disconnect(); @@ -629,12 +696,7 @@ // Bind a one-time function to run the request queue // when the self._raw connects. if ( !self.isConnected() ) { - var alreadyRanRequestQueue = false; - self._raw.on('connect', function whenRawSocketConnects() { - if (alreadyRanRequestQueue) return; - runRequestQueue(self); - alreadyRanRequestQueue = true; - }); + self._raw.once('connect', runRequestQueue.bind(self, self)); } // Or run it immediately if self._raw is already connected else { @@ -721,7 +783,7 @@ * * @api public * @param {String} url :: destination URL - * @param {Object} params :: parameters to send with the request [optional] + * @param {Object} data :: parameters to send with the request [optional] * @param {Function} cb :: callback function to call when finished [optional] */ @@ -749,7 +811,7 @@ * * @api public * @param {String} url :: destination URL - * @param {Object} params :: parameters to send with the request [optional] + * @param {Object} data :: parameters to send with the request [optional] * @param {Function} cb :: callback function to call when finished [optional] */ @@ -777,7 +839,7 @@ * * @api public * @param {String} url :: destination URL - * @param {Object} params :: parameters to send with the request [optional] + * @param {Object} data :: parameters to send with the request [optional] * @param {Function} cb :: callback function to call when finished [optional] */ @@ -805,7 +867,7 @@ * * @api public * @param {String} url :: destination URL - * @param {Object} params :: parameters to send with the request [optional] + * @param {Object} data :: parameters to send with the request [optional] * @param {Function} cb :: callback function to call when finished [optional] */ @@ -870,15 +932,44 @@ throw new Error('Invalid `method` provided (should be a string like "post" or "put")\n' + usage); } if (options.headers && typeof options.headers !== 'object') { - throw new Error('Invalid `headers` provided (should be an object with string values)\n' + usage); + throw new Error('Invalid `headers` provided (should be a dictionary with string values)\n' + usage); } if (options.params && typeof options.params !== 'object') { - throw new Error('Invalid `params` provided (should be an object with string values)\n' + usage); + throw new Error('Invalid `params` provided (should be a dictionary with JSON-serializable values)\n' + usage); + } + if (options.data && typeof options.data !== 'object') { + throw new Error('Invalid `data` provided (should be a dictionary with JSON-serializable values)\n' + usage); } if (cb && typeof cb !== 'function') { throw new Error('Invalid callback function!\n' + usage); } + // Accept either `params` or `data` for backwards compatibility (but not both!) + if (options.data && options.params) { + throw new Error('Cannot specify both `params` and `data`! They are aliases of each other.\n' + usage); + } + else if (options.data) { + options.params = options.data; + delete options.data; + } + + + // If this socket is not connected yet, queue up this request + // instead of sending it. + // (so it can be replayed when the socket comes online.) + if ( ! this.isConnected() ) { + + // If no queue array exists for this socket yet, create it. + this.requestQueue = this.requestQueue || []; + this.requestQueue.push([options, cb]); + return; + } + + // Otherwise, our socket is connected, so continue prepping + // the request. + + // Default headers to an empty object + options.headers = options.headers || {}; // Build a simulated request object // (and sanitize/marshal options along the way) @@ -886,7 +977,7 @@ method: options.method.toLowerCase() || 'get', - headers: options.headers || {}, + headers: options.headers, data: options.params || options.data || {}, @@ -896,19 +987,15 @@ cb: cb }; - // If this socket is not connected yet, queue up this request - // instead of sending it. - // (so it can be replayed when the socket comes online.) - if ( ! this.isConnected() ) { - - // If no queue array exists for this socket yet, create it. - this.requestQueue = this.requestQueue || []; - this.requestQueue.push(requestCtx); - return; + // Merge global headers in + if (this.headers && 'object' == typeof this.headers) { + for (var header in this.headers) { + if (!options.headers.hasOwnProperty(header)) { + options.headers[header] = this.headers[header]; + } + } } - - // Otherwise, our socket is ok! // Send the request. _emitFrom(this, requestCtx); }; @@ -969,6 +1056,14 @@ * @return {Socket} */ io.sails.connect = function(url, opts) { + + // Make URL optional + if ('object' == typeof url) { + opts = url; + url = null; + } + + // Default opts to empty object opts = opts || {}; // If explicit connection url is specified, save it to options @@ -1006,7 +1101,7 @@ setTimeout(function() { // If autoConnect is disabled, delete the eager socket (io.socket) and bail out. - if (!io.sails.autoConnect) { + if (io.sails.autoConnect === false || io.sails.autoconnect === false) { delete io.socket; return; } diff --git a/test/integration/helpers/sails.io.js b/test/integration/helpers/sails.io.js index 0557d7f1b3..f0ea6c70b1 100644 --- a/test/integration/helpers/sails.io.js +++ b/test/integration/helpers/sails.io.js @@ -52,7 +52,7 @@ // Current version of this SDK (sailsDK?!?!) and other metadata // that will be sent along w/ the initial connection request. var SDK_INFO = { - version: '0.11.0', // TODO: pull this automatically from package.json during build. + version: '0.13.4', // <-- pulled automatically from package.json, do not change! platform: typeof module === 'undefined' ? 'browser' : 'node', language: 'javascript' }; @@ -168,13 +168,15 @@ // (e.g. Ember does this. See https://github.com/balderdashy/sails.io.js/pull/5) var isSafeToDereference = ({}).hasOwnProperty.call(queue, i); if (isSafeToDereference) { - // Emit the request. - _emitFrom(socket, queue[i]); + // Get the arguments that were originally made to the "request" method + var requestArgs = queue[i]; + // Call the request method again in the context of the socket, with the original args + socket.request.apply(socket, requestArgs); } } // Now empty the queue to remove it as a source of additional complexity. - queue = null; + socket.requestQueue = null; } @@ -296,6 +298,22 @@ // ``` + var SOCKET_OPTIONS = [ + 'useCORSRouteToGetCookie', + 'url', + 'multiplex', + 'transports', + 'query', + 'path', + 'headers', + 'initialConnectionHeaders', + 'reconnection', + 'reconnectionAttempts', + 'reconnectionDelay', + 'reconnectionDelayMax', + 'randomizationFactor', + 'timeout' + ]; /** * SailsSocket @@ -315,12 +333,39 @@ var self = this; opts = opts||{}; - // Absorb opts - self.useCORSRouteToGetCookie = opts.useCORSRouteToGetCookie; - self.url = opts.url; - self.multiplex = opts.multiplex; - self.transports = opts.transports; - self.query = opts.query; + // Set up connection options so that they can only be changed when socket is disconnected. + var _opts = {}; + SOCKET_OPTIONS.forEach(function(option) { + // Okay to change global headers while socket is connected + if (option == 'headers') {return;} + Object.defineProperty(self, option, { + get: function() { + if (option == 'url') { + return _opts[option] || (self._raw && self._raw.io && self._raw.io.uri); + } + return _opts[option]; + }, + set: function(value) { + // Don't allow value to be changed while socket is connected + if (self.isConnected() && io.sails.strict !== false && value != _opts[option]) { + throw new Error('Cannot change value of `' + option + '` while socket is connected.'); + } + // If socket is attempting to reconnect, stop it. + if (self._raw && self._raw.io && self._raw.io.reconnecting && !self._raw.io.skipReconnect) { + self._raw.io.skipReconnect = true; + consolog("Stopping reconnect; use .reconnect() to connect socket after changing options."); + } + _opts[option] = value; + } + }); + }); + + // Absorb opts into SailsSocket instance + // See https://sailsjs.org/reference/websockets/sails.io.js/SailsSocket/properties.md + // for description of options + SOCKET_OPTIONS.forEach(function(option) { + self[option] = opts[option]; + }); // Set up "eventQueue" to hold event handlers which have not been set on the actual raw socket yet. self.eventQueue = {}; @@ -350,12 +395,24 @@ SailsSocket.prototype._connect = function (){ var self = this; + self.isConnecting = true; + // Apply `io.sails` config as defaults // (now that at least one tick has elapsed) - self.useCORSRouteToGetCookie = self.useCORSRouteToGetCookie||io.sails.useCORSRouteToGetCookie; - self.url = self.url||io.sails.url; - self.transports = self.transports || io.sails.transports; - self.query = self.query || io.sails.query; + // See https://sailsjs.org/reference/websockets/sails.io.js/SailsSocket/properties.md + // for description of options and default values + SOCKET_OPTIONS.forEach(function(option) { + if ('undefined' == typeof self[option]) { + self[option] = io.sails[option]; + } + }); + + // Headers that will be sent with the initial request to /socket.io (Node.js only) + self.extraHeaders = self.initialConnectionHeaders || {}; + + if (!(typeof module === 'object' && typeof module.exports !== 'undefined') && self.initialConnectionHeaders) { + console.warn("initialConnectionHeaders option available in Node.js only!"); + } // Ensure URL has no trailing slash self.url = self.url ? self.url.replace(/(\/)$/, '') : undefined; @@ -466,6 +523,7 @@ var mikealsReq = require('request'); mikealsReq.get(xOriginCookieURL, function(err, httpResponse, body) { if (err) { + self.isConnecting = false; consolog( 'Failed to connect socket (failed to get cookie)', 'Error:', err @@ -490,7 +548,7 @@ * successfully. */ self.on('connect', function socketConnected() { - + self.isConnecting = false; consolog.noPrefix( '\n' + '\n' + @@ -499,7 +557,7 @@ // '\n'+ ' |> Now connected to Sails.' + '\n' + '\\___/ For help, see: http://bit.ly/1DmTvgK' + '\n' + - ' (using '+io.sails.sdk.platform+' SDK @v'+io.sails.sdk.version+')'+ '\n' + + ' (using sails.io.js '+io.sails.sdk.platform+' SDK @v'+io.sails.sdk.version+')'+ '\n' + '\n'+ '\n'+ // '\n'+ @@ -530,6 +588,9 @@ }); self.on('reconnect', function(transport, numAttempts) { + if (!self.isConnecting) { + self.on('connect', runRequestQueue.bind(self, self)); + } var msSinceConnectionLost = ((new Date()).getTime() - self.connectionLostTimestamp); var numSecsOffline = (msSinceConnectionLost / 1000); consolog( @@ -544,7 +605,7 @@ // (usually because of a failed authorization, which is in turn // usually due to a missing or invalid cookie) self.on('error', function failedToConnect(err) { - + self.isConnecting = false; // TODO: // handle failed connections due to failed authorization // in a smarter way (probably can listen for a different event) @@ -569,6 +630,20 @@ }; + /** + * Reconnect the underlying socket. + * + * @api public + */ + SailsSocket.prototype.reconnect = function (){ + if (this.isConnecting) { + throw new Error('Cannot connect- socket is already connecting'); + } + if (this.isConnected()) { + throw new Error('Cannot connect- socket is already connected'); + } + return this._connect(); + }; /** * Disconnect the underlying socket. @@ -576,7 +651,8 @@ * @api public */ SailsSocket.prototype.disconnect = function (){ - if (!this._raw) { + this.isConnecting = false; + if (!this.isConnected()) { throw new Error('Cannot disconnect- socket is already disconnected'); } return this._raw.disconnect(); @@ -620,12 +696,7 @@ // Bind a one-time function to run the request queue // when the self._raw connects. if ( !self.isConnected() ) { - var alreadyRanRequestQueue = false; - self._raw.on('connect', function whenRawSocketConnects() { - if (alreadyRanRequestQueue) return; - runRequestQueue(self); - alreadyRanRequestQueue = true; - }); + self._raw.once('connect', runRequestQueue.bind(self, self)); } // Or run it immediately if self._raw is already connected else { @@ -712,7 +783,7 @@ * * @api public * @param {String} url :: destination URL - * @param {Object} params :: parameters to send with the request [optional] + * @param {Object} data :: parameters to send with the request [optional] * @param {Function} cb :: callback function to call when finished [optional] */ @@ -740,7 +811,7 @@ * * @api public * @param {String} url :: destination URL - * @param {Object} params :: parameters to send with the request [optional] + * @param {Object} data :: parameters to send with the request [optional] * @param {Function} cb :: callback function to call when finished [optional] */ @@ -768,7 +839,7 @@ * * @api public * @param {String} url :: destination URL - * @param {Object} params :: parameters to send with the request [optional] + * @param {Object} data :: parameters to send with the request [optional] * @param {Function} cb :: callback function to call when finished [optional] */ @@ -796,7 +867,7 @@ * * @api public * @param {String} url :: destination URL - * @param {Object} params :: parameters to send with the request [optional] + * @param {Object} data :: parameters to send with the request [optional] * @param {Function} cb :: callback function to call when finished [optional] */ @@ -861,15 +932,44 @@ throw new Error('Invalid `method` provided (should be a string like "post" or "put")\n' + usage); } if (options.headers && typeof options.headers !== 'object') { - throw new Error('Invalid `headers` provided (should be an object with string values)\n' + usage); + throw new Error('Invalid `headers` provided (should be a dictionary with string values)\n' + usage); } if (options.params && typeof options.params !== 'object') { - throw new Error('Invalid `params` provided (should be an object with string values)\n' + usage); + throw new Error('Invalid `params` provided (should be a dictionary with JSON-serializable values)\n' + usage); + } + if (options.data && typeof options.data !== 'object') { + throw new Error('Invalid `data` provided (should be a dictionary with JSON-serializable values)\n' + usage); } if (cb && typeof cb !== 'function') { throw new Error('Invalid callback function!\n' + usage); } + // Accept either `params` or `data` for backwards compatibility (but not both!) + if (options.data && options.params) { + throw new Error('Cannot specify both `params` and `data`! They are aliases of each other.\n' + usage); + } + else if (options.data) { + options.params = options.data; + delete options.data; + } + + + // If this socket is not connected yet, queue up this request + // instead of sending it. + // (so it can be replayed when the socket comes online.) + if ( ! this.isConnected() ) { + + // If no queue array exists for this socket yet, create it. + this.requestQueue = this.requestQueue || []; + this.requestQueue.push([options, cb]); + return; + } + + // Otherwise, our socket is connected, so continue prepping + // the request. + + // Default headers to an empty object + options.headers = options.headers || {}; // Build a simulated request object // (and sanitize/marshal options along the way) @@ -877,7 +977,7 @@ method: options.method.toLowerCase() || 'get', - headers: options.headers || {}, + headers: options.headers, data: options.params || options.data || {}, @@ -887,19 +987,15 @@ cb: cb }; - // If this socket is not connected yet, queue up this request - // instead of sending it. - // (so it can be replayed when the socket comes online.) - if ( ! this.isConnected() ) { - - // If no queue array exists for this socket yet, create it. - this.requestQueue = this.requestQueue || []; - this.requestQueue.push(requestCtx); - return; + // Merge global headers in + if (this.headers && 'object' == typeof this.headers) { + for (var header in this.headers) { + if (!options.headers.hasOwnProperty(header)) { + options.headers[header] = this.headers[header]; + } + } } - - // Otherwise, our socket is ok! // Send the request. _emitFrom(this, requestCtx); }; @@ -960,6 +1056,14 @@ * @return {Socket} */ io.sails.connect = function(url, opts) { + + // Make URL optional + if ('object' == typeof url) { + opts = url; + url = null; + } + + // Default opts to empty object opts = opts || {}; // If explicit connection url is specified, save it to options @@ -997,7 +1101,7 @@ setTimeout(function() { // If autoConnect is disabled, delete the eager socket (io.socket) and bail out. - if (!io.sails.autoConnect) { + if (io.sails.autoConnect === false || io.sails.autoconnect === false) { delete io.socket; return; } From 7ae836b47490d54fb672da8fcb18520d167d709c Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Tue, 2 Feb 2016 15:10:54 -0600 Subject: [PATCH 3/6] Ensure that SSL options (if provided) are buffers See https://github.com/lodash/lodash/issues/1453 --- lib/hooks/http/initialize.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/hooks/http/initialize.js b/lib/hooks/http/initialize.js index 4340c4195f..701c56bc03 100644 --- a/lib/hooks/http/initialize.js +++ b/lib/hooks/http/initialize.js @@ -31,12 +31,22 @@ module.exports = function(sails) { var app = sails.hooks.http.app = express(); app.disable('x-powered-by'); // (required by Express 3.x) + + // Determine whether or not to create an HTTPS server var usingSSL = sails.config.ssl === true || (sails.config.ssl.key && sails.config.ssl.cert) || sails.config.ssl.pfx; // Merge SSL into server options var serverOptions = sails.config.http.serverOptions || {}; _.extend(serverOptions, sails.config.ssl); + // Lodash 3's _.merge transforms buffers into arrays; if we detect an array, then + // transform it back into a buffer + _.each(['key', 'cert', 'pfx'], function(sslOption) { + if (_.isArray(serverOptions[sslOption])) { + serverOptions[sslOption] = new Buffer(serverOptions[sslOption]); + } + }); + // Get the appropriate server creation method for the protocol var createServer = usingSSL ? require('https').createServer : @@ -117,7 +127,7 @@ module.exports = function(sails) { // app.use() the configured express middleware var defaultMiddleware = require('./middleware/defaults')(sails, app); installHTTPMiddleware(app, defaultMiddleware, sails); - + // Note that it is possible for the configured HTTP middleware stack to be shared with the // core router built into Sails-- this would make the same stack take effect for all virtual requests // including sockets. Currently, an abbreviated version of this stack is built-in to `lib/router/` From 24a19bbba8aaa932c8a32d091320b3b677f5756d Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Tue, 2 Feb 2016 15:35:37 -0600 Subject: [PATCH 4/6] Don't use sails-util for Lodash methods --- lib/hooks/blueprints/index.js | 4 ++-- lib/hooks/csrf/index.js | 2 +- lib/hooks/http/index.js | 2 +- lib/hooks/moduleloader/index.js | 6 +++--- lib/hooks/orm/normalize-datastore.js | 4 ++-- lib/hooks/pubsub/index.js | 24 ++++++++++++------------ lib/hooks/userconfig/index.js | 9 ++++----- lib/router/bind.js | 14 +++++++------- 8 files changed, 32 insertions(+), 33 deletions(-) diff --git a/lib/hooks/blueprints/index.js b/lib/hooks/blueprints/index.js index 21aec4f7cd..d27f4e5a62 100644 --- a/lib/hooks/blueprints/index.js +++ b/lib/hooks/blueprints/index.js @@ -248,7 +248,7 @@ module.exports = function(sails) { if (config.pluralize) { baseRouteName = pluralize(baseRouteName); } - + var baseRoute = config.prefix + '/' + baseRouteName; // Determine base route for RESTful service // Note that restPrefix will always start with / @@ -286,7 +286,7 @@ module.exports = function(sails) { // -> or implicitly by globalId // -> or implicitly by controller id var routeConfig = sails.router.explicitRoutes[controllerId] || {}; - var modelFromGlobalId = sails.util.findWhere(sails.models, {globalId: globalId}); + var modelFromGlobalId = _.findWhere(sails.models, {globalId: globalId}); var modelId = config.model || routeConfig.model || (modelFromGlobalId && modelFromGlobalId.identity) || controllerId; // If the orm hook is enabled, it has already been loaded by this time, diff --git a/lib/hooks/csrf/index.js b/lib/hooks/csrf/index.js index ef8c27ac2b..03317e7b10 100644 --- a/lib/hooks/csrf/index.js +++ b/lib/hooks/csrf/index.js @@ -61,7 +61,7 @@ module.exports = function(sails) { } }; // Add the csrfToken directly to the config'd routes, so that the CORS hook can process it - sails.config.routes = sails.util.extend(csrfRoute, sails.config.routes); + sails.config.routes = _.extend(csrfRoute, sails.config.routes); }, initialize: function(cb) { diff --git a/lib/hooks/http/index.js b/lib/hooks/http/index.js index 0345edb18f..d08106c0d1 100644 --- a/lib/hooks/http/index.js +++ b/lib/hooks/http/index.js @@ -165,7 +165,7 @@ module.exports = function(sails) { sails.config.paths.public = path.resolve(sails.config.appPath, sails.config.paths.public); // Merge in legacy `sails.config.express` object for backwards-compat. - sails.util.defaultsDeep(sails.config.http, sails.config.express||{}); + _.defaultsDeep(sails.config.http, sails.config.express||{}); // If no custom middleware order is specified, make sure the default one is used. // This lets you override default middleware without having to explicitly include the diff --git a/lib/hooks/moduleloader/index.js b/lib/hooks/moduleloader/index.js index a240a4bdc4..7cdf2f005e 100644 --- a/lib/hooks/moduleloader/index.js +++ b/lib/hooks/moduleloader/index.js @@ -160,7 +160,7 @@ module.exports = function(sails) { configure: function() { if (sails.config.moduleLoaderOverride) { var override = sails.config.moduleLoaderOverride(sails, this); - sails.util.extend(this, override); + _.extend(this, override); if (override.configure) { this.configure(); } @@ -264,7 +264,7 @@ module.exports = function(sails) { var env = sails.config.environment; // Merge the configs, with env/*.js files taking precedence over others, and local.js // taking precedence over everything - var config = sails.util.merge( + var config = _.merge( async_data['config/*'], async_data['config/env/**'], async_data['config/env/*'], @@ -330,7 +330,7 @@ module.exports = function(sails) { flattenDirectories: true }, bindToSails(function(err, supplements) { if (err) {return cb(err);} - return cb(null, sails.util.merge(models, supplements)); + return cb(null, _.merge(models, supplements)); })); }); }, diff --git a/lib/hooks/orm/normalize-datastore.js b/lib/hooks/orm/normalize-datastore.js index 158290e625..878b79ee16 100644 --- a/lib/hooks/orm/normalize-datastore.js +++ b/lib/hooks/orm/normalize-datastore.js @@ -5,7 +5,7 @@ var path = require('path'); var fs = require('fs'); var Err = require('../../../errors'); - +var _ = require('lodash'); /** * normalizeDatastore() @@ -85,7 +85,7 @@ module.exports = function howto_normalizeDatastore(sails){ // Defaults connection object to its adapter's defaults // TODO: pull this out into waterline core var itsAdapter = sails.adapters[connectionObject.adapter]; - connection = sails.util.merge({}, itsAdapter.defaults, connectionObject); + connection = _.merge({}, itsAdapter.defaults, connectionObject); // If the adapter has a `registerCollection` method, it must be a v0.9.x adapter if (itsAdapter.registerCollection) { diff --git a/lib/hooks/pubsub/index.js b/lib/hooks/pubsub/index.js index 1568fce9bb..b2e63c0cea 100644 --- a/lib/hooks/pubsub/index.js +++ b/lib/hooks/pubsub/index.js @@ -453,7 +453,7 @@ module.exports = function(sails) { if (contexts === true || contexts == '*') { contexts = this.getAllContexts(); - } else if (sails.util.isString(contexts)) { + } else if (_.isString(contexts)) { contexts = [contexts]; } @@ -572,7 +572,7 @@ module.exports = function(sails) { ); } - if (sails.util.isFunction(this.beforePublishUpdate)) { + if (_.isFunction(this.beforePublishUpdate)) { this.beforePublishUpdate(id, changes, req, options); } @@ -724,7 +724,7 @@ module.exports = function(sails) { // Broadcast to the model instance room this.publish(id, this.identity, 'update', data, socketToOmit); - if (sails.util.isFunction(this.afterPublishUpdate)) { + if (_.isFunction(this.afterPublishUpdate)) { this.afterPublishUpdate(id, changes, req, options); } @@ -755,7 +755,7 @@ module.exports = function(sails) { ); } - if (sails.util.isFunction(this.beforePublishDestroy)) { + if (_.isFunction(this.beforePublishDestroy)) { this.beforePublishDestroy(id, req, options); } @@ -840,7 +840,7 @@ module.exports = function(sails) { } - if (sails.util.isFunction(this.afterPublishDestroy)) { + if (_.isFunction(this.afterPublishDestroy)) { this.afterPublishDestroy(id, req, options); } @@ -900,7 +900,7 @@ module.exports = function(sails) { } // Lifecycle event - if (sails.util.isFunction(this.beforePublishAdd)) { + if (_.isFunction(this.beforePublishAdd)) { this.beforePublishAdd(id, alias, idAdded, req); } @@ -961,7 +961,7 @@ module.exports = function(sails) { } - if (sails.util.isFunction(this.afterPublishAdd)) { + if (_.isFunction(this.afterPublishAdd)) { this.afterPublishAdd(id, alias, idAdded, req); } @@ -993,7 +993,7 @@ module.exports = function(sails) { '.publishRemove(id, alias, idRemoved, [socketToOmit])`' ); } - if (sails.util.isFunction(this.beforePublishRemove)) { + if (_.isFunction(this.beforePublishRemove)) { this.beforePublishRemove(id, alias, idRemoved, req); } @@ -1043,7 +1043,7 @@ module.exports = function(sails) { } - if (sails.util.isFunction(this.afterPublishRemove)) { + if (_.isFunction(this.afterPublishRemove)) { this.afterPublishRemove(id, alias, idRemoved, req); } @@ -1085,7 +1085,7 @@ module.exports = function(sails) { options = options || {}; - if (sails.util.isUndefined(values[this.primaryKey])) { + if (_.isUndefined(values[this.primaryKey])) { return sails.log.error( 'Invalid usage of publishCreate() :: ' + 'Values must have an `'+this.primaryKey+'`, instead got ::\n' + @@ -1093,7 +1093,7 @@ module.exports = function(sails) { ); } - if (sails.util.isFunction(this.beforePublishCreate)) { + if (_.isFunction(this.beforePublishCreate)) { this.beforePublishCreate(values, req); } @@ -1235,7 +1235,7 @@ module.exports = function(sails) { this.introduce(values[this.primaryKey]); } - if (sails.util.isFunction(this.afterPublishCreate)) { + if (_.isFunction(this.afterPublishCreate)) { this.afterPublishCreate(values, req); } diff --git a/lib/hooks/userconfig/index.js b/lib/hooks/userconfig/index.js index a8ff2e0307..e9dd5fff98 100644 --- a/lib/hooks/userconfig/index.js +++ b/lib/hooks/userconfig/index.js @@ -5,8 +5,7 @@ module.exports = function(sails) { * Module dependencies */ - var util = require('sails-util'); - + var _ = require('lodash'); /** @@ -29,7 +28,7 @@ module.exports = function(sails) { sails.log.verbose('Loading app config...'); // Grab reference to mapped overrides - var overrides = util.cloneDeep(sails.config); + var overrides = _.cloneDeep(sails.config); // If appPath not specified yet, use process.cwd() @@ -45,11 +44,11 @@ module.exports = function(sails) { // Finally, extend user config with overrides var config = {}; - config = util.merge(userConfig, overrides); + config = _.merge(userConfig, overrides); // Ensure final configuration object is valid // (in case moduleloader fails miserably) - config = util.isObject(config) ? config : (sails.config || {}); + config = _.isObject(config) ? config : (sails.config || {}); // Save final config into sails.config sails.config = config; diff --git a/lib/router/bind.js b/lib/router/bind.js index 8dd32f1617..86d96a7231 100644 --- a/lib/router/bind.js +++ b/lib/router/bind.js @@ -39,23 +39,23 @@ function bind( /* path, target, verb, options */ ) { // Bind a list of multiple functions in order - if (util.isArray(target)) { + if (_.isArray(target)) { bindArray.apply(this, [path, target, verb, options]); } // Handle string redirects // (to either public-facingĀ URLs or internal routes) - else if (util.isString(target) && target.match(/^(https?:|\/)/)) { + else if (_.isString(target) && target.match(/^(https?:|\/)/)) { bindRedirect.apply(this, [path, target, verb, options]); } // Bind a middleware function directly - else if (util.isFunction(target)) { + else if (_.isFunction(target)) { bindFunction.apply(this, [path, target, verb, options]); } // If target is an object with a `target`, pull out the rest // of the keys as route options and then bind the target. - else if (util.isPlainObject(target) && target.target) { + else if (_.isPlainObject(target) && target.target) { var _target = _.cloneDeep(target.target); options = _.merge(options, _.omit(target, 'target')); bind.apply(this, [path, _target, verb, options]); @@ -120,7 +120,7 @@ function bindArray(path, target, verb, options) { sails.log.verbose('Ignoring empty array in `router.bind(' + path + ')`...'); } else { // Bind each middleware fn - util.each(target, function(fn) { + _.each(target, function(fn) { bind.apply(self,[path, fn, verb, options]); }); } @@ -140,7 +140,7 @@ function bindFunction(path, fn, verb, options) { var skipAssetsRegex = /^[^?]*\/[^?/]+\.[^?/]+(\?.*)?$/; // Make sure (optional) options is a valid plain object ({}) - options = util.isPlainObject(options) ? _.cloneDeep(options) : {}; + options = _.isPlainObject(options) ? _.cloneDeep(options) : {}; var _middlewareType = options._middlewareType || fn._middlewareType || (fn.name && ('FUNCTION: ' + fn.name)); sails.log.silly('Binding route :: ', verb || '', path, _middlewareType?('('+_middlewareType+')'):''); @@ -221,7 +221,7 @@ function bindFunction(path, fn, verb, options) { var skipRegexesWrapper = function(regexes, fn) { // Remove anything that's not a regex - regexes = sails.util.compact(regexes.map(function(regex) { + regexes = _.compact(regexes.map(function(regex) { if (regex instanceof RegExp) { return regex; } From 20ff1156bb485185461c1dabc4391cd642ca5f98 Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Tue, 2 Feb 2016 15:37:30 -0600 Subject: [PATCH 5/6] Better HTTPs tests Ensure that cert & key are specified as buffers, and save them in config/env/development.js so that Lodash's merge will be applied. Lodash 3.x merge transforms buffers to arrays, so this tests our fix in https://github.com/balderdashy/sails/commit/7ae836b47490d54fb672da8fcb18520d167d709c --- test/integration/lift.https.test.js | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/test/integration/lift.https.test.js b/test/integration/lift.https.test.js index b27fccc78b..b8d53c8172 100644 --- a/test/integration/lift.https.test.js +++ b/test/integration/lift.https.test.js @@ -24,13 +24,7 @@ describe('Starting HTTPS sails server with lift', function() { var sailsServer; before(function() { - var opts = { - ssl: { - key: require('fs').readFileSync(require('path').resolve(__dirname, 'cert','sailstest-key.pem')).toString(), - cert: require('fs').readFileSync(require('path').resolve(__dirname, 'cert','sailstest-cert.pem')).toString() - } - }; - fs.writeFileSync(path.resolve('../', appName, 'config/ssl.js'), "module.exports = " + JSON.stringify(opts) + ";"); + fs.writeFileSync(path.resolve('../', appName, 'config/env/development.js'), "module.exports = {ssl: {key: require('fs').readFileSync('"+require('path').resolve(__dirname, 'cert','sailstest-key.pem')+"'), cert: require('fs').readFileSync('"+require('path').resolve(__dirname, 'cert','sailstest-cert.pem')+"')}};"); }); after(function(done) { @@ -69,16 +63,7 @@ describe('Starting HTTPS sails server with lift', function() { var sailsServer; before(function() { - var opts = { - ssl: true, - http: { - serverOptions: { - key: require('fs').readFileSync(require('path').resolve(__dirname, 'cert','sailstest-key.pem')).toString(), - cert: require('fs').readFileSync(require('path').resolve(__dirname, 'cert','sailstest-cert.pem')).toString() - } - } - }; - fs.writeFileSync(path.resolve('../', appName, 'config/ssl.js'), "module.exports = " + JSON.stringify(opts) + ";"); + fs.writeFileSync(path.resolve('../', appName, 'config/env/development.js'), "module.exports = {ssl: true, http: {serverOptions: { key: require('fs').readFileSync('"+require('path').resolve(__dirname, 'cert','sailstest-key.pem')+"'), cert: require('fs').readFileSync('"+require('path').resolve(__dirname, 'cert','sailstest-cert.pem')+"')}}};"); }); after(function(done) { From 183f460d17458a0268fed9a980f16c5c9bb18173 Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Tue, 2 Feb 2016 15:58:37 -0600 Subject: [PATCH 6/6] 0.12.0-rc9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f282267389..d14d8e7716 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sails", "author": "Mike McNeil <@mikermcneil>", - "version": "0.12.0-rc8", + "version": "0.12.0-rc9", "description": "API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)", "homepage": "http://sailsjs.org", "keywords": [