From c32cec7bff778fc6a684441d05d7a1acd57db158 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 9 Aug 2023 07:21:41 -0400 Subject: [PATCH] Add support for SSL in development (#5668) --- .env.sample | 2 +- Makefile | 1 + package.json | 1 + plugins/email/server/auth/email.test.ts | 18 +++++++++--------- server/config/certs/.gitkeep | 0 server/env.ts | 2 +- server/index.ts | 7 ++++--- server/routes/api/auth/auth.test.ts | 12 ++++++------ server/routes/app.ts | 12 ++++++++---- server/scripts/install-local-ssl.js | 23 +++++++++++++++++++++++ server/utils/ssl.ts | 6 ++++-- vite.config.ts | 18 +++++++++++++++++- 12 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 server/config/certs/.gitkeep create mode 100644 server/scripts/install-local-ssl.js diff --git a/.env.sample b/.env.sample index c0f5c18e51..41132f7d3e 100644 --- a/.env.sample +++ b/.env.sample @@ -30,7 +30,7 @@ REDIS_URL=redis://localhost:6379 # URL should point to the fully qualified, publicly accessible URL. If using a # proxy the port in URL and PORT may be different. -URL=http://localhost:3000 +URL=https://app.outline.dev:3000 PORT=3000 # See [documentation](docs/SERVICES.md) on running a separate collaboration diff --git a/Makefile b/Makefile index 4b4b7cda8a..5a59afc68a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ up: docker-compose up -d redis postgres s3 + yarn install-local-ssl yarn install --pure-lockfile yarn dev:watch diff --git a/package.json b/package.json index e4a5260acc..6d6f4bdc68 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "lint": "eslint app server shared plugins", "prepare": "husky install", "postinstall": "yarn patch-package", + "install-local-ssl": "node ./server/scripts/install-local-ssl.js", "heroku-postbuild": "yarn build && yarn db:migrate", "db:create-migration": "sequelize migration:create", "db:create": "sequelize db:create", diff --git a/plugins/email/server/auth/email.test.ts b/plugins/email/server/auth/email.test.ts index 19322e8c99..26d713debf 100644 --- a/plugins/email/server/auth/email.test.ts +++ b/plugins/email/server/auth/email.test.ts @@ -60,7 +60,7 @@ describe("email", () => { email: user.email, }, headers: { - host: "example.localoutline.com", + host: "example.outline.dev", }, }); const body = await res.json(); @@ -71,7 +71,7 @@ describe("email", () => { }); it("should not send email when user is on another subdomain but respond with success", async () => { - env.URL = sharedEnv.URL = "http://localoutline.com"; + env.URL = sharedEnv.URL = "https://app.outline.dev"; env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true; env.DEPLOYMENT = "hosted"; @@ -85,7 +85,7 @@ describe("email", () => { email: user.email, }, headers: { - host: "example.localoutline.com", + host: "example.outline.dev", }, }); @@ -109,7 +109,7 @@ describe("email", () => { email: user.email, }, headers: { - host: "example.localoutline.com", + host: "example.outline.dev", }, }); const body = await res.json(); @@ -129,7 +129,7 @@ describe("email", () => { email: "user@example.com", }, headers: { - host: "example.localoutline.com", + host: "example.outline.dev", }, }); const body = await res.json(); @@ -141,7 +141,7 @@ describe("email", () => { describe("with multiple users matching email", () => { it("should default to current subdomain with SSO", async () => { const spy = jest.spyOn(SigninEmail.prototype, "schedule"); - env.URL = sharedEnv.URL = "http://localoutline.com"; + env.URL = sharedEnv.URL = "https://app.outline.dev"; env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true; const email = "sso-user@example.org"; const team = await buildTeam({ @@ -159,7 +159,7 @@ describe("email", () => { email, }, headers: { - host: "example.localoutline.com", + host: "example.outline.dev", }, }); const body = await res.json(); @@ -171,7 +171,7 @@ describe("email", () => { it("should default to current subdomain with guest email", async () => { const spy = jest.spyOn(SigninEmail.prototype, "schedule"); - env.URL = sharedEnv.URL = "http://localoutline.com"; + env.URL = sharedEnv.URL = "https://app.outline.dev"; env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true; const email = "guest-user@example.org"; const team = await buildTeam({ @@ -189,7 +189,7 @@ describe("email", () => { email, }, headers: { - host: "example.localoutline.com", + host: "example.outline.dev", }, }); const body = await res.json(); diff --git a/server/config/certs/.gitkeep b/server/config/certs/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/server/env.ts b/server/env.ts index 47c71eb092..11f2af6f1e 100644 --- a/server/env.ts +++ b/server/env.ts @@ -162,7 +162,7 @@ export class Environment { */ @IsNumber() @IsOptional() - public PORT = this.toOptionalNumber(process.env.PORT); + public PORT = this.toOptionalNumber(process.env.PORT) ?? 3000; /** * Optional extra debugging. Comma separated diff --git a/server/index.ts b/server/index.ts index de33f7632a..e6480af9f0 100644 --- a/server/index.ts +++ b/server/index.ts @@ -128,16 +128,17 @@ async function start(id: number, disconnect: () => void) { }); server.on("listening", () => { const address = server.address(); + const port = (address as AddressInfo).port; Logger.info( "lifecycle", - `Listening on ${useHTTPS ? "https" : "http"}://localhost:${ - (address as AddressInfo).port + `Listening on ${useHTTPS ? "https" : "http"}://localhost:${port} / ${ + env.URL }` ); }); - server.listen(normalizedPortFlag || env.PORT || "3000"); + server.listen(normalizedPortFlag || env.PORT); server.setTimeout(env.REQUEST_TIMEOUT); ShutdownHelper.add( diff --git a/server/routes/api/auth/auth.test.ts b/server/routes/api/auth/auth.test.ts index 8475e691be..b95a0c85d5 100644 --- a/server/routes/api/auth/auth.test.ts +++ b/server/routes/api/auth/auth.test.ts @@ -105,7 +105,7 @@ describe("#auth.config", () => { }); it("should return available providers for team subdomain", async () => { - env.URL = sharedEnv.URL = "http://localoutline.com"; + env.URL = sharedEnv.URL = "https://app.outline.dev"; env.SUBDOMAINS_ENABLED = sharedEnv.SUBDOMAINS_ENABLED = true; env.DEPLOYMENT = "hosted"; @@ -121,7 +121,7 @@ describe("#auth.config", () => { }); const res = await server.post("/api/auth.config", { headers: { - host: `example.localoutline.com`, + host: `example.outline.dev`, }, }); const body = await res.json(); @@ -155,7 +155,7 @@ describe("#auth.config", () => { }); it("should return email provider for team when guest signin enabled", async () => { - env.URL = sharedEnv.URL = "http://localoutline.com"; + env.URL = sharedEnv.URL = "https://app.outline.dev"; env.DEPLOYMENT = "hosted"; await buildTeam({ @@ -170,7 +170,7 @@ describe("#auth.config", () => { }); const res = await server.post("/api/auth.config", { headers: { - host: "example.localoutline.com", + host: "example.outline.dev", }, }); const body = await res.json(); @@ -181,7 +181,7 @@ describe("#auth.config", () => { }); it("should not return provider when disabled", async () => { - env.URL = sharedEnv.URL = "http://localoutline.com"; + env.URL = sharedEnv.URL = "https://app.outline.dev"; env.DEPLOYMENT = "hosted"; await buildTeam({ @@ -197,7 +197,7 @@ describe("#auth.config", () => { }); const res = await server.post("/api/auth.config", { headers: { - host: "example.localoutline.com", + host: "example.outline.dev", }, }); const body = await res.json(); diff --git a/server/routes/app.ts b/server/routes/app.ts index bf0c7b32fe..e3f3e2e92d 100644 --- a/server/routes/app.ts +++ b/server/routes/app.ts @@ -17,7 +17,11 @@ import readManifestFile from "@server/utils/readManifestFile"; const isProduction = env.ENVIRONMENT === "production"; const isDevelopment = env.ENVIRONMENT === "development"; const isTest = env.ENVIRONMENT === "test"; + const readFile = util.promisify(fs.readFile); +const entry = "app/index.tsx"; +const viteHost = env.URL.replace(`:${env.PORT}`, ":3001"); + let indexHtmlCache: Buffer | undefined; const readIndexFile = async (): Promise => { @@ -71,20 +75,20 @@ export const renderApp = async ( window.env = ${JSON.stringify(presentEnv(env, options.analytics))}; `; - const entry = "app/index.tsx"; + const scriptTags = isProduction ? `` : ` - - + + `; ctx.body = page diff --git a/server/scripts/install-local-ssl.js b/server/scripts/install-local-ssl.js new file mode 100644 index 0000000000..f6810b0cea --- /dev/null +++ b/server/scripts/install-local-ssl.js @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ + +const exec = require("child_process").execSync; +const fs = require("fs"); +const path = require("path"); + +const sslDir = path.join(__dirname, "..", "config", "certs"); +const sslCert = path.join(sslDir, "public.cert"); +const sslKey = path.join(sslDir, "private.key"); + +if (!fs.existsSync(sslKey) || !fs.existsSync(sslCert)) { + try { + exec( + `mkcert -cert-file ${sslDir}/public.cert -key-file ${sslDir}/private.key "*.outline.dev" && mkcert -install` + ); + console.log("🔒 Local SSL certificate created"); + } catch (e) { + console.log( + "SSL certificates could not be generated. Ensure mkcert is installed and in your PATH" + ); + console.log(e.message); + } +} diff --git a/server/utils/ssl.ts b/server/utils/ssl.ts index 4a986683e2..52987c7644 100644 --- a/server/utils/ssl.ts +++ b/server/utils/ssl.ts @@ -25,13 +25,15 @@ export function getSSLOptions() { ? Buffer.from(env.SSL_KEY, "base64").toString("ascii") : undefined) || safeReadFile("private.key") || - safeReadFile("private.pem"), + safeReadFile("private.pem") || + safeReadFile("server/config/certs/private.key"), cert: (env.SSL_CERT ? Buffer.from(env.SSL_CERT, "base64").toString("ascii") : undefined) || safeReadFile("public.cert") || - safeReadFile("public.pem"), + safeReadFile("public.pem") || + safeReadFile("server/config/certs/public.cert"), }; } catch (err) { return { diff --git a/vite.config.ts b/vite.config.ts index d3ca07496c..633d1308d0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ +import fs from "fs"; import path from "path"; // eslint-disable-next-line import/no-unresolved import { optimizeLodashImports } from "@optimize-lodash/rollup-plugin"; @@ -5,7 +6,7 @@ import react from "@vitejs/plugin-react"; import browserslistToEsbuild from "browserslist-to-esbuild"; import dotenv from "dotenv"; import { webpackStats } from "rollup-plugin-webpack-stats"; -import { defineConfig } from "vite"; +import { CommonServerOptions, defineConfig } from "vite"; import { VitePWA } from "vite-plugin-pwa"; import { viteStaticCopy } from "vite-plugin-static-copy"; @@ -14,6 +15,20 @@ dotenv.config({ silent: true, }); +let httpsConfig: CommonServerOptions["https"] | undefined; + +if (process.env.NODE_ENV === "development") { + try { + httpsConfig = { + key: fs.readFileSync("./server/config/certs/private.key"), + cert: fs.readFileSync("./server/config/certs/public.cert"), + }; + } catch (err) { + // eslint-disable-next-line no-console + console.warn("No local SSL certs found, HTTPS will not be available"); + } +} + export default () => defineConfig({ root: "./", @@ -22,6 +37,7 @@ export default () => server: { port: 3001, host: true, + https: httpsConfig, }, plugins: [ // https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme