UNPKG

faye

Version:

Simple pub/sub messaging for the web

314 lines (245 loc) 10.4 kB
'use strict'; var Buffer = require('safe-buffer').Buffer, path = require('path'), querystring = require('querystring'), url = require('url'), WebSocket = require('faye-websocket'), EventSource = WebSocket.EventSource; var constants = require('../util/constants'), assign = require('../util/assign'), idFromMessages = require('../util/id_from_messages'), toJSON = require('../util/to_json'), validateOptions = require('../util/validate_options'), Class = require('../util/class'), Logging = require('../mixins/logging'), Publisher = require('../mixins/publisher'), Client = require('../protocol/client'), Server = require('../protocol/server'), contenttypes = require('./content_types'), StaticServer = require('./static_server'); var NodeAdapter = Class({ className: 'NodeAdapter', DEFAULT_ENDPOINT: '/bayeux', SCRIPT_PATH: 'faye-browser-min.js', VALID_JSONP_CALLBACK: /^[a-z_\$][a-z0-9_\$]*(\.[a-z_\$][a-z0-9_\$]*)*$/i, initialize: function(options) { this._options = options || {}; validateOptions(this._options, ['engine', 'mount', 'ping', 'timeout', 'extensions', 'websocketExtensions']); this._extensions = []; this._endpoint = this._options.mount || this.DEFAULT_ENDPOINT; this._endpointRe = new RegExp('^' + this._endpoint.replace(/\/$/, '') + '(/[^/]*)*(\\.[^\\.]+)?$'); this._server = Server.create(this._options); this._static = new StaticServer(path.join(__dirname, '..', '..', 'client'), /\.(?:js|map)$/); this._static.map(path.basename(this._endpoint) + '.js', this.SCRIPT_PATH); this._static.map('client.js', this.SCRIPT_PATH); var extensions = this._options.extensions, websocketExtensions = this._options.websocketExtensions, i, n; if (extensions) { extensions = [].concat(extensions); for (i = 0, n = extensions.length; i < n; i++) this.addExtension(extensions[i]); } if (websocketExtensions) { websocketExtensions = [].concat(websocketExtensions); for (i = 0, n = websocketExtensions.length; i < n; i++) this.addWebsocketExtension(websocketExtensions[i]); } }, listen: function() { throw new Error('The listen() method is deprecated - use the attach() method to bind Faye to an http.Server'); }, addExtension: function(extension) { return this._server.addExtension(extension); }, removeExtension: function(extension) { return this._server.removeExtension(extension); }, addWebsocketExtension: function(extension) { this._extensions.push(extension); }, close: function() { return this._server.close(); }, getClient: function() { return this._client = this._client || new Client(this._server); }, attach: function(httpServer) { this._overrideListeners(httpServer, 'request', 'handle'); this._overrideListeners(httpServer, 'upgrade', 'handleUpgrade'); }, _overrideListeners: function(httpServer, event, method) { var listeners = httpServer.listeners(event), self = this; httpServer.removeAllListeners(event); httpServer.on(event, function(request) { if (self.check(request)) return self[method].apply(self, arguments); for (var i = 0, n = listeners.length; i < n; i++) listeners[i].apply(this, arguments); }); }, check: function(request) { var path = url.parse(request.url, true).pathname; return !!this._endpointRe.test(path); }, handle: function(request, response) { var requestUrl = url.parse(request.url, true), requestMethod = request.method, self = this; request.originalUrl = request.url; request.on('error', function(error) { self._returnError(response, error) }); response.on('error', function(error) { self._returnError(null, error) }); if (this._static.test(requestUrl.pathname)) return this._static.call(request, response); // http://groups.google.com/group/faye-users/browse_thread/thread/4a01bb7d25d3636a if (requestMethod === 'OPTIONS' || request.headers['access-control-request-method'] === 'POST') return this._handleOptions(request, response); if (EventSource.isEventSource(request)) return this.handleEventSource(request, response); if (requestMethod === 'GET') return this._callWithParams(request, response, requestUrl.query); if (requestMethod === 'POST') return this._concatStream(request, function(data) { var type = (request.headers['content-type'] || '').split(';')[0], params = (type === 'application/json') ? { message: data } : querystring.parse(data); request.body = data; this._callWithParams(request, response, params); }, this); this._returnError(response, { message: 'Unrecognized request type' }); }, _callWithParams: function(request, response, params) { if (!params.message) return this._returnError(response, { message: 'Received request with no message: ' + this._formatRequest(request) }); try { this.debug('Received message via HTTP ' + request.method + ': ?', params.message); var message = this._parseJSON(params.message), jsonp = params.jsonp || constants.JSONP_CALLBACK, isGet = (request.method === 'GET'), type = isGet ? contenttypes.TYPE_SCRIPT : contenttypes.TYPE_JSON, headers = assign({}, type), origin = request.headers.origin; if (!this.VALID_JSONP_CALLBACK.test(jsonp)) return this._returnError(response, { message: 'Invalid JSON-P callback: ' + jsonp }); headers['Cache-Control'] = 'no-cache, no-store'; headers['X-Content-Type-Options'] = 'nosniff'; if (origin) { headers['Access-Control-Allow-Credentials'] = 'true'; headers['Access-Control-Allow-Origin'] = origin; } this._server.process(message, request, function(replies) { var body = toJSON(replies); if (isGet) { body = '/**/' + jsonp + '(' + this._jsonpEscape(body) + ');'; headers['Content-Disposition'] = 'attachment; filename=f.txt'; } headers['Content-Length'] = Buffer.from(body, 'utf8').length.toString(); this.debug('HTTP response: ?', body); response.writeHead(200, headers); response.end(body); }, this); } catch (error) { this._returnError(response, error); } }, _jsonpEscape: function(json) { return json.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); }, handleUpgrade: function(request, socket, head) { var options = { extensions: this._extensions, ping: this._options.ping }, ws = new WebSocket(request, socket, head, [], options), clientId = null, self = this; request.originalUrl = request.url; ws.onmessage = function(event) { try { self.debug('Received message via WebSocket[' + ws.version + ']: ?', event.data); var message = self._parseJSON(event.data), cid = idFromMessages(message); if (clientId && cid && cid !== clientId) self._server.closeSocket(clientId, false); self._server.openSocket(cid, ws, request); if (cid) clientId = cid; self._server.process(message, request, function(replies) { if (ws) ws.send(toJSON(replies)); }); } catch (error) { console.log(error.stack); self.error(error.message + '\nBacktrace:\n' + error.stack); } }; ws.onclose = function(event) { self._server.closeSocket(clientId); ws = null; }; }, handleEventSource: function(request, response) { var es = new EventSource(request, response, { ping: this._options.ping }), clientId = es.url.split('/').pop(), self = this; this.debug('Opened EventSource connection for ?', clientId); this._server.openSocket(clientId, es, request); es.onclose = function(event) { self._server.closeSocket(clientId); es = null; }; }, _handleOptions: function(request, response) { var origin = request.headers.origin || request.headers.referer; var headers = { 'Access-Control-Allow-Credentials': 'true', 'Access-Control-Allow-Headers': 'Accept, Authorization, Content-Type, Pragma, X-Requested-With', 'Access-Control-Allow-Methods': 'POST, GET', 'Access-Control-Allow-Origin': origin || '*', 'Access-Control-Max-Age': '86400' }; response.writeHead(200, headers); response.end(''); }, _concatStream: function(stream, callback, context) { var chunks = [], length = 0; stream.on('data', function(chunk) { chunks.push(chunk); length += chunk.length; }); stream.on('end', function() { var buffer = Buffer.alloc(length), offset = 0; for (var i = 0, n = chunks.length; i < n; i++) { chunks[i].copy(buffer, offset); offset += chunks[i].length; } callback.call(context, buffer.toString('utf8')); }); }, _parseJSON: function(json) { var data = JSON.parse(json); if (typeof data === 'object') return data; throw new SyntaxError('JSON messages must contain an object or array'); }, _formatRequest: function(request) { var method = request.method.toUpperCase(), string = 'curl -X ' + method; string += " 'http://" + request.headers.host + request.url + "'"; if (method === 'POST') { string += " -H 'Content-Type: " + request.headers['content-type'] + "'"; string += " -d '" + request.body + "'"; } return string; }, _returnError: function(response, error) { var message = error.message; if (error.stack) message += '\nBacktrace:\n' + error.stack; this.error(message); if (!response) return; response.writeHead(400, contenttypes.TYPE_TEXT); response.end('Bad request'); } }); for (var method in Publisher) (function(method) { NodeAdapter.prototype[method] = function() { return this._server._engine[method].apply(this._server._engine, arguments); }; })(method); assign(NodeAdapter.prototype, Logging); module.exports = NodeAdapter;