141 lines
3.7 KiB
JavaScript
141 lines
3.7 KiB
JavaScript
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.
|
|
// See: https://github.com/fraction/oasis/issues/138
|
|
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;
|
|
};
|