diff --git a/docs/nix_keyserver_deployment.md b/docs/nix_keyserver_deployment.md --- a/docs/nix_keyserver_deployment.md +++ b/docs/nix_keyserver_deployment.md @@ -14,6 +14,7 @@ COMM_JSONCONFIG_secrets_user_credentials='{"username":"","password":""}' COMM_JSONCONFIG_facts_landing_url='{"baseDomain":"http://localhost","basePath":"/commlanding/","baseRoutePath":"/commlanding/","https":false}' COMM_JSONCONFIG_facts_commapp_url='{"baseDomain":"http://localhost:3000","basePath":"/comm/","https":false,"baseRoutePath":"/comm/","proxy":"none"}' +COMM_JSONCONFIG_facts_webapp_cors='{"domain": "http://localhost:3000"}' # Required to connect to production Identity service COMM_JSONCONFIG_secrets_identity_service_config="{\"identitySocketAddr\":\"https://identity.commtechnologies.org:50054\"}" @@ -51,6 +52,11 @@ - `proxy`: `"none" | "apache"` Determines which request headers to use for HTTPS validation and IP address timezone detection. - `https`: If true, checks request headers to validate that HTTPS is in use. +### CORS configuration + +- `COMM_JSONCONFIG_facts_webapp_cors`: Your keyserver needs to be able to include CORS headers with the domain where the comm web application is hosted. + - `domain`: Domain where the web application is hosted. + ### Backup configuration - `COMM_JSONCONFIG_facts_backups`: Specifies whether to enable backups, where to store them, and the max size of the backups directory. diff --git a/keyserver/flow-typed/npm/cors_v2.x.x.js b/keyserver/flow-typed/npm/cors_v2.x.x.js new file mode 100644 --- /dev/null +++ b/keyserver/flow-typed/npm/cors_v2.x.x.js @@ -0,0 +1,26 @@ +// flow-typed signature: 425712a647645fb8847dbd9109337837 +// flow-typed version: c6154227d1/cors_v2.x.x/flow_>=v0.104.x + +// @flow + +type CustomOrigin = ( + requestOrigin: string, + callback: (err: Error | null, allow?: boolean) => void +) => void; + +type CorsOptions = { + origin?: boolean | string | RegExp | string[] | RegExp[] | CustomOrigin, + methods?: string | string[], + allowedHeaders?: string | string[], + exposedHeaders?: string | string[], + credentials?: boolean, + maxAge?: number, + preflightContinue?: boolean, + optionsSuccessStatus?: number, + ... +} + +declare module "cors" { + import type { $Request as Request, $Response as Response, NextFunction } from "express"; + declare module.exports: (options?: CorsOptions) => (req: Request, res: Response, next?: NextFunction) => mixed; +} diff --git a/keyserver/package.json b/keyserver/package.json --- a/keyserver/package.json +++ b/keyserver/package.json @@ -21,6 +21,7 @@ "test": "jest" }, "devDependencies": { + "0x": "^5.7.0", "@babel/cli": "^7.13.14", "@babel/core": "^7.13.14", "@babel/node": "^7.13.13", @@ -41,8 +42,7 @@ "flow-typed": "^3.2.1", "internal-ip": "4.3.0", "jest": "^26.6.3", - "nodemon": "^2.0.4", - "0x": "^5.7.0" + "nodemon": "^2.0.4" }, "dependencies": { "@babel/runtime": "^7.13.10", @@ -54,6 +54,7 @@ "common-tags": "^1.7.2", "compression": "^1.7.4", "cookie-parser": "^1.4.3", + "cors": "^2.8.5", "dateformat": "^3.0.3", "detect-browser": "^4.0.4", "ethers": "^5.7.2", diff --git a/keyserver/src/database/migration-config.js b/keyserver/src/database/migration-config.js --- a/keyserver/src/database/migration-config.js +++ b/keyserver/src/database/migration-config.js @@ -583,6 +583,18 @@ () => dbQuery(SQL`ALTER TABLE cookies MODIFY COLUMN hash char(64) NOT NULL`), ], + [ + 48, + async () => { + if (isDockerEnvironment()) { + return; + } + const defaultCorsConfig = { + domain: 'http://localhost:3000', + }; + writeJSONToFile(defaultCorsConfig, 'facts/webapp_cors.json'); + }, + ], ]); const newDatabaseVersion: number = Math.max(...migrations.keys()); @@ -648,7 +660,7 @@ filePath: string, routePath: string, ): Promise { - if (process.env.COMM_DATABASE_HOST) { + if (isDockerEnvironment()) { return; } // Since the non-Apache config is so opinionated, just write expected config @@ -664,7 +676,7 @@ } async function writeSquadCalRoute(filePath: string): Promise { - if (process.env.COMM_DATABASE_HOST) { + if (isDockerEnvironment()) { return; } // Since the non-Apache config is so opinionated, just write expected config @@ -704,4 +716,8 @@ ); } +function isDockerEnvironment(): boolean { + return !!process.env.COMM_DATABASE_HOST; +} + export { migrations, newDatabaseVersion, createOlmAccounts }; diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js --- a/keyserver/src/keyserver.js +++ b/keyserver/src/keyserver.js @@ -4,6 +4,7 @@ import cluster from 'cluster'; import compression from 'compression'; import cookieParser from 'cookie-parser'; +import cors from 'cors'; import crypto from 'crypto'; import express from 'express'; import expressWs from 'express-ws'; @@ -46,12 +47,18 @@ getSquadCalURLFacts, getLandingURLFacts, getCommAppURLFacts, + getWebAppCorsConfig, } from './utils/urls.js'; const shouldDisplayQRCodeInTerminal = false; (async () => { - await Promise.all([olm.init(), prefetchAllURLFacts(), initENSCache()]); + const [webAppCorsConfig] = await Promise.all([ + getWebAppCorsConfig(), + olm.init(), + prefetchAllURLFacts(), + initENSCache(), + ]); const squadCalBaseRoutePath = getSquadCalURLFacts()?.baseRoutePath; const landingBaseRoutePath = getLandingURLFacts()?.baseRoutePath; @@ -62,6 +69,14 @@ ? undefined : { maxAge: '1y', immutable: true }; + let corsOptions = null; + if (webAppCorsConfig) { + corsOptions = { + origin: webAppCorsConfig.domain, + methods: ['GET', 'POST'], + }; + } + const isCPUProfilingEnabled = process.env.KEYSERVER_CPU_PROFILING_ENABLED; const areEndpointMetricsEnabled = process.env.KEYSERVER_ENDPOINT_METRICS_ENABLED; @@ -226,6 +241,9 @@ if (squadCalBaseRoutePath) { const squadCalRouter = express.Router(); + if (corsOptions) { + squadCalRouter.use(cors(corsOptions)); + } setupAppRouter(squadCalRouter); server.use(squadCalBaseRoutePath, squadCalRouter); } diff --git a/keyserver/src/uploads/uploads.js b/keyserver/src/uploads/uploads.js --- a/keyserver/src/uploads/uploads.js +++ b/keyserver/src/uploads/uploads.js @@ -172,12 +172,6 @@ const { content, mime } = await fetchUpload(viewer, uploadID, secret); res.type(mime); res.set('Cache-Control', 'public, max-age=31557600, immutable'); - if (process.env.NODE_ENV === 'development') { - // Add a CORS header to allow local development using localhost - const port = process.env.PORT || '3000'; - res.set('Access-Control-Allow-Origin', `http://localhost:${port}`); - res.set('Access-Control-Allow-Methods', 'GET'); - } res.send(content); } else { const totalUploadSize = await getUploadSize(uploadID, secret); @@ -207,12 +201,6 @@ 'Content-Type': mime, 'Content-Length': respWidth.toString(), }; - if (process.env.NODE_ENV === 'development') { - // Add a CORS header to allow local development using localhost - const port = process.env.PORT || '3000'; - respHeaders['Access-Control-Allow-Origin'] = `http://localhost:${port}`; - respHeaders['Access-Control-Allow-Methods'] = 'GET'; - } // HTTP 206 Partial Content // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206 diff --git a/keyserver/src/utils/urls.js b/keyserver/src/utils/urls.js --- a/keyserver/src/utils/urls.js +++ b/keyserver/src/utils/urls.js @@ -84,6 +84,15 @@ return urlFacts; } +export type WebAppCorsConfig = { +domain: string }; +async function getWebAppCorsConfig(): Promise { + const config = await getCommConfig({ + folder: 'facts', + name: 'webapp_cors', + }); + return config; +} + export { prefetchAllURLFacts, getSquadCalURLFacts, @@ -92,4 +101,5 @@ getLandingURLFacts, getAndAssertLandingURLFacts, getAppURLFactsFromRequestURL, + getWebAppCorsConfig, }; diff --git a/scripts/create_url_facts.sh b/scripts/create_url_facts.sh --- a/scripts/create_url_facts.sh +++ b/scripts/create_url_facts.sh @@ -8,6 +8,7 @@ COMMAPP_URL_PATH="${KEYSERVER_FACTS_DIR}/commapp_url.json" LANDING_URL_PATH="${KEYSERVER_FACTS_DIR}/landing_url.json" SQUADCAL_URL_PATH="${KEYSERVER_FACTS_DIR}/squadcal_url.json" +WEBAPP_CORS_PATH="${KEYSERVER_FACTS_DIR}/webapp_cors.json" if [[ ! -d "$KEYSERVER_FACTS_DIR" ]]; then mkdir -p "$KEYSERVER_FACTS_DIR" @@ -24,3 +25,7 @@ if [[ ! -f "$SQUADCAL_URL_PATH" ]]; then cp "$SCRIPT_DIR/templates/squadcal_url.json" "$SQUADCAL_URL_PATH" fi + +if [[ ! -f "$WEBAPP_CORS_PATH" ]]; then + cp "$SCRIPT_DIR/templates/webapp_cors.json" "$WEBAPP_CORS_PATH" +fi diff --git a/scripts/templates/webapp_cors.json b/scripts/templates/webapp_cors.json new file mode 100644 --- /dev/null +++ b/scripts/templates/webapp_cors.json @@ -0,0 +1,3 @@ +{ + "domain": "http://localhost:3000" +} diff --git a/yarn.lock b/yarn.lock --- a/yarn.lock +++ b/yarn.lock @@ -13454,9 +13454,9 @@ integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.20.0, globals@^13.6.0, globals@^13.9.0: - version "13.21.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.21.0.tgz#163aae12f34ef502f5153cfbdd3600f36c63c571" - integrity sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg== + version "13.22.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.22.0.tgz#0c9fcb9c48a2494fbb5edbfee644285543eba9d8" + integrity sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw== dependencies: type-fest "^0.20.2"