http.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. const Koa = require("koa");
  2. const koaStatic = require("koa-static");
  3. const path = require("path");
  4. const mount = require("koa-mount");
  5. /**
  6. * @type function
  7. * @param {{ host: string, port: number, middleware: any[], allowHost: string | null }} input
  8. * @return function
  9. */
  10. module.exports = ({ host, port, middleware, allowHost }) => {
  11. const assets = new Koa();
  12. assets.use(koaStatic(path.join(__dirname, "assets")));
  13. const app = new Koa();
  14. const validHosts = [];
  15. // All non-GET requests must have a path that doesn't start with `/blob/`.
  16. const isValidRequest = (request) => {
  17. // All requests must use our hostname to prevent DNS rebind attacks.
  18. if (validHosts.includes(request.hostname) !== true) {
  19. console.log(`Invalid HTTP hostname: ${request.hostname}`);
  20. return false;
  21. }
  22. // All non-GET requests must ...
  23. if (request.method !== "GET") {
  24. // ...have a referer...
  25. if (request.header.referer == null) {
  26. console.log("No referer");
  27. return false;
  28. }
  29. try {
  30. const refererUrl = new URL(request.header.referer);
  31. // ...with a valid hostname...
  32. if (validHosts.includes(refererUrl.hostname) !== true) {
  33. console.log(`Invalid referer hostname: ${refererUrl.hostname}`);
  34. return false;
  35. }
  36. // ...and must not originate from a blob path.
  37. if (refererUrl.pathname.startsWith("/blob/")) {
  38. console.log(`Invalid referer path: ${refererUrl.pathname}`);
  39. return false;
  40. }
  41. } catch (e) {
  42. console.log(`Invalid referer URL: ${request.header.referer}`);
  43. return false;
  44. }
  45. }
  46. // If all of the above checks pass, this is a valid request.
  47. return true;
  48. };
  49. app.on("error", (err, ctx) => {
  50. // Output full error objects
  51. console.error(err);
  52. // Avoid printing errors for invalid requests.
  53. if (isValidRequest(ctx.request)) {
  54. err.message = err.stack;
  55. err.expose = true;
  56. }
  57. return null;
  58. });
  59. app.use(mount("/assets", assets));
  60. // headers
  61. app.use(async (ctx, next) => {
  62. const csp = [
  63. "default-src 'none'",
  64. "img-src 'self'",
  65. "form-action 'self'",
  66. "media-src 'self'",
  67. "style-src 'self'",
  68. ].join("; ");
  69. // Disallow scripts.
  70. ctx.set("Content-Security-Policy", csp);
  71. // Disallow <iframe> embeds from other domains.
  72. ctx.set("X-Frame-Options", "SAMEORIGIN");
  73. const isBlobPath = ctx.path.startsWith("/blob/");
  74. if (isBlobPath === false) {
  75. // Disallow browsers overwriting declared media types.
  76. //
  77. // This should only happen on non-blob URLs.
  78. ctx.set("X-Content-Type-Options", "nosniff");
  79. }
  80. // Disallow sharing referrer with other domains.
  81. ctx.set("Referrer-Policy", "same-origin");
  82. // Disallow extra browser features except audio output.
  83. ctx.set("Feature-Policy", "speaker 'self'");
  84. const validHostsString = validHosts.join(" or ");
  85. ctx.assert(
  86. isValidRequest(ctx.request),
  87. 400,
  88. `Request must be addressed to ${validHostsString} and non-GET requests must contain non-blob referer.`
  89. );
  90. await next();
  91. });
  92. middleware.forEach((m) => app.use(m));
  93. const server = app.listen({ host, port });
  94. server.on("listening", () => {
  95. const address = server.address();
  96. if (typeof address === "string") {
  97. // This shouldn't happen, but TypeScript was complaining about it.
  98. throw new Error("HTTP server should never bind to Unix socket");
  99. }
  100. if (allowHost !== null) {
  101. validHosts.push(allowHost);
  102. }
  103. validHosts.push(address.address);
  104. if (validHosts.includes(host) === false) {
  105. validHosts.push(host);
  106. }
  107. });
  108. return server;
  109. };