You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

server.js 5.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. var app = require('http').createServer(handler)
  2. , sockets = require('./sockets.js')
  3. , log = require("./log.js").log
  4. , path = require('path')
  5. , url = require('url')
  6. , fs = require("fs")
  7. , crypto = require("crypto")
  8. , serveStatic = require("serve-static")
  9. , createSVG = require("./createSVG.js")
  10. , templating = require("./templating.js")
  11. , config = require("./configuration.js")
  12. , polyfillLibrary = require('polyfill-library');
  13. var MIN_NODE_VERSION = 8.0;
  14. if (parseFloat(process.versions.node) < MIN_NODE_VERSION) {
  15. console.warn(
  16. "!!! You are using node " + process.version +
  17. ", wbo requires at least " + MIN_NODE_VERSION + " !!!");
  18. }
  19. var io = sockets.start(app);
  20. app.listen(config.PORT);
  21. log("server started", { port: config.PORT });
  22. var CSP = "default-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:";
  23. var fileserver = serveStatic(config.WEBROOT, {
  24. maxAge: 2 * 3600 * 1000,
  25. setHeaders: function (res) {
  26. res.setHeader("X-UA-Compatible", "IE=Edge");
  27. res.setHeader("Content-Security-Policy", CSP);
  28. }
  29. });
  30. var errorPage = fs.readFileSync(path.join(config.WEBROOT, "error.html"));
  31. function serveError(request, response) {
  32. return function (err) {
  33. log("error", { "error": err && err.toString(), "url": request.url });
  34. response.writeHead(err ? 500 : 404, { "Content-Length": errorPage.length });
  35. response.end(errorPage);
  36. }
  37. }
  38. function logRequest(request) {
  39. log('connection', {
  40. ip: request.connection.remoteAddress,
  41. original_ip: request.headers['x-forwarded-for'] || request.headers['forwarded'],
  42. user_agent: request.headers['user-agent'],
  43. referer: request.headers['referer'],
  44. language: request.headers['accept-language'],
  45. url: request.url,
  46. });
  47. }
  48. function handler(request, response) {
  49. try {
  50. handleRequest(request, response);
  51. } catch (err) {
  52. console.trace(err);
  53. response.writeHead(500, { 'Content-Type': 'text/plain' });
  54. response.end(err.toString());
  55. }
  56. }
  57. const boardTemplate = new templating.BoardTemplate(path.join(config.WEBROOT, 'board.html'));
  58. const indexTemplate = new templating.Template(path.join(config.WEBROOT, 'index.html'));
  59. function validateBoardName(boardName) {
  60. if (/^[\w%\-_~()]*$/.test(boardName)) return boardName;
  61. throw new Error("Illegal board name: " + boardName);
  62. }
  63. function handleRequest(request, response) {
  64. var parsedUrl = url.parse(request.url, true);
  65. var parts = parsedUrl.pathname.split('/');
  66. if (parts[0] === '') parts.shift();
  67. switch (parts[0]) {
  68. case "boards":
  69. // "boards" refers to the root directory
  70. if (parts.length === 1 && parsedUrl.query.board) {
  71. // '/boards?board=...' This allows html forms to point to boards
  72. var headers = { Location: 'boards/' + encodeURIComponent(parsedUrl.query.board) };
  73. response.writeHead(301, headers);
  74. response.end();
  75. } else if (parts.length === 2 && request.url.indexOf('.') === -1) {
  76. validateBoardName(parts[1]);
  77. // If there is no dot and no directory, parts[1] is the board name
  78. boardTemplate.serve(request, response);
  79. } else { // Else, it's a resource
  80. request.url = "/" + parts.slice(1).join('/');
  81. fileserver(request, response, serveError(request, response));
  82. }
  83. break;
  84. case "download":
  85. var boardName = validateBoardName(parts[1]),
  86. history_file = path.join(config.HISTORY_DIR, "board-" + boardName + ".json");
  87. if (parts.length > 2 && /^[0-9A-Za-z.\-]+$/.test(parts[2])) {
  88. history_file += '.' + parts[2] + '.bak';
  89. }
  90. log("download", { "file": history_file });
  91. fs.readFile(history_file, function (err, data) {
  92. if (err) return serveError(request, response)(err);
  93. response.writeHead(200, {
  94. "Content-Type": "application/json",
  95. "Content-Disposition": 'attachment; filename="' + boardName + '.wbo"',
  96. "Content-Length": data.length,
  97. });
  98. response.end(data);
  99. });
  100. break;
  101. case "export":
  102. case "preview":
  103. var boardName = validateBoardName(parts[1]),
  104. history_file = path.join(config.HISTORY_DIR, "board-" + boardName + ".json");
  105. response.writeHead(200, {
  106. "Content-Type": "image/svg+xml",
  107. "Content-Security-Policy": CSP,
  108. "Cache-Control": "public, max-age=30",
  109. });
  110. var t = Date.now();
  111. createSVG.renderBoard(history_file, response).then(function () {
  112. log("preview", { "board": boardName, "time": Date.now() - t });
  113. response.end();
  114. }).catch(function (err) {
  115. log("error", { "error": err.toString() });
  116. response.end('<text>Sorry, an error occured</text>');
  117. });
  118. break;
  119. case "random":
  120. var name = crypto.randomBytes(32).toString('base64').replace(/[^\w]/g, '-');
  121. response.writeHead(307, { 'Location': 'boards/' + name });
  122. response.end(name);
  123. break;
  124. case "polyfill.js": // serve tailored polyfills
  125. case "polyfill.min.js":
  126. polyfillLibrary.getPolyfillString({
  127. uaString: request.headers['user-agent'],
  128. minify: request.url.endsWith(".min.js"),
  129. features: {
  130. 'default': { flags: ['gated'] },
  131. 'es5': { flags: ['gated'] },
  132. 'es6': { flags: ['gated'] },
  133. 'es7': { flags: ['gated'] },
  134. 'performance.now': { flags: ['gated'] },
  135. }
  136. }).then(function (bundleString) {
  137. response.setHeader('Cache-Control', 'public, max-age=172800, stale-while-revalidate=1728000');
  138. response.setHeader('Vary', 'User-Agent');
  139. response.setHeader('Content-Type', 'application/javascript');
  140. response.end(bundleString);
  141. });
  142. break;
  143. case "": // Index page
  144. logRequest(request);
  145. indexTemplate.serve(request, response);
  146. break;
  147. default:
  148. fileserver(request, response, serveError(request, response));
  149. }
  150. }
  151. module.exports = app;