Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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