137 lines
3.6 KiB
TypeScript
137 lines
3.6 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
import crypto from "crypto";
|
|
import { Server } from "https";
|
|
import Koa from "koa";
|
|
import {
|
|
contentSecurityPolicy,
|
|
dnsPrefetchControl,
|
|
referrerPolicy,
|
|
} from "koa-helmet";
|
|
import mount from "koa-mount";
|
|
import enforceHttps, {
|
|
httpsResolver,
|
|
xForwardedProtoResolver,
|
|
} from "koa-sslify";
|
|
import { Second } from "@shared/utils/time";
|
|
import env from "@server/env";
|
|
import Logger from "@server/logging/Logger";
|
|
import Metrics from "@server/logging/Metrics";
|
|
import ShutdownHelper, { ShutdownOrder } from "@server/utils/ShutdownHelper";
|
|
import { initI18n } from "@server/utils/i18n";
|
|
import routes from "../routes";
|
|
import api from "../routes/api";
|
|
import auth from "../routes/auth";
|
|
|
|
// Construct scripts CSP based on services in use by this installation
|
|
const defaultSrc = ["'self'"];
|
|
const scriptSrc = ["'self'", "www.googletagmanager.com"];
|
|
const styleSrc = ["'self'", "'unsafe-inline'"];
|
|
|
|
if (env.isCloudHosted) {
|
|
scriptSrc.push("cdn.zapier.com");
|
|
styleSrc.push("cdn.zapier.com");
|
|
}
|
|
|
|
// Allow to load assets from Vite
|
|
if (!env.isProduction) {
|
|
scriptSrc.push(env.URL.replace(`:${env.PORT}`, ":3001"));
|
|
scriptSrc.push("localhost:3001");
|
|
}
|
|
|
|
if (env.GOOGLE_ANALYTICS_ID) {
|
|
scriptSrc.push("www.google-analytics.com");
|
|
}
|
|
|
|
if (env.CDN_URL) {
|
|
scriptSrc.push(env.CDN_URL);
|
|
styleSrc.push(env.CDN_URL);
|
|
defaultSrc.push(env.CDN_URL);
|
|
}
|
|
|
|
export default function init(app: Koa = new Koa(), server?: Server) {
|
|
void initI18n();
|
|
|
|
if (env.isProduction) {
|
|
// Force redirect to HTTPS protocol unless explicitly disabled
|
|
if (env.FORCE_HTTPS) {
|
|
app.use(
|
|
enforceHttps({
|
|
resolver: (ctx) => {
|
|
if (httpsResolver(ctx)) {
|
|
return true;
|
|
}
|
|
return xForwardedProtoResolver(ctx);
|
|
},
|
|
})
|
|
);
|
|
} else {
|
|
Logger.warn("Enforced https was disabled with FORCE_HTTPS env variable");
|
|
}
|
|
|
|
// trust header fields set by our proxy. eg X-Forwarded-For
|
|
app.proxy = true;
|
|
}
|
|
|
|
app.use(mount("/auth", auth));
|
|
app.use(mount("/api", api));
|
|
|
|
// Monitor server connections
|
|
if (server) {
|
|
setInterval(() => {
|
|
server.getConnections((err, count) => {
|
|
if (err) {
|
|
return;
|
|
}
|
|
Metrics.gaugePerInstance("connections.count", count);
|
|
});
|
|
}, 5 * Second);
|
|
}
|
|
|
|
ShutdownHelper.add("connections", ShutdownOrder.normal, async () => {
|
|
Metrics.gaugePerInstance("connections.count", 0);
|
|
});
|
|
|
|
// Sets common security headers by default, such as no-sniff, hsts, hide powered
|
|
// by etc, these are applied after auth and api so they are only returned on
|
|
// standard non-XHR accessed routes
|
|
app.use((ctx, next) => {
|
|
ctx.state.cspNonce = crypto.randomBytes(16).toString("hex");
|
|
|
|
return contentSecurityPolicy({
|
|
directives: {
|
|
defaultSrc,
|
|
styleSrc,
|
|
scriptSrc: [
|
|
...scriptSrc,
|
|
env.DEVELOPMENT_UNSAFE_INLINE_CSP
|
|
? "'unsafe-inline'"
|
|
: `'nonce-${ctx.state.cspNonce}'`,
|
|
],
|
|
mediaSrc: ["*", "data:", "blob:"],
|
|
imgSrc: ["*", "data:", "blob:"],
|
|
frameSrc: ["*", "data:"],
|
|
// Do not use connect-src: because self + websockets does not work in
|
|
// Safari, ref: https://bugs.webkit.org/show_bug.cgi?id=201591
|
|
connectSrc: ["*"],
|
|
},
|
|
})(ctx, next);
|
|
});
|
|
|
|
// Allow DNS prefetching for performance, we do not care about leaking requests
|
|
// to our own CDN's
|
|
app.use(
|
|
dnsPrefetchControl({
|
|
allow: true,
|
|
})
|
|
);
|
|
app.use(
|
|
referrerPolicy({
|
|
policy: "no-referrer",
|
|
})
|
|
);
|
|
|
|
app.use(mount(routes));
|
|
|
|
return app;
|
|
}
|