Beispiel #1
0
			// Wrap up the view middleware to supply access to
			// the original target when requests comes in
			function viewHandler (originalFn) {

				if ( !util.isFunction(originalFn) ) {
					sails.log.error(
						'Error binding view to route :: View middleware is not a function!', 
						originalFn, 'for path: ', path, verb ? ('and verb: ' + verb) : '');
					return;
				}
				
				// Bind intercepted middleware function to route
				return function wrapperFn (req, res, next) {
					
					// Set target metadata
					req.target = {
						view: target.view
					};

					// Merge in any view locals specified in route options
					if (options.locals) {
						util.extend(res.locals || {}, options.locals);
						sails.log.silly('Merged in view locals..',res.locals);
					}
					
					// Call actual controller
					originalFn(req, res, next);
				};
			}
Beispiel #2
0
		normalizePolicy: function (policy) {


			// Recursively normalize lists of policies
			if ( util.isArray(policy) ) {

				var policyChain = util.clone(policy);

				// Normalize each policy in the chain
				policyChain = util.map(policyChain, function normalize_each_policy (policy) {
					return policyHookDef.normalizePolicy(policy);
				}, this);

				// Then flatten the policy chain
				return util.flatten(policyChain);
			}

			// Look up the policy in the policy registry
			if ( util.isString(policy) ) {
				var policyFn = this.lookupFn(policy, 'config.policies');
        // Set the "policy" key on the policy function to the policy name, for debugging
        policyFn._middlewareType = 'POLICY: '+policy;
				return [policyFn];
			}

			// An explicitly defined, anonymous policy middleware can be directly attached
			if ( util.isFunction(policy) ) {

				var anonymousPolicy = util.clone(policy);
        // Set the "policy" key on the function name (if any) for debugging
        anonymousPolicy._middlewareType = 'POLICY: '+(anonymousPolicy.name || 'anonymous');
				return [anonymousPolicy];
			}

			// A false or null policy means NEVER allow any requests
			if (policy === false || policy === null) {
        var neverAllow = function neverAllow (req, res, next) {
          res.send(403);
        };
        neverAllow._middlewareType = 'POLICY: neverAllow';
				return [neverAllow];
			}

			// A true policy means ALWAYS allow requests
			if (policy === true) {
        var alwaysAllow = function alwaysAllow (req, res, next) {
          next();
        };
        alwaysAllow._middlewareType  = 'POLICY: alwaysAllow';
				return [alwaysAllow];
			}

			// If we made it here, the policy is invalid
			sails.log.error('Cannot map invalid policy: ', policy);
			return [function(req, res) {
				throw new Error('Invalid policy: ' + policy);
			}];
		},
Beispiel #3
0
		util.each(socketConfig, function(value, propertyName) {

			// Handle `Memory` adapter value
			if (propertyName === 'adapter' && value === 'memory') return;

			// Setup custom socket.io MQ (pubsub) store(s)
			if (propertyName === 'adapter' && value === 'redis') {
				var host = socketConfig.host || '127.0.0.1';
				var port = socketConfig.port || 6379;

				var pub = createRedisConnection(port, host);
				var sub = createRedisConnection(port, host);
				var client = createRedisConnection(port, host);

				var storeConfig = {
					redisPub: pub,
					redisSub: sub,
					redisClient: client
				};

				// Add a pointer to the redis, required with Auth
				if(socketConfig.pass) {
					storeConfig.redis = Redis;
				}

				io.set('store', new RedisStore(storeConfig));
				return;
			}

			// Configure logic to be run before allowing sockets to connect
			if (propertyName === 'authorization') {

				// Custom logic
				if (util.isFunction(value)) {
					io.set('authorization', value);
					return;
				}

				// `authorization: true` means go ahead and use the default behavior
				if (value === true) {
					io.set('authorization', Socket.authorization);
					return;
				}

				// Otherwise skip the authorization step
				io.set('authorization', false);

				return;
			}

			// If value is explicitly undefined, do nothing
			if (util.isUndefined(value)) return;

			// In the general case, pass the configuration straight down to socket.io
			io.set(propertyName, value);

		});
Beispiel #4
0
function bind( /* path, target, verb, options */ ) {
  var sails = this.sails;

  var args = sanitize.apply(this, Array.prototype.slice.call(arguments));
  var path = args.path;
  var target = args.target;
  var verb = args.verb;
  var options = args.options;


  // Bind a list of multiple functions in order
  if (util.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?:|\/)/)) {
    bindRedirect.apply(this, [path, target, verb, options]);
  }

  // Bind a middleware function directly
  else if (util.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) {
    var _target = _.cloneDeep(target.target);
    delete target.target;
    options = _.merge(options, target);
    bind.apply(this, [path, _target, verb, options]);
  }
  else {

    // If we make it here, the router doesn't know how to parse the target.
    //
    // This doesn't mean that it's necessarily invalid though--
    // so we'll emit an event informing any listeners that an unrecognized route
    // target was encountered.  Then hooks can listen to this event and act
    // accordingly.  This makes it easier to add functionality to Sails.
    sails.emit('route:typeUnknown', {
      path: path,
      target: target,
      verb: verb,
      options: options
    });

    // TODO: track emissions of "typeUnknown" to avoid logic errors that result in circular routes
    // (part of the effort to make a more friendly environment for custom hook developers)
  }

  // Makes `.bind()` chainable (sort of)
  return sails;

}
Beispiel #5
0
		_interpretRouteSyntax: function (route) {
			var target = route.target,
				path = route.path,
				verb = route.verb,
				options = route.options;

			if (util.isObject(target) && !util.isFunction(target) && !util.isArray(target)) {
				
				// Support { view: 'foo/bar' } notation
				if (util.isString(target.view)) {
					return this.bindView(path, target, verb, options);
				}
			}

			// Ignore unknown route syntax
			// If it needs to be understood by another hook, the hook would have also received
			// the typeUnknown event, so we're done.
			return;
		},
Beispiel #6
0
	return function load (configOverride, cb) {

		// configOverride is optional
		if (util.isFunction(configOverride)) {
			cb = configOverride;
			configOverride = {};
		}

		// Ensure override is an object and clone it (or make an empty object if it's not)
		configOverride = configOverride || {};
		sails.config = util.cloneDeep(configOverride);


		// If host is explicitly specified, set `explicitHost`
		// (otherwise when host is omitted, Express will accept all connections via INADDR_ANY)
		if (configOverride.host) {
			configOverride.explicitHost = configOverride.host;
		}
		

		async.auto({

			// Apply core defaults and hook-agnostic configuration,
			// esp. overrides including command-line options, environment variables,
			// and options that were passed in programmatically.
			config: [ Configuration.load ],

			// Load hooks into memory, with their middleware and routes
			hooks: [ 'config', loadHooks ],

			// Populate the middleware registry
			// (Basically, that means: grab `middleware` object from each hook
			//  and make it available as `sails.middleware.[HOOK_ID]`.)
			middleware: [ 'hooks', MiddlewareRegistry.load ],

			// Load the router and bind routes in `sails.config.routes`
			router: [ 'middleware', Router.load ]

		}, ready__(cb) );
	};
Beispiel #7
0
    /**
     * Send a low-level error back over the socket
     * (useful in cases where basic interpreter plumbing is not working)
     *
     * Request callback function will NOT be triggered!
     * Instead, an error message will be emitted.
     */
    function emitError (error) {

      // TODO: implement best practice for socket.io error reporting

      // TODO: something like this..?
      // e.g.
      // socket.emit('sails__500', 'error');

      // ********************************************************
      // WARNING
      //
      // This is a breaking change!!
      // Do not implement until next minor release (0.10.0)
      //
      // Will require documentation of clear steps in changelog
      // and some changes in bundled client-side SDK, i.e.
      // assets/js/sails.io.js
      // -AND-
      // assets/linker/js/sails.io.js
      // ********************************************************

      ////////////////////////////////////
      // But for now:
      ////////////////////////////////////

      // Log error
      sails.log.error(error);

      // If callback is invalid or non-existent:
      if ( !util.isFunction(socketIOCallback) ) {
        // do nothing...
        return;
      }

      // Otherwise just send the error directly to the callback...
      socketIOCallback(error);
    }
Beispiel #8
0
		configure: function () {

			if (sails.config.viewEngine) {
				log.warn('The `config.viewEngine` config has been deprecated in favor of `config.views.engine`.');
				log.warn('It has been automatically migrated, but you\'ll continue to see this warning until you change your configuration files.');
				sails.config.views.engine = sails.config.viewEngine;
			}

			// Normalize view engine config
			if (typeof sails.config.views.engine === 'string') {
				var viewExt = sails.config.views.engine;
				sails.config.views.engine = {
					ext: viewExt
				};
			}

			// Ensure valid config
			if (! (sails.config.views.engine && sails.config.views.engine.ext) ) {
				log.error('Invalid view engine configuration. `config.views.engine` should');
				log.error('be set to either a `string` or an `object` with the following properties:');
				log.error('    {');
				log.error('        ext: <string>,   // the file extension');
				log.error('        fn: <function>   // the template engine render function');
				log.error('    }');
				log.error('For example: {ext:"html", fn: require("consolidate").swig}');
				log.error('For details: http://expressjs.com/api.html#app.engine');
				throw new Error('Invalid view engine configuration.');
			}

			// Try to load view module if a function wasn't specified directly
			if ( !sails.config.views.engine.fn ) {
				var viewEngineModulePath = sails.config.appPath + '/node_modules/' + sails.config.views.engine.ext, 
					fn;
				try {
					// 
					fn = consolidate(sails.config.appPath + '/node_modules')[sails.config.views.engine.ext];

					if ( !util.isFunction(fn) ) {
						throw new Error('Invalid view engine-- are you sure it supports `consolidate`?');
					}
				}
				catch (e) {
					log.error('Your configured server-side view engine (' + sails.config.views.engine.ext + ') could not be found.');
					log.error('Usually, this just means you need to install a dependency.');
					log.error('To install ' + sails.config.views.engine.ext + ', run:  `npm install ' + sails.config.views.engine.ext + ' --save`');
					log.error('Otherwise, please change your `engine` configuration in config/views.js.');
					throw e;
				}

				// Save reference to view rendering function
				sails.config.views.engine.fn = fn;
				sails.log.silly('Configured view engine, `' + sails.config.views.engine.ext + '`');
			}


			// Let user know that a leading . is not required in the viewEngine option and then fix it
			if (sails.config.views.engine.ext[0] === '.') {
				log.warn('A leading `.` is not required in the views.engine option.  Removing it for you...');
				sails.config.views.engine.ext = sails.config.views.engine.ext.substr(1);
			}

			// Custom layout location
			// (if string specified, it's used as the relative path from the views folder)
			// (if not string, but truthy, relative path from views folder defaults to ./layout.*)
			// (if falsy, don't use layout)
			if ( !util.isString(sails.config.views.layout) && sails.config.views.layout ) {
				sails.config.views.layout = 'layout.' + sails.config.views.engine.ext;
			}

			if ( sails.config.views.engine.ext !== 'ejs' &&
					sails.config.views.layout ) {
				sails.log.warn('Sails\' built-in layout support only works with the `ejs` view engine.');
				sails.log.warn('You\'re using `'+ sails.config.views.engine.ext +'`, which very well may have its own built-in layout support.');
				sails.log.warn('But that\'s up to you to figure out!  Ignoring `config.layout`...');
			}
		},
Beispiel #9
0
 get: function(sessionId, cb) {
   if (!util.isFunction(cb)) {
     throw new Error('Invalid usage :: `Session.get(sessionId, cb)`');
   }
   return sails.config.session.store.get(sessionId, cb);
 },
Beispiel #10
0
	return function interpretSocketReq (socketReq, socketIOCallback, socket, verb, cb) {

		var msg;

		// If invalid callback function specified, freak out
		if (socketIOCallback && !util.isFunction(socketIOCallback)) {
			msg = 'Invalid socket request! The following JSON could not be parsed :: '+socketReq;
			return sails.log.error(msg);
		}

		// Parse request as JSON (or just use the object if we have one)
		if (! util.isObject(socketReq) && util.isString(socketReq) ) {
			try {
				socketReq = JSON.parse(socketReq);
			} catch (e) {
				msg = 'Invalid socket request! The following JSON could not be parsed :: '+socketReq;
				return emitError(msg);
			}
		}

		// If no URL specified, error out
		if (!socketReq.url) {
			msg = 'No url provided in request: '+socketReq;
			return emitError(msg);
		}

		if (!util.isString(socketReq.url)) {
			msg = 'Invalid url provided in request: ' + socketReq.url;
			return emitError(msg);
		}

		// Parse query string (`req.query`)
		var queryStringPos = socketReq.url.indexOf('?');
		var queryParams =	( queryStringPos === -1 ) ? 
							{} : 
							Qs.parse( socketReq.url.substr(queryStringPos + 1) );

		// Attached data becomes simulated HTTP body (`req.body`)
		var bodyParams = util.extend({}, socketReq.params || {}, socketReq.data || {});

		// Parse out enough information from message to mock an HTTP request
		var path = socketReq.url;

		// Build request object
		var req = {
			
			// TODO: grab actual transports from socket.io
			transport: 'socket.io',

			method	: verb,

			protocol: 'ws',

			ip      : socket.handshake.address.address ,

			port	: socket.handshake.address.port ,

			url		: socketReq.url,

			socket	: socket,

			// Backwards compatibility for isSocket qualifier
			isSocket: true,

			// Request params (`req.params`) are automatically parsed from URL path by slave router
			query	: queryParams || {},
			body	: bodyParams || {},

			// Lookup parameter
			param: function(paramName) {

				var key, params = {};
				for (key in (req.params || {}) ) {
					params[key] = req.params[key];
				}
				for (key in (req.query || {}) ) {
					params[key] = req.query[key];
				}
				for (key in (req.body || {}) ) {
					params[key] = req.body[key];
				}

				// Grab the value of the parameter from the appropriate place
				// and return it
				return params[paramName];
			},
			
			// Allow optional headers
			headers: util.defaults({
				host: sails.config.host
			}, socketReq.headers || {}),

		};


		/**
		 * req.header( headerName, [defaultValue] )
		 *
		 * Backwards compat. for Express 2.x
		 * http://expressjs.com/2x/guide.html#req.header()
		 *
		 * Looks up value of INCOMING request header called `headerName`
		 *
		 * @api deprecated
		 */
		req.header = function getHeader(headerName, defaultValue) {
			var headerValue = req.headers[headerName];
			return (typeof headerValue === 'undefined') ? defaultValue : headerValue;
		};

		

		/**
		 * socket.join()
		 * https://github.com/LearnBoost/socket.io/wiki/Rooms
		 * 
		 * Join the specified room (listen for messages/broadcasts to it)
		 * Associates the current socket
		 *
		 * @api public
		 * @alias req.listen()
		 * @alias req.subscribe()
		 */
		req.join = function (room) {

			// TODO: add support for optional callback (for use w/ redis)
			return this.socket.join(roomName);
		};
		req.subscribe = req.join;
		req.listen = req.join;


		


		// Build response object as stream
		var res = util.extend(new ResStream(), {

			/**
			 * http://nodejs.org/api/http.html#http_response_statuscode
			 * Equivalent to Node's status code for HTTP.
			 *
			 * @api private
			 */
			statusCode: null,

			/**
			 * http://expressjs.com/api.html#res.charset
			 * Assign the charset.
			 *
			 * @defaultsTo 'utf-8'
			 * @api public
			 */			
			charset: 'utf-8'

		});


		/**
		 * Set status code
		 *
		 * http://expressjs.com/api.html#res.status
		 *
		 * @chainable
		 * @api public
		 */
		res.status = function setStatusCode (code) {
			res.statusCode = code;
			return res;
		};


		/**
		 * Send a response if a callback was specified
		 * if no callback was specified, emit event over socket
		 *
		 * http://expressjs.com/api.html#res.send
		 *
		 * @api public
		 */
		res.send =
		saveSessionAndThen(
		function sendSimpleResponse (	/* [statusCode|body],[statusCode|body] */ ) {
			var args		= normalizeResArgs(arguments),
				statusCode	= args.statusCode,
				body		= args.other;

			// Don't allow users to respond/redirect more than once per request
			onlyAllowOneResponse(res);

			// Ensure statusCode is set
			// (override `this.statusCode` if `statusCode` argument specified)
			this.statusCode = statusCode || this.statusCode || 200;

			// Ensure charset is set
			this.charset = this.charset || 'utf-8';

			// Trigger callback with response body
			// TODO: and headers
			// TODO: and status code
			socketIOCallback(body);
			return res;
		});
		


		/**
		 * Redirect to a different url
		 *
		 * @api public
		 */
		res.redirect =
		saveSessionAndThen(
		function doRedirect ( /* [location|statusCode], [location|statusCode] */ ) {
			var args		= normalizeResArgs(arguments),
				statusCode	= args.statusCode,
				location	= args.other;

			// Don't allow users to respond/redirect more than once per request
			onlyAllowOneResponse(res);

			// Ensure statusCode is set
			res.statusCode = statusCode || res.statusCode || 302;

			// Prevent redirects to public URLs
			var PUBLIC_URL = /^[^\/].+/;
			if ( location.match( PUBLIC_URL ) ) {
				return emitError( Err.invalidRedirect(location) );
			}

			// Set URL for redirect
			req.url = location;

			// Simulate another request at the new url
			sails.emit('router:request', req, res);
		});



		/**
		 * Send json response
		 *
		 * @api public
		 */
		res.json = function sendJSON ( /* [statusCode|obj],[statusCode|obj] */ ) {
			var args		= normalizeResArgs(arguments),
				statusCode	= args.statusCode,
				obj			= args.other;

			// Stringify JSON and send response
			var body = sails.util.stringify(obj);

			if (!body) {
				return sendError(
				'res.json() :: Error stringifying JSON :: ' + obj,
				500);
			}

			return this.send(statusCode, body);
		};



		/**
		 * There isn't really an equivalent for JSONP over sockets
		 * so we can just transparently defer to `res.json()`
		 *
		 * @api public
		 */
		res.jsonp = function sendJSONP ( /* [statusCode|obj],[statusCode|obj] */ ) {
			return this.json.apply(this, arguments);
		};


		/**
		 * res.header( headerName [,value] )
		 *
		 * Backwards compat. for Express 2.x
		 * http://expressjs.com/2x/guide.html#res.header()
		 *
		 * Gets or sets value of OUTGOING response header.
		 *
		 * @api deprecated
		 */
		res.header = function getHeader(headerName, value) {
			// `res.header(headerName, value)` 
			// Sets `headerName` to `value`
			if (value) {
				res.headers[headerName] = value;
				return value;
			}

			// `res.header(headerName)`
			// Returns value of `headerName`
			return res.headers[headerName];
		};


		/**
		 * http://expressjs.com/api.html#res.render
		 * http://expressjs.com/api.html#res.locals
		 *
		 * TODO: Built-in support for rendering view templates (use `consolidate`)
		 * TODO: Built-in support for locals
		 * TODO: Built-in support for partials
		 * TODO: Built-in support for layouts equivalent to the built-in ejs-locals support for HTTP requests
		 *
		 * @chainable
		 * @api unsupported
		 * @todo
		 */
		res.render = function renderViewOverSockets (view, options, fn) {
			sendError(
				'You are trying to render a view (' + view + '), ' +
				'but Sails doesn\'t support rendering views over Socket.io... yet!\n' +
				'You might consider serving your HTML view normally, then fetching data with sockets ' +
				'in your client-side JavaScript.\n' +
				'If you didn\'t intend to serve a view here, you might look into content-negotiation\n' +
				'to handle AJAX/socket requests explictly, instead of `res.redirect()`/`res.view()`.'
			);
			return res;
		};
		 

		/**
		 * Scoped local variables accesible from views
		 * see also: http://expressjs.com/api.html#res.locals
		 */
		res.locals = (new function Locals (){
			this.partial = function renderPartial () {
				return sendError('View partials not implemented over socket.io.');
			};
		}());

		/**
		 * Get or set the value of a local variable in the view
		 *
		 * Backwards compat. for Express 2.x
		 * http://expressjs.com/2x/guide.html#res.local()
		 *
		 * @chainable
		 * @api deprecated
		 */
		res.local = function setLocal (localName, value) {
			// `res.local(localName)`
			// Sets `localName` to `value`
			if (value) {
				res.locals[localName] = value;
				return value;
			}

			// `res.local(localName)`
			// Returns value of `localName`
			return res.locals[localName];
		};


		/**
		 * http://expressjs.com/api.html#res.format
		 *
		 * Serving files is not part of the short-term roadmap for the socket interpreter.
		 *
		 * @chainable
		 * @api unsupported
		 */
		res.format = todo('format');


		/**
		 * http://expressjs.com/api.html#res.download
		 * http://expressjs.com/api.html#res.attachment
		 * http://expressjs.com/api.html#res.sendfile
		 *
		 * Serving files is not part of the short-term roadmap for the socket interpreter.
		 *
		 * @chainable
		 * @api unsupported
		 */
		res.download = todo('download');
		res.sendfile = todo('sendfile');
		res.attachment = todo('attachment');


		// TODO: Implement support for other `res.*` methods from Express
		res.contentType = todo('contentType');
		res.type = todo('type');
		res.links = todo('links');
		res.header = todo('header');
		res.set = todo('set');
		res.get = todo('get');
		res.clearCookie = todo('clearCookie');
		res.signedCookie = todo('signedCookie');
		res.cookie = todo('cookie');





		/**
		 * Access to underlying socket
		 *
		 * @api public
		 */
		res.socket = socket;


		/**
		 * Publish some data to a room
		 *
		 * @param {String} room
		 * @param {Object} data
		 *
		 * @api public
		 * @alias res.publish()
		 */
		res.broadcast = function broadcastMessage (room, data) {
			req.socket.broadcast.to(room).json.send(data);
			return res;
		};
		res.publish = res.broadcast;


		// Populate req.session using shared session store
		sails.session.fromSocket(req.socket, function sessionReady (err, session) {
			if (err) return cb(err);

			// Provide access to session data as req.session
			req.session = session;

			// Send newly constructed req and res objects back to router
			return cb(null, {
				req: req,
				res: res
			});
		});




		// Retrieve session data from store
		// var sessionKey = socket.handshake.sessionID;

		// if (!sessionKey) {
		// 	sails.log.verbose('The socket trying to connect has no session id.');
		// }

		// sails.session.get(sessionKey, function (err, sessionData) {
		// 	if (err) {
		// 		sails.log.error('Error retrieving session: ' + err);
		// 		return cb('Error retrieving session: ' + err);
		// 	}

		// 	// Create session for first time if necessary
		// 	if (!util.isObject(sessionData)) {
				
		// 		// If a socket makes it here, even though its associated session is unknown, 
		// 		// it's authorized as far as the app is concerned, so no need to do that again.
		// 		// Instead, create a new session:
		// 		sessionData = {
		// 			cookie: { path: '/', httpOnly: true, maxAge: null }
		// 		};
		// 	}
		// 	// Otherwise session exists and everything is ok.
			

		// 	// Add method to trigger a save() of the session data
		// 	function SocketIOSession () {
		// 		this.save = function (cb) {
		// 			sails.session.set(sessionKey, req.session, function (err) {
		// 				if (err) {
		// 					sails.log.error('Error encountered saving session:');
		// 					sails.log.error(err);
		// 				}
		// 				if (cb) cb(err);
		// 			});
		// 		};
		// 	}

		// 	// Instantiate SocketIOSession
		// 	req.session = new SocketIOSession();

		// 	// Provide access to session data in req.session
		// 	util.extend(req.session, sessionData);

			// return cb(null, {
			// 	req: req,
			// 	res: res
			// });
			
		// });







		// Respond with a message indicating that the feature is not compatible with sockets
		function notSupportedError() {
			return sendError('Trying to invoke unsupported response method (`res.foo`) in response to a request from socket.io!');
		}

		// Return function which responds with a message indicating that the method
		// is not yet implemented
		function todo (method) {
			return function () {
				return sendError(
					'res.' + method + '() is not yet supported over socket.io.  '+
					'If you need this functionality, please don\'t hesitate to get involved!'
				);
			};
		}

		// Respond with an error message
		function sendError(errmsg) {
			sails.log.warn(errmsg);
			res.json({
				error: errmsg,
				success: false
			});
		}

		/**
		 * Send a low-level error back over the socket
		 * (useful in cases where basic interpreter plumbing is not working)
		 *
		 * Request callback function will NOT be triggered! 
		 * Instead, an error message will be emitted.
		 */
		function emitError (error) {

			// TODO: implement best practice for socket.io error reporting

			// TODO: something like this..?
			// e.g.
			// socket.emit('sails__500', 'error');

			// ********************************************************
			// WARNING
			//
			// This is a breaking change!!
			// Do not implement until next minor release (0.10.0)
			//
			// Will require documentation of clear steps in changelog
			// and some changes in bundled client-side SDK, i.e.
			// assets/js/sails.io.js
			// -AND-
			// assets/linker/js/sails.io.js
			// ********************************************************

			////////////////////////////////////
			// But for now:
			////////////////////////////////////

			// Log error
			sails.log.error(error);

			// If callback is invalid or non-existent:
			if ( !util.isFunction(socketIOCallback) ) {
				// do nothing...
			}

			// Otherwise just send the error directly to the callback...
			socketIOCallback(error);
		}


		/**
		 * NOTE: ALL RESPONSES (INCLUDING REDIRECTS) ARE PREVENTED ONCE THE RESPONSE HAS BEEN SENT!!
		 * Even though this is not strictly required with sockets, since res.redirect()
		 * is an HTTP-oriented method from Express, it's important to maintain consistency.
		 *
		 * @api private
		 */
		function onlyAllowOneResponse () {
			// TODO
			return;
		}

		/**
		 * As long as one of them is a number (i.e. a status code),
		 * allows a 2-nary method to be called with flip-flopped arguments:
		 *		method( [statusCode|other], [statusCode|other] )
		 *
		 * This avoids confusing errors & provides Express 2.x backwards compat.
		 *
		 * E.g. usage in res.send():
		 *		var args		= normalizeResArgs.apply(this, arguments),
		 *			body		= args.other,
		 *			statusCode	= args.statusCode;
		 * 
		 * @api private
		 */
		function normalizeResArgs( args ) {

			// Traditional usage:
			// `method( other [,statusCode] )`
			var isTraditionalUsage = 
				'number' !== typeof args[0] && 
				( !args[1] || 'number' === typeof args[1] );

			if ( isTraditionalUsage ) {
				return {
					statusCode: args[1],
					other: args[0]
				};
			}

			// Explicit usage, i.e. Express 3:
			// `method( statusCode [,other] )`
			return {
				statusCode: args[0],
				other: args[1]
			};
		}

		// Returns function which saves session, then triggers callback.
		//
		// Session is saved automatically when:
		//	+ res.send() or res.json() is called
		//	+ res.redirect() is called
		//	+ TODO: res receives an 'end' event from a stream piped into it
		function saveSessionAndThen(cb) {
			return function saveSession () {
				var ctx = this,
					args = Array.prototype.slice.call(arguments);

				req.session.save(function (err) {
					if (err) {
						sails.log.error('Session could not be persisted:',err);
					}
					cb.apply(ctx,args);
				});
			};
		}
	};
Beispiel #11
0
			/**
			 * Apply hook default configuration to sails.config
			 *
			 * @api private
			 */
			function applyDefaults () {
				util.defaultsDeep(sails.config,
					util.isFunction(self.defaults) ?
						self.defaults(sails.config) :
						self.defaults);
			}
Beispiel #12
0
  return function interpretSocketReq (socketReq, socketIOCallback, socket, messageName, cb) {

    var msg;

    // If invalid callback function specified, freak out
    if (socketIOCallback && !util.isFunction(socketIOCallback)) {
      msg = 'Invalid socket request Could not be parse :: '+socketReq;
      return sails.log.error(msg);
    }

    // Parse request as JSON (or just use the object if we have one)
    if (! util.isObject(socketReq) && util.isString(socketReq) ) {
      try {
        socketReq = JSON.parse(socketReq);
      } catch (e) {
        msg = 'Invalid socket request! The following JSON could not be parsed :: '+socketReq;
        return emitError(msg);
      }
    }

    // If no URL specified, error out
    if (!socketReq.url) {
      msg = 'No url provided in request: '+socketReq;
      return emitError(msg);
    }

    if (!util.isString(socketReq.url)) {
      msg = 'Invalid url provided in request: ' + socketReq.url;
      return emitError(msg);
    }


    // Grab the metadata for the SDK
    var sdk = getSDKMetadata(socket.handshake);

    // Attached data becomes simulated HTTP body (`req.body`)
    // Allow `params` or `data` to be specified for backwards/sideways-compatibility.
    var bodyParams = util.extend({}, socketReq.params || {}, socketReq.data || {});

    // Get forwarded ip:port from x-forwarded-for header if IIS
    var forwarded = socket.handshake.headers['x-forwarded-for'];
    forwarded = forwarded && forwarded.split(':') || [];

    // Build request object
    var req = {

      // TODO: grab actual transports from socket.io
      transport: 'socket.io',

      method  : getVerb(socketReq, messageName),

      protocol: 'ws',

      ip      : forwarded[0] || socket.handshake.address && socket.handshake.address.address ,

      port  : forwarded[1] || socket.handshake.address && socket.handshake.address.port ,

      url   : socketReq.url,

      socket  : socket,

      isSocket: true,

      // Request params (`req.params`) are automatically parsed from URL path by the private router
      // query : queryParams || {},
      body  : bodyParams || {},

      // Lookup parameter
      param: function(paramName) {

        var key, params = {};
        for (key in (req.params || {}) ) {
          params[key] = req.params[key];
        }
        for (key in (req.query || {}) ) {
          params[key] = req.query[key];
        }
        for (key in (req.body || {}) ) {
          params[key] = req.body[key];
        }

        // Grab the value of the parameter from the appropriate place
        // and return it
        return params[paramName];
      },

      // Allow optional headers
      headers: util.defaults({
        host: sails.config.host
      }, socketReq.headers || {}),

    };


    /**
     * req.header( headerName, [defaultValue] )
     *
     * Backwards compat. for Express 2.x
     * http://expressjs.com/2x/guide.html#req.header()
     *
     * Looks up value of INCOMING request header called `headerName`
     *
     * @api deprecated
     */
    req.header = function getHeader(headerName, defaultValue) {
      var headerValue = req.headers[headerName];
      return (typeof headerValue === 'undefined') ? defaultValue : headerValue;
    };



    /**
     * socket.join()
     * https://github.com/LearnBoost/socket.io/wiki/Rooms
     *
     * Join the specified room (listen for messages/broadcasts to it)
     * Associates the current socket
     *
     * @api public
     * @alias req.listen()
     * @alias req.subscribe()
     */
    req.join = function (room) {

      // TODO: add support for optional callback (for use w/ redis)
      return this.socket.join(roomName);
    };
    req.subscribe = req.join;
    req.listen = req.join;





    // Build response object as stream
    var res = util.extend(new ResStream(), {

      /**
       * http://nodejs.org/api/http.html#http_response_statuscode
       * Equivalent to Node's status code for HTTP.
       *
       * @api private
       */
      statusCode: null,

      /**
       * http://expressjs.com/api.html#res.charset
       * Assign the charset.
       *
       * @defaultsTo 'utf-8'
       * @api public
       */
      charset: 'utf-8'

    });


    /**
     * Set status code
     *
     * http://expressjs.com/api.html#res.status
     *
     * @chainable
     * @api public
     */
    res.status = function setStatusCode (code) {
      res.statusCode = code;
      return res;
    };


    /**
     * Send a response if a callback was specified
     * if no callback was specified, emit event over socket
     *
     * http://expressjs.com/api.html#res.send
     *
     * @api public
     */
    res.send = saveSessionAndThen(req, sails,
    function sendSimpleResponse ( /* [statusCode|body],[statusCode|body] */ ) {
      var args    = normalizeResArgs(arguments),
        statusCode  = args.statusCode,
        body    = args.other;

      // Don't allow users to respond/redirect more than once per request
      onlyAllowOneResponse(res);

      // Ensure statusCode is set
      // (override `this.statusCode` if `statusCode` argument specified)
      this.statusCode = statusCode || this.statusCode || 200;

      // Ensure charset is set
      this.charset = this.charset || 'utf-8';



      // Modern behavior
      // (builds a complete simulation of an HTTP response.)
      if ( sdk.version === '0.10.0' ) {

        var responseCtx = {
          body: body
        };

        // Allow headers and status code to be disabled to allow for squeezing
        // out a little more performance when relevant (and reducing bandwidth usage).
        // To achieve this, set `sails.config.sockets.sendResponseHeaders=false` and/or
        // `sails.config.sockets.sendStatusCode=false`.
        if ( typeof sails.config.sockets === 'object' ) {
          if (sails.config.sockets.sendResponseHeaders) {
            responseCtx.headers = res.headers;
          }
          if (sails.config.sockets.sendStatusCode) {
            responseCtx.statusCode = res.statusCode;
          }
        }

        // Send down response.
        socketIOCallback(responseCtx);
        return res;
      }

      // Backwards compat. for the 0.9.0 version of the sails.io browser SDK
      // (triggers callback with ONLY the response body)
      else {
        socketIOCallback(body);
        return res;
      }

    });



    /**
     * Redirect to a different url
     *
     * @api public
     */
    res.redirect =
    saveSessionAndThen(req, sails,
    function doRedirect ( /* [location|statusCode], [location|statusCode] */ ) {
      var args    = normalizeResArgs(arguments),
        statusCode  = args.statusCode,
        location  = args.other;

      // Don't allow users to respond/redirect more than once per request
      onlyAllowOneResponse(res);

      // Ensure statusCode is set
      res.statusCode = statusCode || res.statusCode || 302;

      // Prevent redirects to public URLs
      var PUBLIC_URL = /^[^\/].+/;
      if ( location.match( PUBLIC_URL ) ) {
        return emitError( Err.invalidRedirect(location) );
      }

      // Set URL for redirect
      req.url = location;

      // Simulate another request at the new url
      sails.emit('router:request', req, res);
    });



    /**
     * Send json response
     *
     * @api public
     */
    res.json = function sendJSON ( /* [statusCode|obj],[statusCode|obj] */ ) {
      var args    = normalizeResArgs(arguments),
        statusCode  = args.statusCode,
        obj     = args.other;

      // TODO: use configured json replacer
      // TODO: use configured json spaces

      var body = obj;

      // Modern behavior
      // (don't stringify JSON- let socket.io take care of it)
      if ( sdk.version === '0.10.0' ) {
        return this.send(statusCode, body);
      }

      // Backwards compat. for the 0.9.0 version of the sails.io browser SDK
      // (safe-stringifies JSON)
      else {
        body = sails.util.stringify(obj);
        if (!body) {
          return sendError('res.json() :: Error stringifying JSON :: ' + obj, 500);
        }
      }


      // send response
      return this.send(statusCode, body);
    };



    /**
     * There isn't really an equivalent for JSONP over sockets
     * so we can just transparently defer to `res.json()`
     *
     * @api public
     */
    res.jsonp = function sendJSONP ( /* [statusCode|obj],[statusCode|obj] */ ) {
      return this.json.apply(this, arguments);
    };


    /**
     * res.header( headerName [,value] )
     *
     * Backwards compat. for Express 2.x
     * http://expressjs.com/2x/guide.html#res.header()
     *
     * Gets or sets value of OUTGOING response header.
     *
     * @api deprecated
     */
    res.header = function getHeader(headerName, value) {

      // Sets `headerName` to `value`
      if (value) {
        return res.set(headerName, value);
      }

      // `res.header(headerName)`
      // Returns value of `headerName`
      return res.get(headerName);
    };


    /**
     * res.set( headerName, value )
     *
     * @param {[type]} headerName [description]
     * @param {[type]} value   [description]
     */
    res.set = function (headerName, value) {
      res.headers = res.headers || {};
      res.headers[headerName] = value;
      return value;
    };

    /**
     * res.get( headerName )
     *
     * @param  {[type]} headerName [description]
     * @return {[type]}            [description]
     */
    res.get = function (headerName) {
      return res.headers && res.headers[headerName];
    };


    /**
     * http://expressjs.com/api.html#res.render
     * http://expressjs.com/api.html#res.locals
     *
     * TODO: Built-in support for rendering view templates (use `consolidate`)
     * TODO: Built-in support for locals
     * TODO: Built-in support for partials
     * TODO: Built-in support for layouts equivalent to the built-in ejs-locals support for HTTP requests
     *
     * @chainable
     * @api unsupported
     * @todo
     */
    res.render = function renderViewOverSockets (view, options, fn) {
      sendError(
        'You are trying to render a view (' + view + '), ' +
        'but Sails doesn\'t support rendering views over Socket.io... yet!\n' +
        'You might consider serving your HTML view normally, then fetching data with sockets ' +
        'in your client-side JavaScript.\n' +
        'If you didn\'t intend to serve a view here, you might look into content-negotiation\n' +
        'to handle AJAX/socket requests explictly, instead of `res.redirect()`/`res.view()`.'
      );
      return res;
    };


    /**
     * Scoped local variables accesible from views
     * see also: http://expressjs.com/api.html#res.locals
     */
    res.locals = (new function Locals (){
      this.partial = function renderPartial () {
        return sendError('View partials not implemented over socket.io.');
      };
    }());

    /**
     * Get or set the value of a local variable in the view
     *
     * Backwards compat. for Express 2.x
     * http://expressjs.com/2x/guide.html#res.local()
     *
     * @chainable
     * @api deprecated
     */
    res.local = function setLocal (localName, value) {
      // `res.local(localName)`
      // Sets `localName` to `value`
      if (value) {
        res.locals[localName] = value;
        return value;
      }

      // `res.local(localName)`
      // Returns value of `localName`
      return res.locals[localName];
    };


    /**
     * http://expressjs.com/api.html#res.format
     *
     * Performs content-negotiation on the request Accept header field when present.
     * This method uses req.accepted, an array of acceptable types ordered by their quality values,
     * otherwise the first callback is invoked. When no match is performed the server responds with
     * 406 "Not Acceptable", or invokes the default callback.
     *
     * The Content-Type is set for you when a callback is selected, however you may alter this within
     * the callback using res.set() or res.type() etc.
     *
     * @chainable
     * @api unsupported
     */
    res.format = todo('format');


    /**
     * http://expressjs.com/api.html#res.download
     * http://expressjs.com/api.html#res.attachment
     * http://expressjs.com/api.html#res.sendfile
     *
     * Serving files is not part of the short-term roadmap for the socket interpreter.
     *
     * @chainable
     * @api unsupported
     */
    res.download = todo('download');
    res.sendfile = todo('sendfile');
    res.attachment = todo('attachment');


    // TODO: Implement support for other `res.*` methods from Express
    res.contentType = todo('contentType');
    res.type = todo('type');
    res.links = todo('links');
    // res.header = todo('header');
    res.clearCookie = todo('clearCookie');
    res.signedCookie = todo('signedCookie');
    res.cookie = todo('cookie');




    /**
     * Access to underlying socket
     *
     * @api public
     */
    res.socket = socket;


    /**
     * Publish some data to a room
     *
     * @param {String} room
     * @param {Object} data
     *
     * @api public
     */
    res.broadcast = function broadcastMessage (room, data) {
      req.socket.broadcast.to(room).json.send(data);
      return res;
    };


    // Populate req.session using shared session store
    sails.session.fromSocket(req.socket, function sessionReady (err, session) {
      if (err) return cb(err);

      // Provide access to session data as req.session
      req.session = session;

      // Now streamify the things
      req = buildReq(req,res);
      res = buildRes(req,res);

      // Pipe response back to the socket.io callback
      // TODO

      // Set request/response timeout
      // TODO

      // Send newly constructed req and res objects back to router
      cb(null, {
        req: req,
        res: res
      });

    });




    // Respond with a message indicating that the feature is not compatible with sockets
    function notSupportedError() {
      return sendError('Trying to invoke unsupported response method (`res.foo`) in response to a request from socket.io!');
    }

    // Return function which responds with a message indicating that the method
    // is not yet implemented
    function todo (method) {
      return function () {
        return sendError(
          'res.' + method + '() is not yet supported over socket.io.  '+
          'If you need this functionality, please don\'t hesitate to get involved!'
        );
      };
    }

    // Respond with an error message
    function sendError(errmsg, statusCode) {
      sails.log.warn(errmsg);
      res.json( statusCode || 500, {
        error: errmsg
      });
    }

    /**
     * Send a low-level error back over the socket
     * (useful in cases where basic interpreter plumbing is not working)
     *
     * Request callback function will NOT be triggered!
     * Instead, an error message will be emitted.
     */
    function emitError (error) {

      // TODO: implement best practice for socket.io error reporting

      // TODO: something like this..?
      // e.g.
      // socket.emit('sails__500', 'error');

      // ********************************************************
      // WARNING
      //
      // This is a breaking change!!
      // Do not implement until next minor release (0.10.0)
      //
      // Will require documentation of clear steps in changelog
      // and some changes in bundled client-side SDK, i.e.
      // assets/js/sails.io.js
      // -AND-
      // assets/linker/js/sails.io.js
      // ********************************************************

      ////////////////////////////////////
      // But for now:
      ////////////////////////////////////

      // Log error
      sails.log.error(error);

      // If callback is invalid or non-existent:
      if ( !util.isFunction(socketIOCallback) ) {
        // do nothing...
        return;
      }

      // Otherwise just send the error directly to the callback...
      socketIOCallback(error);
    }


    /**
     * NOTE: ALL RESPONSES (INCLUDING REDIRECTS) ARE PREVENTED ONCE THE RESPONSE HAS BEEN SENT!!
     * Even though this is not strictly required with sockets, since res.redirect()
     * is an HTTP-oriented method from Express, it's important to maintain consistency.
     *
     * @api private
     */
    function onlyAllowOneResponse () {
      // TODO
      return;
    }

    /**
     * As long as one of them is a number (i.e. a status code),
     * allows a 2-nary method to be called with flip-flopped arguments:
     *    method( [statusCode|other], [statusCode|other] )
     *
     * This avoids confusing errors & provides Express 2.x backwards compat.
     *
     * E.g. usage in res.send():
     *    var args    = normalizeResArgs.apply(this, arguments),
     *      body    = args.other,
     *      statusCode  = args.statusCode;
     *
     * @api private
     */
    function normalizeResArgs( args ) {

      // Traditional usage:
      // `method( other [,statusCode] )`
      var isTraditionalUsage =
        'number' !== typeof args[0] &&
        ( !args[1] || 'number' === typeof args[1] );

      if ( isTraditionalUsage ) {
        return {
          statusCode: args[1],
          other: args[0]
        };
      }

      // Explicit usage, i.e. Express 3:
      // `method( statusCode [,other] )`
      return {
        statusCode: args[0],
        other: args[1]
      };
    }
  };