您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

server.js 6.6KB

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