server.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import fs from 'fs';
  2. import http from 'http';
  3. import https from 'https';
  4. import events from 'events';
  5. import {parse} from 'url';
  6. import Client from './client';
  7. import config from '../package.json';
  8. import anybody from 'body/any';
  9. import qs from 'qs';
  10. const debug = require('debug')('tinylr:server');
  11. const CONTENT_TYPE = 'content-type';
  12. const FORM_TYPE = 'application/x-www-form-urlencoded';
  13. function buildRootPath (prefix = '/') {
  14. let rootUrl = prefix;
  15. // Add trailing slash
  16. if (prefix[prefix.length - 1] !== '/') {
  17. rootUrl = `${rootUrl}/`;
  18. }
  19. // Add leading slash
  20. if (prefix[0] !== '/') {
  21. rootUrl = `/${rootUrl}`;
  22. }
  23. return rootUrl;
  24. }
  25. class Server extends events.EventEmitter {
  26. constructor (options = {}) {
  27. super();
  28. this.options = options;
  29. options.livereload = options.livereload || require.resolve('livereload-js/dist/livereload.js');
  30. // todo: change falsy check to allow 0 for random port
  31. options.port = parseInt(options.port || 35729, 10);
  32. if (options.errorListener) {
  33. this.errorListener = options.errorListener;
  34. }
  35. this.rootPath = buildRootPath(options.prefix);
  36. this.clients = {};
  37. this.configure(options.app);
  38. this.routes(options.app);
  39. }
  40. routes () {
  41. if (!this.options.dashboard) {
  42. this.on(`GET ${this.rootPath}`, this.index.bind(this));
  43. }
  44. this.on(`GET ${this.rootPath}changed`, this.changed.bind(this));
  45. this.on(`POST ${this.rootPath}changed`, this.changed.bind(this));
  46. this.on(`POST ${this.rootPath}alert`, this.alert.bind(this));
  47. this.on(`GET ${this.rootPath}livereload.js`, this.livereload.bind(this));
  48. this.on(`GET ${this.rootPath}kill`, this.close.bind(this));
  49. }
  50. configure (app) {
  51. debug('Configuring %s', app ? 'connect / express application' : 'HTTP server');
  52. let handler = this.options.handler || this.handler;
  53. if (!app) {
  54. if ((this.options.key && this.options.cert) || this.options.pfx) {
  55. this.server = https.createServer(this.options, handler.bind(this));
  56. } else {
  57. this.server = http.createServer(handler.bind(this));
  58. }
  59. this.server.on('upgrade', this.websocketify.bind(this));
  60. this.server.on('error', this.error.bind(this));
  61. return this;
  62. }
  63. this.app = app;
  64. this.app.listen = (port, done) => {
  65. done = done || function () {};
  66. if (port !== this.options.port) {
  67. debug('Warn: LiveReload port is not standard (%d). You are listening on %d', this.options.port, port);
  68. debug('You\'ll need to rely on the LiveReload snippet');
  69. debug('> http://feedback.livereload.com/knowledgebase/articles/86180-how-do-i-add-the-script-tag-manually-');
  70. }
  71. let srv = this.server = http.createServer(app);
  72. srv.on('upgrade', this.websocketify.bind(this));
  73. srv.on('error', this.error.bind(this));
  74. srv.on('close', this.close.bind(this));
  75. return srv.listen(port, done);
  76. };
  77. return this;
  78. }
  79. handler (req, res, next) {
  80. let middleware = typeof next === 'function';
  81. debug('LiveReload handler %s (middleware: %s)', req.url, middleware ? 'on' : 'off');
  82. next = next || this.defaultHandler.bind(this, res);
  83. req.headers[CONTENT_TYPE] = req.headers[CONTENT_TYPE] || FORM_TYPE;
  84. return anybody(req, res, (err, body) => {
  85. if (err) return next(err);
  86. req.body = body;
  87. if (!req.query) {
  88. req.query = req.url.indexOf('?') !== -1
  89. ? qs.parse(parse(req.url).query)
  90. : {};
  91. }
  92. return this.handle(req, res, next);
  93. });
  94. }
  95. index (req, res) {
  96. res.setHeader('Content-Type', 'application/json');
  97. res.write(JSON.stringify({
  98. tinylr: 'Welcome',
  99. version: config.version
  100. }));
  101. res.end();
  102. }
  103. handle (req, res, next) {
  104. let url = parse(req.url);
  105. debug('Request:', req.method, url.href);
  106. let middleware = typeof next === 'function';
  107. // do the routing
  108. let route = req.method + ' ' + url.pathname;
  109. let respond = this.emit(route, req, res);
  110. if (respond) return;
  111. if (middleware) return next();
  112. // Only apply content-type on non middleware setup #70
  113. return this.notFound(res);
  114. }
  115. defaultHandler (res, err) {
  116. if (!err) return this.notFound(res);
  117. this.error(err);
  118. res.setHeader('Content-Type', 'text/plain');
  119. res.statusCode = 500;
  120. res.end('Error: ' + err.stack);
  121. }
  122. notFound (res) {
  123. res.setHeader('Content-Type', 'application/json');
  124. res.writeHead(404);
  125. res.write(JSON.stringify({
  126. error: 'not_found',
  127. reason: 'no such route'
  128. }));
  129. res.end();
  130. }
  131. websocketify (req, socket, head) {
  132. let client = new Client(req, socket, head, this.options);
  133. this.clients[client.id] = client;
  134. // handle socket error to prevent possible app crash, such as ECONNRESET
  135. socket.on('error', (e) => {
  136. // ignore frequent ECONNRESET error (seems inevitable when refresh)
  137. if (e.code === 'ECONNRESET') return;
  138. this.error(e);
  139. });
  140. client.once('info', (data) => {
  141. debug('Create client %s (url: %s)', data.id, data.url);
  142. this.emit('MSG /create', data.id, data.url);
  143. });
  144. client.once('end', () => {
  145. debug('Destroy client %s (url: %s)', client.id, client.url);
  146. this.emit('MSG /destroy', client.id, client.url);
  147. delete this.clients[client.id];
  148. });
  149. }
  150. listen (port, host, fn) {
  151. port = port || this.options.port;
  152. // Last used port for error display
  153. this.port = port;
  154. if (typeof host === 'function') {
  155. fn = host;
  156. host = undefined;
  157. }
  158. this.server.listen(port, host, fn);
  159. }
  160. close (req, res) {
  161. Object.keys(this.clients).forEach(function (id) {
  162. this.clients[id].close();
  163. }, this);
  164. if (this.server._handle) this.server.close(this.emit.bind(this, 'close'));
  165. if (res) res.end();
  166. }
  167. error (e) {
  168. if (this.errorListener) {
  169. this.errorListener(e);
  170. return;
  171. }
  172. console.error();
  173. if (typeof e === 'undefined') {
  174. console.error('... Uhoh. Got error %s ...', e);
  175. } else {
  176. console.error('... Uhoh. Got error %s ...', e.message);
  177. console.error(e.stack);
  178. if (e.code !== 'EADDRINUSE') return;
  179. console.error();
  180. console.error('You already have a server listening on %s', this.port);
  181. console.error('You should stop it and try again.');
  182. console.error();
  183. }
  184. }
  185. // Routes
  186. livereload (req, res) {
  187. res.setHeader('Content-Type', 'application/javascript');
  188. fs.createReadStream(this.options.livereload).pipe(res);
  189. }
  190. changed (req, res) {
  191. let files = this.param('files', req);
  192. debug('Changed event (Files: %s)', files.join(' '));
  193. let clients = this.notifyClients(files);
  194. if (!res) return;
  195. res.setHeader('Content-Type', 'application/json');
  196. res.write(JSON.stringify({
  197. clients: clients,
  198. files: files
  199. }));
  200. res.end();
  201. }
  202. alert (req, res) {
  203. let message = this.param('message', req);
  204. debug('Alert event (Message: %s)', message);
  205. let clients = this.alertClients(message);
  206. if (!res) return;
  207. res.setHeader('Content-Type', 'application/json');
  208. res.write(JSON.stringify({
  209. clients: clients,
  210. message: message
  211. }));
  212. res.end();
  213. }
  214. notifyClients (files) {
  215. let clients = Object.keys(this.clients).map(function (id) {
  216. let client = this.clients[id];
  217. debug('Reloading client %s (url: %s)', client.id, client.url);
  218. client.reload(files);
  219. return {
  220. id: client.id,
  221. url: client.url
  222. };
  223. }, this);
  224. return clients;
  225. };
  226. alertClients (message) {
  227. let clients = Object.keys(this.clients).map(function (id) {
  228. let client = this.clients[id];
  229. debug('Alert client %s (url: %s)', client.id, client.url);
  230. client.alert(message);
  231. return {
  232. id: client.id,
  233. url: client.url
  234. };
  235. }, this);
  236. return clients;
  237. }
  238. // Lookup param from body / params / query.
  239. param (name, req) {
  240. let param;
  241. if (req.body && req.body[name]) param = req.body[name];
  242. else if (req.params && req.params[name]) param = req.params[name];
  243. else if (req.query && req.query[name]) param = req.query[name];
  244. // normalize files array
  245. if (name === 'files') {
  246. param = Array.isArray(param) ? param
  247. : typeof param === 'string' ? param.split(/[\s,]/)
  248. : [];
  249. }
  250. return param;
  251. }
  252. }
  253. export default Server;