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 6.7KB

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