123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- const Koa = require("koa");
- const koaStatic = require("koa-static");
- const path = require("path");
- const mount = require("koa-mount");
- /**
- * @type function
- * @param {{ host: string, port: number, middleware: any[], allowHost: string | null }} input
- * @return function
- */
- module.exports = ({ host, port, middleware, allowHost }) => {
- const assets = new Koa();
- assets.use(koaStatic(path.join(__dirname, "assets")));
- const app = new Koa();
- const validHosts = [];
- // All non-GET requests must have a path that doesn't start with `/blob/`.
- const isValidRequest = (request) => {
- // All requests must use our hostname to prevent DNS rebind attacks.
- if (validHosts.includes(request.hostname) !== true) {
- console.log(`Invalid HTTP hostname: ${request.hostname}`);
- return false;
- }
- // All non-GET requests must ...
- if (request.method !== "GET") {
- // ...have a referer...
- if (request.header.referer == null) {
- console.log("No referer");
- return false;
- }
- try {
- const refererUrl = new URL(request.header.referer);
- // ...with a valid hostname...
- if (validHosts.includes(refererUrl.hostname) !== true) {
- console.log(`Invalid referer hostname: ${refererUrl.hostname}`);
- return false;
- }
- // ...and must not originate from a blob path.
- if (refererUrl.pathname.startsWith("/blob/")) {
- console.log(`Invalid referer path: ${refererUrl.pathname}`);
- return false;
- }
- } catch (e) {
- console.log(`Invalid referer URL: ${request.header.referer}`);
- return false;
- }
- }
- // If all of the above checks pass, this is a valid request.
- return true;
- };
- app.on("error", (err, ctx) => {
- // Output full error objects
- console.error(err);
- // Avoid printing errors for invalid requests.
- if (isValidRequest(ctx.request)) {
- err.message = err.stack;
- err.expose = true;
- }
- return null;
- });
- app.use(mount("/assets", assets));
- // headers
- app.use(async (ctx, next) => {
- const csp = [
- "default-src 'none'",
- "img-src 'self'",
- "form-action 'self'",
- "media-src 'self'",
- "style-src 'self'",
- ].join("; ");
- // Disallow scripts.
- ctx.set("Content-Security-Policy", csp);
- // Disallow <iframe> embeds from other domains.
- ctx.set("X-Frame-Options", "SAMEORIGIN");
- const isBlobPath = ctx.path.startsWith("/blob/");
- if (isBlobPath === false) {
- // Disallow browsers overwriting declared media types.
- //
- // This should only happen on non-blob URLs.
- ctx.set("X-Content-Type-Options", "nosniff");
- }
- // Disallow sharing referrer with other domains.
- ctx.set("Referrer-Policy", "same-origin");
- // Disallow extra browser features except audio output.
- ctx.set("Feature-Policy", "speaker 'self'");
- const validHostsString = validHosts.join(" or ");
- ctx.assert(
- isValidRequest(ctx.request),
- 400,
- `Request must be addressed to ${validHostsString} and non-GET requests must contain non-blob referer.`
- );
- await next();
- });
- middleware.forEach((m) => app.use(m));
- const server = app.listen({ host, port });
- server.on("listening", () => {
- const address = server.address();
- if (typeof address === "string") {
- // This shouldn't happen, but TypeScript was complaining about it.
- throw new Error("HTTP server should never bind to Unix socket");
- }
- if (allowHost !== null) {
- validHosts.push(allowHost);
- }
- validHosts.push(address.address);
- if (validHosts.includes(host) === false) {
- validHosts.push(host);
- }
- });
- return server;
- };
|