diff --git a/keyserver/src/session/cookies.js b/keyserver/src/session/cookies.js index 3683a7539..f848d93cb 100644 --- a/keyserver/src/session/cookies.js +++ b/keyserver/src/session/cookies.js @@ -1,826 +1,826 @@ // @flow import crypto from 'crypto'; import type { $Response, $Request } from 'express'; import invariant from 'invariant'; import url from 'url'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; import { type Platform, type PlatformDetails, isDeviceType, } from 'lib/types/device-types.js'; import type { CalendarQuery } from 'lib/types/entry-types.js'; import { type ServerSessionChange, cookieLifetime, cookieTypes, sessionIdentifierTypes, type SessionIdentifierType, } from 'lib/types/session-types.js'; import type { SIWESocialProof } from 'lib/types/siwe-types.js'; import type { InitialClientSocketMessage } from 'lib/types/socket-types.js'; import type { UserInfo } from 'lib/types/user-types.js'; import { ignorePromiseRejections } from 'lib/utils/promises.js'; import { isBcryptHash, getCookieHash, verifyCookieHash, } from './cookie-hash.js'; import { Viewer } from './viewer.js'; import type { AnonymousViewerData, UserViewerData } from './viewer.js'; import createIDs from '../creators/id-creator.js'; import { createSession } from '../creators/session-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { deleteCookie } from '../deleters/cookie-deleters.js'; import { clearDeviceToken } from '../updaters/device-token-updaters.js'; import { assertSecureRequest } from '../utils/security-utils.js'; import { type AppURLFacts, getAppURLFactsFromRequestURL, } from '../utils/urls.js'; function cookieIsExpired(lastUsed: number) { return lastUsed + cookieLifetime <= Date.now(); } type SessionParameterInfo = { isSocket: boolean, sessionID: ?string, sessionIdentifierType: SessionIdentifierType, ipAddress: string, userAgent: ?string, }; type FetchViewerResult = | { +type: 'valid', +viewer: Viewer } | InvalidFetchViewerResult; type InvalidFetchViewerResult = | { +type: 'nonexistant', +cookieName: ?string, +sessionParameterInfo: SessionParameterInfo, } | { +type: 'invalidated', +cookieName: string, +cookieID: string, +sessionParameterInfo: SessionParameterInfo, +platformDetails: ?PlatformDetails, +deviceToken: ?string, }; async function fetchUserViewer( cookie: string, sessionParameterInfo: SessionParameterInfo, ): Promise { const [cookieID, cookiePassword] = cookie.split(':'); if (!cookieID || !cookiePassword) { return { type: 'nonexistant', cookieName: cookieTypes.USER, sessionParameterInfo, }; } const query = SQL` SELECT hash, user, last_used, platform, device_token, versions FROM cookies WHERE id = ${cookieID} AND user IS NOT NULL `; const [[result], allSessionInfo] = await Promise.all([ dbQuery(query), fetchSessionInfo(sessionParameterInfo, cookieID), ]); if (result.length === 0) { return { type: 'nonexistant', cookieName: cookieTypes.USER, sessionParameterInfo, }; } let sessionID = null, sessionInfo = null; if (allSessionInfo) { ({ sessionID, ...sessionInfo } = allSessionInfo); } const cookieRow = result[0]; let platformDetails; let versions = null; if (cookieRow.versions) { versions = JSON.parse(cookieRow.versions); platformDetails = { platform: cookieRow.platform, codeVersion: versions.codeVersion, stateVersion: versions.stateVersion, }; } else { platformDetails = { platform: cookieRow.platform }; } if (versions && versions.majorDesktopVersion) { platformDetails = { ...platformDetails, majorDesktopVersion: versions.majorDesktopVersion, }; } const deviceToken = cookieRow.device_token; const cookieHash = cookieRow.hash; if ( !verifyCookieHash(cookiePassword, cookieHash) || cookieIsExpired(cookieRow.last_used) ) { return { type: 'invalidated', cookieName: cookieTypes.USER, cookieID, sessionParameterInfo, platformDetails, deviceToken, }; } const userID = cookieRow.user.toString(); const viewer = new Viewer({ isSocket: sessionParameterInfo.isSocket, loggedIn: true, id: userID, platformDetails, deviceToken, userID, cookieID, cookiePassword, cookieHash, sessionIdentifierType: sessionParameterInfo.sessionIdentifierType, sessionID, sessionInfo, isScriptViewer: false, ipAddress: sessionParameterInfo.ipAddress, userAgent: sessionParameterInfo.userAgent, }); return { type: 'valid', viewer }; } async function fetchAnonymousViewer( cookie: string, sessionParameterInfo: SessionParameterInfo, ): Promise { const [cookieID, cookiePassword] = cookie.split(':'); if (!cookieID || !cookiePassword) { return { type: 'nonexistant', cookieName: cookieTypes.ANONYMOUS, sessionParameterInfo, }; } const query = SQL` SELECT last_used, hash, platform, device_token, versions FROM cookies WHERE id = ${cookieID} AND user IS NULL `; const [[result], allSessionInfo] = await Promise.all([ dbQuery(query), fetchSessionInfo(sessionParameterInfo, cookieID), ]); if (result.length === 0) { return { type: 'nonexistant', cookieName: cookieTypes.ANONYMOUS, sessionParameterInfo, }; } let sessionID = null, sessionInfo = null; if (allSessionInfo) { ({ sessionID, ...sessionInfo } = allSessionInfo); } const cookieRow = result[0]; let platformDetails = null; let versions = null; if (cookieRow.platform && cookieRow.versions) { versions = JSON.parse(cookieRow.versions); platformDetails = { platform: cookieRow.platform, codeVersion: versions.codeVersion, stateVersion: versions.stateVersion, }; } else if (cookieRow.platform) { platformDetails = { platform: cookieRow.platform }; } if (platformDetails && versions && versions.majorDesktopVersion) { platformDetails = { ...platformDetails, majorDesktopVersion: versions.majorDesktopVersion, }; } const deviceToken = cookieRow.device_token; const cookieHash = cookieRow.hash; if ( !verifyCookieHash(cookiePassword, cookieHash) || cookieIsExpired(cookieRow.last_used) ) { return { type: 'invalidated', cookieName: cookieTypes.ANONYMOUS, cookieID, sessionParameterInfo, platformDetails, deviceToken, }; } const viewer = new Viewer({ isSocket: sessionParameterInfo.isSocket, loggedIn: false, id: cookieID, platformDetails, deviceToken, cookieID, cookiePassword, cookieHash, sessionIdentifierType: sessionParameterInfo.sessionIdentifierType, sessionID, sessionInfo, isScriptViewer: false, ipAddress: sessionParameterInfo.ipAddress, userAgent: sessionParameterInfo.userAgent, }); return { type: 'valid', viewer }; } type SessionInfo = { +sessionID: ?string, +lastValidated: number, +lastUpdate: number, +calendarQuery: CalendarQuery, }; async function fetchSessionInfo( sessionParameterInfo: SessionParameterInfo, cookieID: string, ): Promise { const { sessionID } = sessionParameterInfo; const session = sessionID !== undefined ? sessionID : cookieID; if (!session) { return null; } const query = SQL` SELECT query, last_validated, last_update FROM sessions WHERE id = ${session} AND cookie = ${cookieID} `; const [result] = await dbQuery(query); if (result.length === 0) { return null; } return { sessionID, lastValidated: result[0].last_validated, lastUpdate: result[0].last_update, calendarQuery: JSON.parse(result[0].query), }; } async function fetchViewerFromRequestBody( body: mixed, sessionParameterInfo: SessionParameterInfo, ): Promise { if (!body || typeof body !== 'object') { return { type: 'nonexistant', cookieName: null, sessionParameterInfo, }; } const cookiePair = body.cookie; if (cookiePair === null || cookiePair === '') { return { type: 'nonexistant', cookieName: null, sessionParameterInfo, }; } if (!cookiePair || typeof cookiePair !== 'string') { return { type: 'nonexistant', cookieName: null, sessionParameterInfo, }; } const [type, cookie] = cookiePair.split('='); if (type === cookieTypes.USER && cookie) { return await fetchUserViewer(cookie, sessionParameterInfo); } else if (type === cookieTypes.ANONYMOUS && cookie) { return await fetchAnonymousViewer(cookie, sessionParameterInfo); } return { type: 'nonexistant', cookieName: null, sessionParameterInfo, }; } function getRequestIPAddress(req: $Request) { const { proxy } = getAppURLFactsFromRequestURL(req.originalUrl); let ipAddress; if (proxy === 'none') { ipAddress = req.socket.remoteAddress; - } else if (proxy === 'apache') { + } else if (proxy === 'apache' || proxy === 'aws') { ipAddress = req.get('X-Forwarded-For'); } invariant(ipAddress, 'could not determine requesting IP address'); return ipAddress; } function getSessionParameterInfoFromRequestBody( req: $Request, ): SessionParameterInfo { const body = (req.body: any); let sessionID = body.sessionID !== undefined || req.method !== 'GET' ? body.sessionID : null; if (sessionID === '') { sessionID = null; } const sessionIdentifierType = req.method === 'GET' || sessionID !== undefined ? sessionIdentifierTypes.BODY_SESSION_ID : sessionIdentifierTypes.COOKIE_ID; return { isSocket: false, sessionID, sessionIdentifierType, ipAddress: getRequestIPAddress(req), userAgent: req.get('User-Agent'), }; } async function fetchViewerForJSONRequest(req: $Request): Promise { assertSecureRequest(req); const sessionParameterInfo = getSessionParameterInfoFromRequestBody(req); const result = await fetchViewerFromRequestBody( req.body, sessionParameterInfo, ); return await handleFetchViewerResult(result); } async function fetchViewerForSocket( req: $Request, clientMessage: InitialClientSocketMessage, ): Promise { assertSecureRequest(req); const { sessionIdentification } = clientMessage.payload; const { sessionID } = sessionIdentification; const sessionParameterInfo = { isSocket: true, sessionID, sessionIdentifierType: sessionID !== undefined ? sessionIdentifierTypes.BODY_SESSION_ID : sessionIdentifierTypes.COOKIE_ID, ipAddress: getRequestIPAddress(req), userAgent: req.get('User-Agent'), }; const result = await fetchViewerFromRequestBody( clientMessage.payload.sessionIdentification, sessionParameterInfo, ); if (result.type === 'valid') { return result.viewer; } const anonymousViewerDataPromise: Promise = (async () => { const platformDetails = result.type === 'invalidated' ? result.platformDetails : null; const deviceToken = result.type === 'invalidated' ? result.deviceToken : null; return await createNewAnonymousCookie({ platformDetails, deviceToken, }); })(); const deleteCookiePromise = (async () => { if (result.type === 'invalidated') { await deleteCookie(result.cookieID); } })(); const [anonymousViewerData] = await Promise.all([ anonymousViewerDataPromise, deleteCookiePromise, ]); return createViewerForInvalidFetchViewerResult(result, anonymousViewerData); } async function handleFetchViewerResult( result: FetchViewerResult, inputPlatformDetails?: PlatformDetails, ) { if (result.type === 'valid') { return result.viewer; } let platformDetails: ?PlatformDetails = inputPlatformDetails; if (!platformDetails && result.type === 'invalidated') { platformDetails = result.platformDetails; } const deviceToken = result.type === 'invalidated' ? result.deviceToken : null; const deleteCookiePromise = (async () => { if (result.type === 'invalidated') { await deleteCookie(result.cookieID); } })(); const [anonymousViewerData] = await Promise.all([ createNewAnonymousCookie({ platformDetails, deviceToken }), deleteCookiePromise, ]); return createViewerForInvalidFetchViewerResult(result, anonymousViewerData); } function createViewerForInvalidFetchViewerResult( result: InvalidFetchViewerResult, anonymousViewerData: AnonymousViewerData, ): Viewer { const viewer = new Viewer({ ...anonymousViewerData, sessionIdentifierType: result.sessionParameterInfo.sessionIdentifierType, isSocket: result.sessionParameterInfo.isSocket, ipAddress: result.sessionParameterInfo.ipAddress, userAgent: result.sessionParameterInfo.userAgent, }); viewer.sessionChanged = true; // If cookieName is falsey, that tells us that there was no cookie specified // in the request, which means we can't be invalidating anything. if (result.cookieName) { viewer.cookieInvalidated = true; viewer.initialCookieName = result.cookieName; } return viewer; } function addSessionChangeInfoToResult( viewer: Viewer, res: $Response, result: Object, ) { let threadInfos = {}, userInfos: $ReadOnlyArray = []; if (result.cookieChange) { ({ threadInfos, userInfos } = result.cookieChange); } let sessionChange; if (viewer.cookieInvalidated) { sessionChange = ({ cookieInvalidated: true, threadInfos, userInfos, currentUserInfo: { anonymous: true, }, }: ServerSessionChange); } else { sessionChange = ({ cookieInvalidated: false, threadInfos, userInfos, }: ServerSessionChange); } sessionChange.cookie = viewer.cookiePairString; if (viewer.sessionIdentifierType === sessionIdentifierTypes.BODY_SESSION_ID) { sessionChange.sessionID = viewer.sessionID ? viewer.sessionID : null; } result.cookieChange = sessionChange; } type AnonymousCookieCreationParams = Partial<{ +platformDetails: ?PlatformDetails, +deviceToken: ?string, }>; const defaultPlatformDetails = {}; // The result of this function should not be passed directly to the Viewer // constructor. Instead, it should be passed to viewer.setNewCookie. There are // several fields on AnonymousViewerData that are not set by this function: // sessionIdentifierType, ipAddress, and userAgent. These parameters all depend // on the initial request. If the result of this function is passed to the // Viewer constructor directly, the resultant Viewer object will throw whenever // anybody attempts to access the relevant properties. async function createNewAnonymousCookie( params: AnonymousCookieCreationParams, ): Promise { const { platformDetails, deviceToken } = params; const { platform, ...versions } = platformDetails || defaultPlatformDetails; const versionsString = Object.keys(versions).length > 0 ? JSON.stringify(versions) : null; const time = Date.now(); const cookiePassword = crypto.randomBytes(32).toString('hex'); const cookieHash = getCookieHash(cookiePassword); const [[id]] = await Promise.all([ createIDs('cookies', 1), deviceToken ? clearDeviceToken(deviceToken) : undefined, ]); const cookieRow = [ id, cookieHash, null, platform, time, time, deviceToken, versionsString, ]; const query = SQL` INSERT INTO cookies(id, hash, user, platform, creation_time, last_used, device_token, versions) VALUES ${[cookieRow]} `; await dbQuery(query); return { loggedIn: false, id, platformDetails, deviceToken, cookieID: id, cookiePassword, cookieHash, sessionID: undefined, sessionInfo: null, cookieInsertedThisRequest: true, isScriptViewer: false, }; } type UserCookieCreationParams = { +platformDetails: PlatformDetails, +deviceToken?: ?string, +socialProof?: ?SIWESocialProof, +signedIdentityKeysBlob?: ?SignedIdentityKeysBlob, }; // The result of this function should never be passed directly to the Viewer // constructor. Instead, it should be passed to viewer.setNewCookie. There are // several fields on UserViewerData that are not set by this function: // sessionID, sessionIdentifierType, and ipAddress. These parameters all depend // on the initial request. If the result of this function is passed to the // Viewer constructor directly, the resultant Viewer object will throw whenever // anybody attempts to access the relevant properties. async function createNewUserCookie( userID: string, params: UserCookieCreationParams, ): Promise { const { platformDetails, deviceToken, socialProof, signedIdentityKeysBlob } = params; const { platform, ...versions } = platformDetails || defaultPlatformDetails; const versionsString = Object.keys(versions).length > 0 ? JSON.stringify(versions) : null; const time = Date.now(); const cookiePassword = crypto.randomBytes(32).toString('hex'); const cookieHash = getCookieHash(cookiePassword); const [[cookieID]] = await Promise.all([ createIDs('cookies', 1), deviceToken ? clearDeviceToken(deviceToken) : undefined, ]); const cookieRow = [ cookieID, cookieHash, userID, platform, time, time, deviceToken, versionsString, JSON.stringify(socialProof), signedIdentityKeysBlob ? JSON.stringify(signedIdentityKeysBlob) : null, ]; const query = SQL` INSERT INTO cookies(id, hash, user, platform, creation_time, last_used, device_token, versions, social_proof, signed_identity_keys) VALUES ${[cookieRow]} `; await dbQuery(query); return { loggedIn: true, id: userID, platformDetails, deviceToken, userID, cookieID, sessionID: undefined, sessionInfo: null, cookiePassword, cookieHash, cookieInsertedThisRequest: true, isScriptViewer: false, }; } // This gets called after createNewUserCookie and from websiteResponder. If the // Viewer's sessionIdentifierType is COOKIE_ID then the cookieID is used as the // session identifier; otherwise, a new ID is created for the session. async function setNewSession( viewer: Viewer, calendarQuery: CalendarQuery, initialLastUpdate: number, ): Promise { if (viewer.sessionIdentifierType !== sessionIdentifierTypes.COOKIE_ID) { const [sessionID] = await createIDs('sessions', 1); viewer.setSessionID(sessionID); } await createSession(viewer, calendarQuery, initialLastUpdate); } async function updateCookie(viewer: Viewer) { const time = Date.now(); const { cookieID, cookieHash, cookiePassword } = viewer; const updateObj: { [string]: string | number } = {}; updateObj.last_used = time; if (isBcryptHash(cookieHash)) { updateObj.hash = getCookieHash(cookiePassword); } const query = SQL` UPDATE cookies SET ${updateObj} WHERE id = ${cookieID} `; await dbQuery(query); } function addCookieToJSONResponse( viewer: Viewer, res: $Response, result: Object, expectCookieInvalidation: boolean, ) { if (expectCookieInvalidation) { viewer.cookieInvalidated = false; } if (!viewer.getData().cookieInsertedThisRequest) { ignorePromiseRejections(updateCookie(viewer)); } if (viewer.sessionChanged) { addSessionChangeInfoToResult(viewer, res, result); } } function addCookieToHomeResponse( req: $Request, res: $Response, appURLFacts: AppURLFacts, ) { const { user, anonymous } = req.cookies; if (user) { res.cookie(cookieTypes.USER, user, getCookieOptions(appURLFacts)); } if (anonymous) { res.cookie(cookieTypes.ANONYMOUS, anonymous, getCookieOptions(appURLFacts)); } } function getCookieOptions(appURLFacts: AppURLFacts) { const { baseDomain, basePath, https } = appURLFacts; const domainAsURL = new url.URL(baseDomain); return { domain: domainAsURL.hostname, path: basePath, httpOnly: false, secure: https, maxAge: cookieLifetime, sameSite: 'Strict', }; } async function setCookieSignedIdentityKeysBlob( cookieID: string, signedIdentityKeysBlob: SignedIdentityKeysBlob, ) { const signedIdentityKeysStr = JSON.stringify(signedIdentityKeysBlob); const query = SQL` UPDATE cookies SET signed_identity_keys = ${signedIdentityKeysStr} WHERE id = ${cookieID} `; await dbQuery(query); } // Returns `true` if row with `id = cookieID` exists AND // `signed_identity_keys` is `NULL`. Otherwise, returns `false`. async function isCookieMissingSignedIdentityKeysBlob( cookieID: string, ): Promise { const query = SQL` SELECT signed_identity_keys FROM cookies WHERE id = ${cookieID} `; const [queryResult] = await dbQuery(query); return ( queryResult.length === 1 && queryResult[0].signed_identity_keys === null ); } async function isCookieMissingOlmNotificationsSession( viewer: Viewer, ): Promise { const isDeviceSupportingE2ENotifs = isDeviceType(viewer.platformDetails?.platform) && hasMinCodeVersion(viewer.platformDetails, { native: 222 }); const isWebSupportingE2ENotifs = viewer.platformDetails?.platform === 'web' && hasMinCodeVersion(viewer.platformDetails, { web: 43 }); const isMacOSSupportingE2ENotifs = viewer.platformDetails?.platform === 'macos' && hasMinCodeVersion(viewer.platformDetails, { web: 43, majorDesktop: 9 }); const isWindowsSupportingE2ENotifs = viewer.platformDetails?.platform === 'windows' && hasMinCodeVersion(viewer.platformDetails, { majorDesktop: 10, }); if ( !isDeviceSupportingE2ENotifs && !isWebSupportingE2ENotifs && !isMacOSSupportingE2ENotifs && !isWindowsSupportingE2ENotifs ) { return false; } const query = SQL` SELECT COUNT(*) AS count FROM olm_sessions WHERE cookie_id = ${viewer.cookieID} AND is_content = FALSE `; const [queryResult] = await dbQuery(query); return queryResult[0].count === 0; } async function setCookiePlatform( viewer: Viewer, platform: Platform, ): Promise { const newPlatformDetails = { ...viewer.platformDetails, platform }; viewer.setPlatformDetails(newPlatformDetails); const query = SQL` UPDATE cookies SET platform = ${platform} WHERE id = ${viewer.cookieID} `; await dbQuery(query); } async function setCookiePlatformDetails( viewer: Viewer, platformDetails: PlatformDetails, ): Promise { viewer.setPlatformDetails(platformDetails); const { platform, ...versions } = platformDetails; const versionsString = Object.keys(versions).length > 0 ? JSON.stringify(versions) : null; const query = SQL` UPDATE cookies SET platform = ${platform}, versions = ${versionsString} WHERE id = ${viewer.cookieID} `; await dbQuery(query); } export { fetchViewerForJSONRequest, fetchViewerForSocket, createNewAnonymousCookie, createNewUserCookie, setNewSession, updateCookie, addCookieToJSONResponse, addCookieToHomeResponse, setCookieSignedIdentityKeysBlob, isCookieMissingSignedIdentityKeysBlob, setCookiePlatform, setCookiePlatformDetails, isCookieMissingOlmNotificationsSession, }; diff --git a/keyserver/src/utils/security-utils.js b/keyserver/src/utils/security-utils.js index 491c1d416..bf5dd1d22 100644 --- a/keyserver/src/utils/security-utils.js +++ b/keyserver/src/utils/security-utils.js @@ -1,20 +1,22 @@ // @flow import type { $Request } from 'express'; import { getAppURLFactsFromRequestURL } from './urls.js'; function assertSecureRequest(req: $Request) { const { https, proxy } = getAppURLFactsFromRequestURL(req.originalUrl); + if (!https) { return; } if ( (proxy === 'none' && req.protocol !== 'https') || - (proxy === 'apache' && req.get('X-Forwarded-SSL') !== 'on') + (proxy === 'apache' && req.get('X-Forwarded-SSL') !== 'on') || + (proxy === 'aws' && req.get('X-Forwarded-Proto') !== 'https') ) { throw new Error('insecure request'); } } export { assertSecureRequest }; diff --git a/keyserver/src/utils/urls.js b/keyserver/src/utils/urls.js index c9af924f7..8d8cd3cf5 100644 --- a/keyserver/src/utils/urls.js +++ b/keyserver/src/utils/urls.js @@ -1,105 +1,105 @@ // @flow import invariant from 'invariant'; import { getCommConfig } from 'lib/utils/comm-config.js'; import { values } from 'lib/utils/objects.js'; export type AppURLFacts = { +baseDomain: string, +basePath: string, +https: boolean, +baseRoutePath: string, - +proxy?: 'apache' | 'none', // defaults to apache + +proxy?: 'apache' | 'none' | 'aws', // defaults to apache }; -const validProxies = new Set(['apache', 'none']); +const validProxies = new Set(['apache', 'none', 'aws']); const sitesObj = Object.freeze({ a: 'landing', b: 'webapp', c: 'keyserver', }); export type Site = $Values; const sites: $ReadOnlyArray = values(sitesObj); const cachedURLFacts = new Map(); async function fetchURLFacts(site: Site): Promise { const existing = cachedURLFacts.get(site); if (existing !== undefined) { return existing; } let urlFacts: ?AppURLFacts = await getCommConfig({ folder: 'facts', name: `${site}_url`, }); if (urlFacts) { const { proxy } = urlFacts; urlFacts = { ...urlFacts, proxy: proxy && validProxies.has(proxy) ? proxy : 'apache', }; } cachedURLFacts.set(site, urlFacts); return urlFacts; } async function prefetchAllURLFacts() { await Promise.all(sites.map(fetchURLFacts)); } function getKeyserverURLFacts(): ?AppURLFacts { return cachedURLFacts.get('keyserver'); } function getWebAppURLFacts(): ?AppURLFacts { return cachedURLFacts.get('webapp'); } function getAndAssertKeyserverURLFacts(): AppURLFacts { const urlFacts = getKeyserverURLFacts(); invariant(urlFacts, 'keyserver/facts/keyserver_url.json missing'); return urlFacts; } // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return function getAppURLFactsFromRequestURL(url: string): AppURLFacts { const webAppURLFacts = getWebAppURLFacts(); if (webAppURLFacts && url.startsWith(webAppURLFacts.baseRoutePath)) { return webAppURLFacts; } const keyserverURLFacts = getKeyserverURLFacts(); if (keyserverURLFacts && url.startsWith(keyserverURLFacts.baseRoutePath)) { return keyserverURLFacts; } invariant(false, 'request received but no URL facts are present'); } function getLandingURLFacts(): ?AppURLFacts { return cachedURLFacts.get('landing'); } function getAndAssertLandingURLFacts(): AppURLFacts { const urlFacts = getLandingURLFacts(); invariant(urlFacts, 'keyserver/facts/landing_url.json missing'); return urlFacts; } export type WebAppCorsConfig = { +domain: string }; async function getWebAppCorsConfig(): Promise { const config = await getCommConfig({ folder: 'facts', name: 'webapp_cors', }); return config; } export { prefetchAllURLFacts, getKeyserverURLFacts, getWebAppURLFacts, getAndAssertKeyserverURLFacts, getLandingURLFacts, getAndAssertLandingURLFacts, getAppURLFactsFromRequestURL, getWebAppCorsConfig, }; diff --git a/services/terraform/self-host/keyserver_primary.tf b/services/terraform/self-host/keyserver_primary.tf index 161daf3e8..498039694 100644 --- a/services/terraform/self-host/keyserver_primary.tf +++ b/services/terraform/self-host/keyserver_primary.tf @@ -1,196 +1,196 @@ locals { keyserver_service_image_tag = "0.1" keyserver_service_server_image = "commapp/keyserver:${local.keyserver_service_image_tag}" keyserver_primary_container_name = "keyserver-primary" } resource "aws_cloudwatch_log_group" "keyserver_primary_service" { name = "/ecs/keyserver-primary-task-def" retention_in_days = 7 } output "mariadb_address" { value = aws_db_instance.mariadb.address } resource "aws_ecs_task_definition" "keyserver_primary_service" { network_mode = "awsvpc" family = "keyserver-primary-task-def" requires_compatibilities = ["FARGATE"] task_role_arn = aws_iam_role.ecs_task_role.arn execution_role_arn = aws_iam_role.ecs_task_execution.arn cpu = "2048" memory = "4096" ephemeral_storage { size_in_gib = 40 } container_definitions = jsonencode([ { name = local.keyserver_primary_container_name image = local.keyserver_service_server_image essential = true portMappings = [ { name = "keyserver-port" containerPort = 3000 hostPort = 3000, protocol = "tcp" }, ] environment = [ { name = "REDIS_URL" value = "rediss://${aws_elasticache_serverless_cache.redis.endpoint[0].address}:6379" }, { name = "COMM_NODE_ROLE" value = "primary" }, { name = "COMM_LISTEN_ADDR" value = "0.0.0.0" }, { name = "COMM_DATABASE_HOST" value = "${aws_db_instance.mariadb.address}" }, { name = "COMM_DATABASE_DATABASE" value = "comm" }, { name = "COMM_DATABASE_PORT" value = "3307" }, { name = "COMM_DATABASE_USER" value = "${var.mariadb_username}" }, { name = "COMM_DATABASE_PASSWORD" value = "${var.mariadb_password}" }, { name = "COMM_JSONCONFIG_secrets_user_credentials" value = jsonencode(var.keyserver_user_credentials) }, { name = "COMM_JSONCONFIG_facts_webapp_cors" value = jsonencode({ "domain" : "https://web.comm.app" }) }, { name = "COMM_JSONCONFIG_facts_keyserver_url" value = jsonencode({ "baseDomain" : "https://${var.domain_name}", "basePath" : "/", "baseRoutePath" : "/", - "https" : false, - "proxy" : "none" + "https" : true, + "proxy" : "aws" }) }, { name = "COMM_JSONCONFIG_secrets_identity_service_config", value = jsonencode({ "identitySocketAddr" : "${var.identity_socket_address}" }) }, { name = "COMM_JSONCONFIG_facts_authoritative_keyserver", value = jsonencode(var.authoritative_keyserver_config), } ] logConfiguration = { "logDriver" = "awslogs" "options" = { "awslogs-create-group" = "true" "awslogs-group" = aws_cloudwatch_log_group.keyserver_primary_service.name "awslogs-stream-prefix" = "ecs" "awslogs-region" = "${var.region}" } } linuxParameters = { initProcessEnabled = true } } ]) runtime_platform { cpu_architecture = "ARM64" operating_system_family = "LINUX" } skip_destroy = false } resource "aws_ecs_service" "keyserver_primary_service" { depends_on = [null_resource.create_comm_database] name = "keyserver-primary-service" cluster = aws_ecs_cluster.keyserver_cluster.id task_definition = aws_ecs_task_definition.keyserver_primary_service.arn launch_type = "FARGATE" enable_execute_command = true enable_ecs_managed_tags = true force_new_deployment = true desired_count = 1 deployment_maximum_percent = 100 deployment_minimum_healthy_percent = 0 network_configuration { subnets = local.vpc_subnets security_groups = [aws_security_group.keyserver_service.id] assign_public_ip = true } load_balancer { target_group_arn = aws_lb_target_group.keyserver_service.arn container_name = local.keyserver_primary_container_name container_port = 3000 } deployment_circuit_breaker { enable = true rollback = true } } resource "aws_security_group" "keyserver_service" { name = "keyserver-service-ecs-sg" vpc_id = local.vpc_id # Allow all inbound traffic on port 3000 ingress { from_port = 3000 to_port = 3000 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { description = "Allow inbound traffic from any IPv6 address" from_port = 3000 to_port = 3000 protocol = "tcp" ipv6_cidr_blocks = ["::/0"] } # Allow all outbound traffic egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } lifecycle { create_before_destroy = true } } diff --git a/services/terraform/self-host/keyserver_secondary.tf b/services/terraform/self-host/keyserver_secondary.tf index c40c0f4dc..9854efc17 100644 --- a/services/terraform/self-host/keyserver_secondary.tf +++ b/services/terraform/self-host/keyserver_secondary.tf @@ -1,154 +1,154 @@ locals { keyserver_secondary_container_name = "keyserver-secondary" } resource "aws_cloudwatch_log_group" "keyserver_secondary_service" { name = "/ecs/keyserver-secondary-task-def" retention_in_days = 7 } resource "aws_ecs_task_definition" "keyserver_secondary_service" { depends_on = [aws_ecs_service.keyserver_primary_service] network_mode = "awsvpc" family = "keyserver-secondary-task-def" requires_compatibilities = ["FARGATE"] task_role_arn = aws_iam_role.ecs_task_role.arn execution_role_arn = aws_iam_role.ecs_task_execution.arn cpu = "2048" memory = "4096" ephemeral_storage { size_in_gib = 40 } container_definitions = jsonencode([ { name = local.keyserver_secondary_container_name image = local.keyserver_service_server_image essential = true portMappings = [ { name = "keyserver-port" containerPort = 3000 hostPort = 3000, protocol = "tcp" }, ] environment = [ { name = "REDIS_URL" value = "rediss://${aws_elasticache_serverless_cache.redis.endpoint[0].address}:6379" }, { name = "COMM_NODE_ROLE" value = "secondary" }, { name = "COMM_LISTEN_ADDR" value = "0.0.0.0" }, { name = "COMM_DATABASE_HOST" value = "${aws_db_instance.mariadb.address}" }, { name = "COMM_DATABASE_DATABASE" value = "comm" }, { name = "COMM_DATABASE_PORT" value = "3307" }, { name = "COMM_DATABASE_USER" value = "${var.mariadb_username}" }, { name = "COMM_DATABASE_PASSWORD" value = "${var.mariadb_password}" }, { name = "COMM_JSONCONFIG_secrets_user_credentials" value = jsonencode(var.keyserver_user_credentials) }, { name = "COMM_JSONCONFIG_facts_keyserver_url" value = jsonencode({ "baseDomain" : "https://${var.domain_name}", "basePath" : "/", "baseRoutePath" : "/", - "https" : false, - "proxy" : "none" + "https" : true, + "proxy" : "aws" }) }, { name = "COMM_JSONCONFIG_facts_webapp_cors" value = jsonencode({ "domain" : "https://web.comm.app" }) }, { name = "COMM_JSONCONFIG_secrets_identity_service_config", value = jsonencode({ "identitySocketAddr" : "${var.identity_socket_address}" }) }, { name = "COMM_JSONCONFIG_facts_authoritative_keyserver", value = jsonencode(var.authoritative_keyserver_config), } ] logConfiguration = { "logDriver" = "awslogs" "options" = { "awslogs-create-group" = "true" "awslogs-group" = aws_cloudwatch_log_group.keyserver_secondary_service.name "awslogs-stream-prefix" = "ecs" "awslogs-region" = "${var.region}" } } linuxParameters = { initProcessEnabled = true } } ]) runtime_platform { cpu_architecture = "ARM64" operating_system_family = "LINUX" } skip_destroy = false } resource "aws_ecs_service" "keyserver_secondary_service" { depends_on = [aws_ecs_service.keyserver_primary_service] name = "keyserver-secondary-service" cluster = aws_ecs_cluster.keyserver_cluster.id task_definition = aws_ecs_task_definition.keyserver_secondary_service.arn launch_type = "FARGATE" enable_execute_command = true enable_ecs_managed_tags = true force_new_deployment = true desired_count = 1 network_configuration { subnets = local.vpc_subnets security_groups = [aws_security_group.keyserver_service.id] assign_public_ip = true } load_balancer { target_group_arn = aws_lb_target_group.keyserver_service.arn container_name = local.keyserver_secondary_container_name container_port = 3000 } deployment_circuit_breaker { enable = true rollback = true } }