diff --git a/.env.sample b/.env.sample index 2d153caf2..997f417b8 100644 --- a/.env.sample +++ b/.env.sample @@ -102,6 +102,12 @@ OIDC_SCOPES="openid profile email" # –––––––––––––––– OPTIONAL –––––––––––––––– +# Base64 encoded private key and certificate for HTTPS termination. This is only +# required if you do not use an external reverse proxy. See documentation: +# https://wiki.generaloutline.com/share/1c922644-40d8-41fe-98f9-df2b67239d45 +SSL_KEY= +SSL_CERT= + # If using a Cloudfront/Cloudflare distribution or similar it can be set below. # This will cause paths to javascript, stylesheets, and images to be updated to # the hostname defined in CDN_URL. In your CDN configuration the origin server diff --git a/.gitignore b/.gitignore index 9e9441087..3beab0401 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ stats.json fakes3/* .idea *.pem +*.key +*.cert diff --git a/server/index.ts b/server/index.ts index 397440399..4e679b365 100644 --- a/server/index.ts +++ b/server/index.ts @@ -4,6 +4,7 @@ import env from "./env"; import "./tracing"; // must come before importing any instrumented module import http from "http"; +import https from "https"; import Koa from "koa"; import compress from "koa-compress"; import helmet from "koa-helmet"; @@ -18,6 +19,7 @@ import Logger from "./logging/logger"; import { requestErrorHandler } from "./logging/sentry"; import services from "./services"; import { getArg } from "./utils/args"; +import { getSSLOptions } from "./utils/ssl"; import { checkEnv, checkMigrations } from "./utils/startup"; import { checkUpdates } from "./utils/updates"; @@ -67,10 +69,18 @@ function master() { // This function will only be called in each forked process async function start(id: number, disconnect: () => void) { + // Find if SSL certs are available + const ssl = getSSLOptions(); + const useHTTPS = !!ssl.key && !!ssl.cert; + // If a --port flag is passed then it takes priority over the env variable const normalizedPortFlag = getArg("port", "p"); const app = new Koa(); - const server = stoppable(http.createServer(app.callback())); + const server = stoppable( + useHTTPS + ? https.createServer(ssl, app.callback()) + : http.createServer(app.callback()) + ); const router = new Router(); // install basic middleware shared by all services @@ -108,7 +118,9 @@ async function start(id: number, disconnect: () => void) { Logger.info( "lifecycle", - `Listening on http://localhost:${(address as AddressInfo).port}` + `Listening on ${useHTTPS ? "https" : "http"}://localhost:${ + (address as AddressInfo).port + }` ); }); server.listen(normalizedPortFlag || env.PORT || "3000"); diff --git a/server/utils/ssl.ts b/server/utils/ssl.ts new file mode 100644 index 000000000..4a986683e --- /dev/null +++ b/server/utils/ssl.ts @@ -0,0 +1,42 @@ +import fs from "fs"; +import path from "path"; +import env from "../env"; + +/** + * Find if SSL certs are available in the environment or filesystem and return + * as a valid ServerOptions object + */ +export function getSSLOptions() { + function safeReadFile(name: string) { + try { + return fs.readFileSync( + path.normalize(`${__dirname}/../../../${name}`), + "utf8" + ); + } catch (err) { + return undefined; + } + } + + try { + return { + key: + (env.SSL_KEY + ? Buffer.from(env.SSL_KEY, "base64").toString("ascii") + : undefined) || + safeReadFile("private.key") || + safeReadFile("private.pem"), + cert: + (env.SSL_CERT + ? Buffer.from(env.SSL_CERT, "base64").toString("ascii") + : undefined) || + safeReadFile("public.cert") || + safeReadFile("public.pem"), + }; + } catch (err) { + return { + key: undefined, + cert: undefined, + }; + } +}