index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. /**
  2. * Module dependencies.
  3. */
  4. var http = require('http');
  5. var read = require('fs').readFileSync;
  6. var engine = require('engine.io');
  7. var client = require('socket.io-client');
  8. var clientVersion = require('socket.io-client/package').version;
  9. var Client = require('./client');
  10. var Emitter = require('events').EventEmitter;
  11. var Namespace = require('./namespace');
  12. var Adapter = require('socket.io-adapter');
  13. var debug = require('debug')('socket.io:server');
  14. var url = require('url');
  15. /**
  16. * Module exports.
  17. */
  18. module.exports = Server;
  19. /**
  20. * Socket.IO client source.
  21. */
  22. var clientSource = undefined;
  23. var clientSourceMap = undefined;
  24. /**
  25. * Server constructor.
  26. *
  27. * @param {http.Server|Number|Object} srv http server, port or options
  28. * @param {Object} [opts]
  29. * @api public
  30. */
  31. function Server(srv, opts){
  32. if (!(this instanceof Server)) return new Server(srv, opts);
  33. if ('object' == typeof srv && !srv.listen) {
  34. opts = srv;
  35. srv = null;
  36. }
  37. opts = opts || {};
  38. this.nsps = {};
  39. this.path(opts.path || '/socket.io');
  40. this.serveClient(false !== opts.serveClient);
  41. this.adapter(opts.adapter || Adapter);
  42. this.origins(opts.origins || '*:*');
  43. this.sockets = this.of('/');
  44. if (srv) this.attach(srv, opts);
  45. }
  46. /**
  47. * Server request verification function, that checks for allowed origins
  48. *
  49. * @param {http.IncomingMessage} req request
  50. * @param {Function} fn callback to be called with the result: `fn(err, success)`
  51. */
  52. Server.prototype.checkRequest = function(req, fn) {
  53. var origin = req.headers.origin || req.headers.referer;
  54. // file:// URLs produce a null Origin which can't be authorized via echo-back
  55. if ('null' == origin || null == origin) origin = '*';
  56. if (!!origin && typeof(this._origins) == 'function') return this._origins(origin, fn);
  57. if (this._origins.indexOf('*:*') !== -1) return fn(null, true);
  58. if (origin) {
  59. try {
  60. var parts = url.parse(origin);
  61. var defaultPort = 'https:' == parts.protocol ? 443 : 80;
  62. parts.port = parts.port != null
  63. ? parts.port
  64. : defaultPort;
  65. var ok =
  66. ~this._origins.indexOf(parts.hostname + ':' + parts.port) ||
  67. ~this._origins.indexOf(parts.hostname + ':*') ||
  68. ~this._origins.indexOf('*:' + parts.port);
  69. return fn(null, !!ok);
  70. } catch (ex) {
  71. }
  72. }
  73. fn(null, false);
  74. };
  75. /**
  76. * Sets/gets whether client code is being served.
  77. *
  78. * @param {Boolean} v whether to serve client code
  79. * @return {Server|Boolean} self when setting or value when getting
  80. * @api public
  81. */
  82. Server.prototype.serveClient = function(v){
  83. if (!arguments.length) return this._serveClient;
  84. this._serveClient = v;
  85. if (v && !clientSource) {
  86. clientSource = read(require.resolve('socket.io-client/dist/socket.io.min.js'), 'utf-8');
  87. try {
  88. clientSourceMap = read(require.resolve('socket.io-client/dist/socket.io.js.map'), 'utf-8');
  89. } catch(err) {
  90. debug('could not load sourcemap file');
  91. }
  92. }
  93. return this;
  94. };
  95. /**
  96. * Old settings for backwards compatibility
  97. */
  98. var oldSettings = {
  99. "transports": "transports",
  100. "heartbeat timeout": "pingTimeout",
  101. "heartbeat interval": "pingInterval",
  102. "destroy buffer size": "maxHttpBufferSize"
  103. };
  104. /**
  105. * Backwards compatibility.
  106. *
  107. * @api public
  108. */
  109. Server.prototype.set = function(key, val){
  110. if ('authorization' == key && val) {
  111. this.use(function(socket, next) {
  112. val(socket.request, function(err, authorized) {
  113. if (err) return next(new Error(err));
  114. if (!authorized) return next(new Error('Not authorized'));
  115. next();
  116. });
  117. });
  118. } else if ('origins' == key && val) {
  119. this.origins(val);
  120. } else if ('resource' == key) {
  121. this.path(val);
  122. } else if (oldSettings[key] && this.eio[oldSettings[key]]) {
  123. this.eio[oldSettings[key]] = val;
  124. } else {
  125. console.error('Option %s is not valid. Please refer to the README.', key);
  126. }
  127. return this;
  128. };
  129. /**
  130. * Sets the client serving path.
  131. *
  132. * @param {String} v pathname
  133. * @return {Server|String} self when setting or value when getting
  134. * @api public
  135. */
  136. Server.prototype.path = function(v){
  137. if (!arguments.length) return this._path;
  138. this._path = v.replace(/\/$/, '');
  139. return this;
  140. };
  141. /**
  142. * Sets the adapter for rooms.
  143. *
  144. * @param {Adapter} v pathname
  145. * @return {Server|Adapter} self when setting or value when getting
  146. * @api public
  147. */
  148. Server.prototype.adapter = function(v){
  149. if (!arguments.length) return this._adapter;
  150. this._adapter = v;
  151. for (var i in this.nsps) {
  152. if (this.nsps.hasOwnProperty(i)) {
  153. this.nsps[i].initAdapter();
  154. }
  155. }
  156. return this;
  157. };
  158. /**
  159. * Sets the allowed origins for requests.
  160. *
  161. * @param {String} v origins
  162. * @return {Server|Adapter} self when setting or value when getting
  163. * @api public
  164. */
  165. Server.prototype.origins = function(v){
  166. if (!arguments.length) return this._origins;
  167. this._origins = v;
  168. return this;
  169. };
  170. /**
  171. * Attaches socket.io to a server or port.
  172. *
  173. * @param {http.Server|Number} server or port
  174. * @param {Object} options passed to engine.io
  175. * @return {Server} self
  176. * @api public
  177. */
  178. Server.prototype.listen =
  179. Server.prototype.attach = function(srv, opts){
  180. if ('function' == typeof srv) {
  181. var msg = 'You are trying to attach socket.io to an express ' +
  182. 'request handler function. Please pass a http.Server instance.';
  183. throw new Error(msg);
  184. }
  185. // handle a port as a string
  186. if (Number(srv) == srv) {
  187. srv = Number(srv);
  188. }
  189. if ('number' == typeof srv) {
  190. debug('creating http server and binding to %d', srv);
  191. var port = srv;
  192. srv = http.Server(function(req, res){
  193. res.writeHead(404);
  194. res.end();
  195. });
  196. srv.listen(port);
  197. }
  198. // set engine.io path to `/socket.io`
  199. opts = opts || {};
  200. opts.path = opts.path || this.path();
  201. // set origins verification
  202. opts.allowRequest = opts.allowRequest || this.checkRequest.bind(this);
  203. // initialize engine
  204. debug('creating engine.io instance with opts %j', opts);
  205. this.eio = engine.attach(srv, opts);
  206. // attach static file serving
  207. if (this._serveClient) this.attachServe(srv);
  208. // Export http server
  209. this.httpServer = srv;
  210. // bind to engine events
  211. this.bind(this.eio);
  212. return this;
  213. };
  214. /**
  215. * Attaches the static file serving.
  216. *
  217. * @param {Function|http.Server} srv http server
  218. * @api private
  219. */
  220. Server.prototype.attachServe = function(srv){
  221. debug('attaching client serving req handler');
  222. var url = this._path + '/socket.io.js';
  223. var urlMap = this._path + '/socket.io.js.map';
  224. var evs = srv.listeners('request').slice(0);
  225. var self = this;
  226. srv.removeAllListeners('request');
  227. srv.on('request', function(req, res) {
  228. if (0 === req.url.indexOf(urlMap)) {
  229. self.serveMap(req, res);
  230. } else if (0 === req.url.indexOf(url)) {
  231. self.serve(req, res);
  232. } else {
  233. for (var i = 0; i < evs.length; i++) {
  234. evs[i].call(srv, req, res);
  235. }
  236. }
  237. });
  238. };
  239. /**
  240. * Handles a request serving `/socket.io.js`
  241. *
  242. * @param {http.Request} req
  243. * @param {http.Response} res
  244. * @api private
  245. */
  246. Server.prototype.serve = function(req, res){
  247. // Per the standard, ETags must be quoted:
  248. // https://tools.ietf.org/html/rfc7232#section-2.3
  249. var expectedEtag = '"' + clientVersion + '"';
  250. var etag = req.headers['if-none-match'];
  251. if (etag) {
  252. if (expectedEtag == etag) {
  253. debug('serve client 304');
  254. res.writeHead(304);
  255. res.end();
  256. return;
  257. }
  258. }
  259. debug('serve client source');
  260. res.setHeader('Content-Type', 'application/javascript');
  261. res.setHeader('ETag', expectedEtag);
  262. res.setHeader('X-SourceMap', 'socket.io.js.map');
  263. res.writeHead(200);
  264. res.end(clientSource);
  265. };
  266. /**
  267. * Handles a request serving `/socket.io.js.map`
  268. *
  269. * @param {http.Request} req
  270. * @param {http.Response} res
  271. * @api private
  272. */
  273. Server.prototype.serveMap = function(req, res){
  274. // Per the standard, ETags must be quoted:
  275. // https://tools.ietf.org/html/rfc7232#section-2.3
  276. var expectedEtag = '"' + clientVersion + '"';
  277. var etag = req.headers['if-none-match'];
  278. if (etag) {
  279. if (expectedEtag == etag) {
  280. debug('serve client 304');
  281. res.writeHead(304);
  282. res.end();
  283. return;
  284. }
  285. }
  286. debug('serve client sourcemap');
  287. res.setHeader('Content-Type', 'application/json');
  288. res.setHeader('ETag', expectedEtag);
  289. res.writeHead(200);
  290. res.end(clientSourceMap);
  291. };
  292. /**
  293. * Binds socket.io to an engine.io instance.
  294. *
  295. * @param {engine.Server} engine engine.io (or compatible) server
  296. * @return {Server} self
  297. * @api public
  298. */
  299. Server.prototype.bind = function(engine){
  300. this.engine = engine;
  301. this.engine.on('connection', this.onconnection.bind(this));
  302. return this;
  303. };
  304. /**
  305. * Called with each incoming transport connection.
  306. *
  307. * @param {engine.Socket} conn
  308. * @return {Server} self
  309. * @api public
  310. */
  311. Server.prototype.onconnection = function(conn){
  312. debug('incoming connection with id %s', conn.id);
  313. var client = new Client(this, conn);
  314. client.connect('/');
  315. return this;
  316. };
  317. /**
  318. * Looks up a namespace.
  319. *
  320. * @param {String} name nsp name
  321. * @param {Function} [fn] optional, nsp `connection` ev handler
  322. * @api public
  323. */
  324. Server.prototype.of = function(name, fn){
  325. if (String(name)[0] !== '/') name = '/' + name;
  326. var nsp = this.nsps[name];
  327. if (!nsp) {
  328. debug('initializing namespace %s', name);
  329. nsp = new Namespace(this, name);
  330. this.nsps[name] = nsp;
  331. }
  332. if (fn) nsp.on('connect', fn);
  333. return nsp;
  334. };
  335. /**
  336. * Closes server connection
  337. *
  338. * @param {Function} [fn] optional, called as `fn([err])` on error OR all conns closed
  339. * @api public
  340. */
  341. Server.prototype.close = function(fn){
  342. for (var id in this.nsps['/'].sockets) {
  343. if (this.nsps['/'].sockets.hasOwnProperty(id)) {
  344. this.nsps['/'].sockets[id].onclose();
  345. }
  346. }
  347. this.engine.close();
  348. if (this.httpServer) {
  349. this.httpServer.close(fn);
  350. } else {
  351. fn && fn();
  352. }
  353. };
  354. /**
  355. * Expose main namespace (/).
  356. */
  357. var emitterMethods = Object.keys(Emitter.prototype).filter(function(key){
  358. return typeof Emitter.prototype[key] === 'function';
  359. });
  360. emitterMethods.concat(['to', 'in', 'use', 'send', 'write', 'clients', 'compress']).forEach(function(fn){
  361. Server.prototype[fn] = function(){
  362. return this.sockets[fn].apply(this.sockets, arguments);
  363. };
  364. });
  365. Namespace.flags.forEach(function(flag){
  366. Object.defineProperty(Server.prototype, flag, {
  367. get: function() {
  368. this.sockets.flags = this.sockets.flags || {};
  369. this.sockets.flags[flag] = true;
  370. return this;
  371. }
  372. });
  373. });
  374. /**
  375. * BC with `io.listen`
  376. */
  377. Server.listen = Server;