From bc5fd2c1772bb920eea99d1be5b441349db8921d Mon Sep 17 00:00:00 2001 From: Sergey Petushkov Date: Mon, 16 Dec 2024 18:22:41 +0100 Subject: [PATCH 1/3] chore(compass-web): use ccs for atlas sandbox instead of local proxy --- packages/atlas-service/src/util.ts | 28 +- .../helpers/compass-web-sandbox.ts | 4 - .../src/preferences-schema.ts | 9 +- packages/compass-web/polyfills/net/index.ts | 36 ++- packages/compass-web/sandbox/index.tsx | 5 +- .../compass-web/scripts/electron-proxy.js | 266 ++++++++---------- packages/compass-web/scripts/ws-proxy.js | 59 +--- .../compass-web/src/connection-storage.tsx | 4 +- 8 files changed, 182 insertions(+), 229 deletions(-) diff --git a/packages/atlas-service/src/util.ts b/packages/atlas-service/src/util.ts index 6a7070c1743..48abca07cd4 100644 --- a/packages/atlas-service/src/util.ts +++ b/packages/atlas-service/src/util.ts @@ -122,9 +122,11 @@ export type AtlasServiceConfig = { * Atlas service backend configurations. * - atlas-local: local mms backend (localhost) * - atlas-dev: dev mms backend (cloud-dev.mongodb.com) + * - atlas-qa: qa mms backend (cloud-qa.mongodb.com) * - atlas: mms backend (cloud.mongodb.com) * - web-sandbox-atlas-local: local mms backend + proxy (localhost / proxy prefix) * - web-sandbox-atlas-dev: dev mms backend + proxy (cloud-dev.mongodb.com / proxy prefix) + * - web-sandbox-atlas-qa: qa mms backend + proxy (cloud-qa.mongodb.com / proxy prefix) * - web-sandbox-atlas: mms backend + proxy (cloud.mongodb.com / proxy prefix) */ const config = { @@ -148,6 +150,16 @@ const config = { }, authPortalUrl: 'https://account-dev.mongodb.com/account/login', }, + 'atlas-qa': { + wsBaseUrl: '', + cloudBaseUrl: '', + atlasApiBaseUrl: 'https://cloud-qa.mongodb.com/api/private', + atlasLogin: { + clientId: '0oaq1le5jlzxCuTbu357', + issuer: 'https://auth-qa.mongodb.com/oauth2/default', + }, + authPortalUrl: 'https://account-qa.mongodb.com/account/login', + }, atlas: { wsBaseUrl: '', cloudBaseUrl: '', @@ -159,7 +171,7 @@ const config = { authPortalUrl: 'https://account.mongodb.com/account/login', }, 'web-sandbox-atlas-local': { - wsBaseUrl: 'ws://localhost:1337', + wsBaseUrl: '/ccs', cloudBaseUrl: '/cloud-mongodb-com', atlasApiBaseUrl: 'http://localhost:8080/api/private', atlasLogin: { @@ -169,7 +181,17 @@ const config = { authPortalUrl: 'https://account-dev.mongodb.com/account/login', }, 'web-sandbox-atlas-dev': { - wsBaseUrl: 'ws://localhost:1337', + wsBaseUrl: '/ccs', + cloudBaseUrl: '/cloud-mongodb-com', + atlasApiBaseUrl: 'https://cloud-dev.mongodb.com/api/private', + atlasLogin: { + clientId: '0oaq1le5jlzxCuTbu357', + issuer: 'https://auth-qa.mongodb.com/oauth2/default', + }, + authPortalUrl: 'https://account-dev.mongodb.com/account/login', + }, + 'web-sandbox-atlas-qa': { + wsBaseUrl: '/ccs', cloudBaseUrl: '/cloud-mongodb-com', atlasApiBaseUrl: 'https://cloud-dev.mongodb.com/api/private', atlasLogin: { @@ -179,7 +201,7 @@ const config = { authPortalUrl: 'https://account-dev.mongodb.com/account/login', }, 'web-sandbox-atlas': { - wsBaseUrl: 'ws://localhost:1337', + wsBaseUrl: '/ccs', cloudBaseUrl: '/cloud-mongodb-com', atlasApiBaseUrl: 'https://cloud.mongodb.com/api/private', atlasLogin: { diff --git a/packages/compass-e2e-tests/helpers/compass-web-sandbox.ts b/packages/compass-e2e-tests/helpers/compass-web-sandbox.ts index c299f33d2fd..ad3c0e554e8 100644 --- a/packages/compass-e2e-tests/helpers/compass-web-sandbox.ts +++ b/packages/compass-e2e-tests/helpers/compass-web-sandbox.ts @@ -182,10 +182,6 @@ export async function spawnCompassWebSandboxAndSignInToAtlas( return electronProxyRemote; } - debug('Waiting for x509 cert to propagate to Atlas clusters ...'); - - await fetch(`${sandboxUrl}/x509`); - return electronProxyRemote; } diff --git a/packages/compass-preferences-model/src/preferences-schema.ts b/packages/compass-preferences-model/src/preferences-schema.ts index 2cf49d575b9..a98a7652870 100644 --- a/packages/compass-preferences-model/src/preferences-schema.ts +++ b/packages/compass-preferences-model/src/preferences-schema.ts @@ -52,9 +52,11 @@ export type UserConfigurablePreferences = PermanentFeatureFlags & atlasServiceBackendPreset: | 'atlas-local' | 'atlas-dev' + | 'atlas-qa' | 'atlas' | 'web-sandbox-atlas-local' | 'web-sandbox-atlas-dev' + | 'web-sandbox-atlas-qa' | 'web-sandbox-atlas'; optInDataExplorerGenAIFeatures: boolean; // Features that are enabled by default in Compass, but are disabled in Data @@ -672,6 +674,7 @@ export const storedUserPreferencesProps: Required<{ * Chooses atlas service backend configuration from preset * - atlas-local: local mms backend (http://localhost:8080) * - atlas-dev: dev mms backend (cloud-dev.mongodb.com) + * - atlas-qa: qa mms backend (cloud-qa.mongodb.com) * - atlas: mms backend (cloud.mongodb.com) */ atlasServiceBackendPreset: { @@ -683,11 +686,13 @@ export const storedUserPreferencesProps: Required<{ }, validator: z .enum([ - 'atlas-dev', 'atlas-local', + 'atlas-dev', + 'atlas-qa', 'atlas', - 'web-sandbox-atlas-dev', 'web-sandbox-atlas-local', + 'web-sandbox-atlas-dev', + 'web-sandbox-atlas-qa', 'web-sandbox-atlas', ]) .default('atlas'), diff --git a/packages/compass-web/polyfills/net/index.ts b/packages/compass-web/polyfills/net/index.ts index 01a98f9c7b3..a8f966110f4 100644 --- a/packages/compass-web/polyfills/net/index.ts +++ b/packages/compass-web/polyfills/net/index.ts @@ -2,16 +2,13 @@ import { ipVersion } from 'is-ip'; import type { ConnectionOptions } from 'mongodb-data-service'; import { Duplex } from 'stream'; -const WS_PROXY_PORT = process.env.COMPASS_WEB_WS_PROXY_PORT - ? Number(process.env.COMPASS_WEB_WS_PROXY_PORT) - : 1337; - /** * net.Socket interface that works over the WebSocket connection. For now, only * used when running compass-web in a local sandbox, mms has their own * implementation */ class Socket extends Duplex { + private _tls = false; private _ws: WebSocket | null = null; private _setOptions: { setKeepAlive?: { enabled?: boolean; initialDelay?: number }; @@ -30,17 +27,20 @@ class Socket extends Duplex { lookup?: ConnectionOptions['lookup']; tls?: boolean; }) { - const { wsURL, ...atlasOptions } = lookup?.() ?? {}; - this._ws = new WebSocket(wsURL ?? `ws://localhost:${WS_PROXY_PORT}`); + this._tls = !!options.tls; + const { wsURL, ...atlasOptions } = + lookup?.() ?? ({} as { wsURL?: string; clusterName?: string }); + this._ws = new WebSocket(wsURL ?? '/ws-proxy'); this._ws.binaryType = 'arraybuffer'; this._ws.addEventListener( 'open', () => { const connectMsg = JSON.stringify({ - connectOptions: options, - atlasOptions: - Object.keys(atlasOptions).length > 0 ? atlasOptions : undefined, - setOptions: this._setOptions, + port: options.port, + host: options.host, + tls: options.tls ?? false, + clusterName: atlasOptions.clusterName, + ok: 1, }); setTimeout(() => { this._ws?.send(connectMsg); @@ -59,16 +59,12 @@ class Socket extends Duplex { ({ data }: MessageEvent) => { if (typeof data === 'string') { try { - const { evt, error } = JSON.parse(data) as { - evt: string; - error?: Error; - }; - setTimeout(() => { - this.emit( - evt, - evt === 'error' ? Object.assign(new Error(), error) : undefined - ); - }); + const res = JSON.parse(data) as { preMessageOk: 1 }; + if (res.preMessageOk) { + setTimeout(() => { + this.emit(this._tls ? 'secureConnect' : 'connect'); + }); + } } catch (err) { // eslint-disable-next-line no-console console.error('error parsing proxy message "%s":', data, err); diff --git a/packages/compass-web/sandbox/index.tsx b/packages/compass-web/sandbox/index.tsx index f7fbd05e9f8..fb3f3b46df4 100644 --- a/packages/compass-web/sandbox/index.tsx +++ b/packages/compass-web/sandbox/index.tsx @@ -36,9 +36,10 @@ const App = () => { const atlasServiceSandboxBackendVariant = process.env.COMPASS_WEB_HTTP_PROXY_CLOUD_CONFIG === 'local' ? 'web-sandbox-atlas-local' - : process.env.COMPASS_WEB_HTTP_PROXY_CLOUD_CONFIG === 'dev' || - process.env.COMPASS_WEB_HTTP_PROXY_CLOUD_CONFIG === 'qa' + : process.env.COMPASS_WEB_HTTP_PROXY_CLOUD_CONFIG === 'dev' ? 'web-sandbox-atlas-dev' + : process.env.COMPASS_WEB_HTTP_PROXY_CLOUD_CONFIG === 'qa' + ? 'web-sandbox-atlas-qa' : 'web-sandbox-atlas'; const overrideGenAIEnablement = diff --git a/packages/compass-web/scripts/electron-proxy.js b/packages/compass-web/scripts/electron-proxy.js index 4b670414527..9dec16324d0 100644 --- a/packages/compass-web/scripts/electron-proxy.js +++ b/packages/compass-web/scripts/electron-proxy.js @@ -15,6 +15,8 @@ const { webpack, WebpackDevServer, } = require('@mongodb-js/webpack-config-compass'); +const http = require('http'); +const https = require('https'); const webpackConfig = require('../webpack.config')( { WEBPACK_SERVE: true }, @@ -34,6 +36,10 @@ const WEBPACK_DEV_SERVER_PORT = process.env ? Number(process.env.COMPASS_WEB_HTTP_WEBPACK_DEV_SERVER_PORT) : 4242; +const WEBSOCKET_PROXY_PORT = process.env.COMPASS_WEBSOCKET_PROXY_PORT + ? Number(process.env.COMPASS_WEBSOCKET_PROXY_PORT) + : 1337; + const CLOUD_CONFIG_VARIANTS = { local: { protocol: 'http:', @@ -63,9 +69,6 @@ const CLOUD_HOST = CLOUD_CONFIG_VARIANTS[CLOUD_CONFIG].cloudHost; const CLOUD_ORIGIN = `${CLOUD_CONFIG_VARIANTS[CLOUD_CONFIG].protocol}//${CLOUD_HOST}`; -const TEST_DB_USER = `compass-web-test-user-x509-${Date.now()}`; -const TEST_X509_CERT_PROMISE = new Map(); - function isSignedOutRedirect(location) { if (location) { const redirectLocation = new URL(location, CLOUD_ORIGIN); @@ -167,11 +170,11 @@ class AtlasCloudAuthenticator { }; } - async getCloudHeaders() { + async getCloudHeaders(hostSubdomain = '') { const cookie = (await this.#getCloudSessionCookies()).join('; '); return { cookie, - host: CLOUD_HOST, + host: `${hostSubdomain}${CLOUD_HOST}`, origin: CLOUD_ORIGIN, }; } @@ -242,112 +245,10 @@ class AtlasCloudAuthenticator { } async cleanupAndLogout() { - // When logging out, delete the test user too. If we don't do this now, we - // will lose a chance to do it later due to missing auth - try { - await atlasCloudAuthenticator.deleteTestDBUser(); - } catch (err) { - logger.err('[electron-proxy] failed to remove the test user:', err); - } await session.defaultSession.clearStorageData({ storages: ['cookies', 'localstorage'], }); } - - #createTestDBUser(projectId) { - return this.#fetch(`/nds/${projectId}/users`, { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ - awsIAMType: 'NONE', - db: '$external', - deleteAfterDate: null, - hasScramSha256Auth: false, - hasUserToDNMapping: false, - isEditable: true, - labels: [], - ldapAuthType: 'NONE', - oidcAuthType: 'NONE', - roles: [{ collection: null, db: 'admin', role: 'atlasAdmin' }], - scopes: [], - user: `CN=${TEST_DB_USER}`, - x509Type: 'MANAGED', - }), - }); - } - - async #ensureTestDBUserExists(projectId) { - const users = await this.#fetch(`/nds/${projectId}/users`); - const testCertUser = users.find((user) => { - return ( - user.x509Type === 'MANAGED' && - user.user.replace(/^cn=/i, '') === TEST_DB_USER - ); - }); - const user = testCertUser ?? (await this.#createTestDBUser(projectId)); - let mongodbConfigurationInProgress = true; - let attempts = 0; - while (mongodbConfigurationInProgress && attempts <= 10) { - attempts++; - await new Promise((resolve) => { - setTimeout(resolve, 5_000); - }); - const project = await this.#fetch(`/nds/${projectId}`); - const configuringUserOrRoles = project?.plans?.some((plan) => { - return plan.moves?.some((move) => { - return move.name === 'ConfigureMongoDBForProject'; - }); - }); - mongodbConfigurationInProgress = - project?.state === 'UPDATING' && - (!project?.plans || configuringUserOrRoles); - } - return user; - } - - async deleteTestDBUser() { - const projectId = await this.getProjectId(); - const certUsername = encodeURIComponent( - `CN=${TEST_DB_USER.replace(/^cn=/i, '')}` - ); - return this.#fetch(`/nds/${projectId}/users/$external/${certUsername}`, { - method: 'DELETE', - }); - } - - /** - * - * @returns {Promise} - */ - async getX509Cert() { - const projectId = await this.getProjectId(); - if (TEST_X509_CERT_PROMISE.has(projectId)) { - return TEST_X509_CERT_PROMISE.get(projectId); - } - const promise = (async () => { - try { - const testUser = await this.#ensureTestDBUserExists(projectId); - const certUsername = encodeURIComponent( - `CN=${testUser.user.replace(/^cn=/i, '')}` - ); - const certAuthDb = testUser.db ?? '$external'; - return await this.#fetch( - `/nds/${projectId}/users/${certAuthDb}/${certUsername}/certs?monthsUntilExpiration=1` - ); - } catch (err) { - TEST_X509_CERT_PROMISE.delete(projectId, promise); - logger.error( - '[electron-proxy] failed to issue a cert for the test user', - err - ); - throw err; - } - })(); - TEST_X509_CERT_PROMISE.set(projectId, promise); - return promise; - } } const atlasCloudAuthenticator = new AtlasCloudAuthenticator(); @@ -363,10 +264,6 @@ expressProxy.use('/authenticate', async (req, res) => { try { const { projectId } = await atlasCloudAuthenticator.authenticate(); - // Start issuing the cert to save some time when signing in - void atlasCloudAuthenticator.getX509Cert().catch(() => { - // ignore errors - }); res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify({ projectId })); } catch (err) { @@ -388,25 +285,6 @@ expressProxy.use('/logout', async (req, res) => { res.end(); }); -expressProxy.use('/x509', async (req, res) => { - if (req.method !== 'GET') { - res.statusCode = 400; - res.end(); - return; - } - - try { - await atlasCloudAuthenticator.authenticate(); - const cert = await atlasCloudAuthenticator.getX509Cert(); - res.setHeader('Content-Type', 'text/plain'); - res.send(cert); - } catch (err) { - res.statusCode = 500; - res.send(err.stack ?? err.message); - } - res.end(); -}); - expressProxy.use('/projectId', async (req, res) => { if (req.method !== 'GET') { res.statusCode = 400; @@ -429,21 +307,6 @@ expressProxy.use('/projectId', async (req, res) => { res.end(); }); -expressProxy.use('/create-cluster', async (req, res) => { - if (req.method !== 'GET') { - res.statusCode = 400; - res.end(); - return; - } - - res.setHeader( - 'Location', - `${CLOUD_ORIGIN}/v2/${await atlasCloudAuthenticator.getProjectId()}#/clusters/edit/` - ); - res.statusCode = 303; - res.end(); -}); - // Prefixed proxy to the cloud backend expressProxy.use( '/cloud-mongodb-com', @@ -472,6 +335,7 @@ expressProxy.use( ); // Everything else is proxied directly to webpack-dev-server + expressProxy.use( proxyMiddleware(`http://localhost:${WEBPACK_DEV_SERVER_PORT}`) ); @@ -482,7 +346,112 @@ logger.log('[electron-proxy] starting proxy server on port %s', PROXY_PORT); const proxyServer = expressProxy.listen(PROXY_PORT, 'localhost'); -const websocketProxyServer = createWebSocketProxy(); +/** + * Keeping a track of websocket proxy sockets so that we can clean them up on + * close + */ +const wsProxySockets = new Set(); + +/** + * @param {import('stream').Duplex} socket + * @param {Buffer} head + */ +const prepareSocket = function (socket, head) { + wsProxySockets.add(socket); + socket.setTimeout(0); + socket.setNoDelay(true); + socket.setKeepAlive(true, 0); + if (head?.length) { + socket.unshift(head); + } +}; + +/** + * @param {http.IncomingMessage} res + * @param {Record} headerOverride + */ +const createHeader = function (res, headerOverride = {}) { + let header = `HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}\r\n`; + for (let i = 0; i < res.rawHeaders.length; i += 2) { + const k = res.rawHeaders[i]; + let v = res.rawHeaders[i + 1]; + const override = headerOverride[k.toLowerCase()]; + if (override) { + v = override; + delete headerOverride[k.toLowerCase()]; + } + header += `${k}: ${v}\r\n`; + } + for (const [k, v] of Object.entries(headerOverride)) { + header += `${k}: ${v}\r\n`; + } + header += '\r\n'; + return header; +}; + +/** + * Proxying websocket requests is handled separately from express application + * directly on the proxy server + */ +proxyServer.on('upgrade', async (req, socket, head) => { + const isCCS = Boolean(req.url?.startsWith('/ccs')); + let websocketTarget; + + if (isCCS) { + websocketTarget = `wss://cluster-connection.${CLOUD_HOST}${ + req.url?.replace('/ccs', '') ?? '' + }`; + } else { + websocketTarget = `ws://localhost:${WEBSOCKET_PROXY_PORT}`; + } + + const targetWebsocketURL = new URL(websocketTarget); + const isSecure = targetWebsocketURL.protocol === 'wss:'; + const createRequest = isSecure ? https.request : http.request; + + targetWebsocketURL.protocol = isSecure ? 'https:' : 'http:'; + prepareSocket(socket, head); + + let extraHeaders = {}; + + if (isCCS) { + extraHeaders = await atlasCloudAuthenticator.getCloudHeaders( + 'cluster-connection.' + ); + } + + const proxyReq = createRequest(targetWebsocketURL.toString(), { + method: req.method, + headers: { ...req.headers, ...extraHeaders }, + }); + + proxyReq.on('error', (err) => { + logger.error(err); + socket.end(); + }); + + proxyReq.on('response', (proxyRes) => { + // We only get response back if upgrade didn't happen, so stream back + // whatever server responded (it's probably an error) + socket.write(createHeader(proxyRes)); + proxyRes.pipe(socket); + }); + + proxyReq.on('upgrade', async (proxyRes, proxySocket, proxyHead) => { + // Will not be piped, so we do this manually + proxySocket.on('error', (err) => { + logger.error(err); + socket.end(); + }); + prepareSocket(proxySocket, proxyHead); + socket.write(createHeader(proxyRes)); + proxySocket.pipe(socket).pipe(proxySocket); + }); + + proxyReq.end(); +}); + +const websocketProxyServer = createWebSocketProxy(WEBSOCKET_PROXY_PORT); const webpackCompiler = webpack(webpackConfig); @@ -518,6 +487,11 @@ function cleanupAndExit() { proxyServer.close(resolve); }), + // close all proxy sockets + Array.from(wsProxySockets.values()).map((socket) => { + return socket.destroy(); + }), + // close the driver websocket proxy server Array.from(websocketProxyServer.clients.values()).map((ws) => { return ws.terminate(); diff --git a/packages/compass-web/scripts/ws-proxy.js b/packages/compass-web/scripts/ws-proxy.js index c3e2e6a2561..c41a41780d0 100644 --- a/packages/compass-web/scripts/ws-proxy.js +++ b/packages/compass-web/scripts/ws-proxy.js @@ -12,33 +12,6 @@ function serializeError(err) { } } -const certsMap = new Map(); - -const WEB_PROXY_PORT = process.env.COMPASS_WEB_HTTP_PROXY_PORT - ? Number(process.env.COMPASS_WEB_HTTP_PROXY_PORT) - : 7777; - -async function resolveX509Cert({ projectId, clusterName }) { - if (certsMap.has(projectId)) { - return certsMap.get(projectId); - } - try { - const certUrl = new URL(`http://localhost:${WEB_PROXY_PORT}/x509`); - certUrl.searchParams.set('projectId', projectId); - certUrl.searchParams.set('clusterName', clusterName); - const cert = await fetch(certUrl).then((res) => { - if (!res.ok) { - throw new Error('Failed to resolve x509 cert for the connection'); - } - return res.text(); - }); - certsMap.set(projectId, cert); - return cert; - } catch { - return null; - } -} - /** * Creates a simple passthrough proxy that accepts a websocket connection and * establishes a corresponding socket connection to a server. The connection @@ -65,40 +38,24 @@ function createWebSocketProxy(port = 1337, logger = console) { socket.write(data, 'binary'); } else { // First message before socket is created is with connection info - const { - connectOptions: { tls: useSecureConnection, ...connectOptions }, - setOptions: { setKeepAlive, setTimeout, setNoDelay }, - atlasOptions, - } = JSON.parse(data.toString()); + const { tls: useSecureConnection, ...connectOptions } = JSON.parse( + data.toString() + ); logger.log( 'setting up new%s connection to %s:%s', useSecureConnection ? ' secure' : '', connectOptions.host, connectOptions.port ); - let cert = null; - if (atlasOptions) { - logger.log( - 'detected atlas connection, resolving x509 cert for cluster %s', - atlasOptions.clusterName - ); - cert = await resolveX509Cert(atlasOptions); - } socket = useSecureConnection ? tls.connect({ + servername: connectOptions.host, ...connectOptions, - ...(cert && { cert: cert, key: cert }), }) : net.createConnection(connectOptions); - if (setKeepAlive) { - socket.setKeepAlive(setKeepAlive.enabled, setKeepAlive.initialDelay); - } - if (setTimeout) { - socket.setKeepAlive(setKeepAlive.timeout); - } - if (setNoDelay) { - socket.setKeepAlive(setKeepAlive.noDelay); - } + socket.setKeepAlive(true, 300000); + socket.setTimeout(30000); + socket.setNoDelay(true); const connectEvent = useSecureConnection ? 'secureConnect' : 'connect'; SOCKET_ERROR_EVENT_LIST.forEach((evt) => { socket.on(evt, (err) => { @@ -113,7 +70,7 @@ function createWebSocketProxy(port = 1337, logger = console) { connectOptions.port ); socket.setTimeout(0); - ws.send(JSON.stringify({ evt: connectEvent })); + ws.send(JSON.stringify({ preMessageOk: 1 })); }); socket.on('data', async (data) => { ws.send(data); diff --git a/packages/compass-web/src/connection-storage.tsx b/packages/compass-web/src/connection-storage.tsx index cf3cdf18ec1..5c9f62dbe26 100644 --- a/packages/compass-web/src/connection-storage.tsx +++ b/packages/compass-web/src/connection-storage.tsx @@ -285,7 +285,9 @@ class AtlasCloudConnectionStorage return clusterDescriptions.map((description) => { return buildConnectionInfoFromClusterDescription( - this.atlasService.driverProxyEndpoint(), + this.atlasService.driverProxyEndpoint( + `/clusterConnection/${this.projectId}` + ), this.orgId, this.projectId, description, From fc887634585cc796476aeedcb97c166b75116b6d Mon Sep 17 00:00:00 2001 From: Sergey Petushkov Date: Tue, 17 Dec 2024 17:30:05 +0100 Subject: [PATCH 2/3] chore: clean-up headers override, allow to filter out headers --- .../compass-web/scripts/electron-proxy.js | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/compass-web/scripts/electron-proxy.js b/packages/compass-web/scripts/electron-proxy.js index 9dec16324d0..679654cec22 100644 --- a/packages/compass-web/scripts/electron-proxy.js +++ b/packages/compass-web/scripts/electron-proxy.js @@ -368,17 +368,17 @@ const prepareSocket = function (socket, head) { /** * @param {http.IncomingMessage} res - * @param {Record} headerOverride + * @param {Record} headerOverride */ -const createHeader = function (res, headerOverride = {}) { +const createHeaderStringFromResponse = function (res, headerOverride = {}) { let header = `HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}\r\n`; for (let i = 0; i < res.rawHeaders.length; i += 2) { - const k = res.rawHeaders[i]; - let v = res.rawHeaders[i + 1]; - const override = headerOverride[k.toLowerCase()]; - if (override) { - v = override; - delete headerOverride[k.toLowerCase()]; + const k = res.rawHeaders[i].toLowerCase(); + const override = headerOverride[k]; + const v = override ?? res.rawHeaders[i + 1]; + delete headerOverride[k]; + if (!v) { + continue; } header += `${k}: ${v}\r\n`; } @@ -395,21 +395,15 @@ const createHeader = function (res, headerOverride = {}) { */ proxyServer.on('upgrade', async (req, socket, head) => { const isCCS = Boolean(req.url?.startsWith('/ccs')); - let websocketTarget; + const websocketTarget = isCCS + ? `https://cluster-connection.${CLOUD_HOST}${ + req.url?.replace('/ccs', '') ?? '' + }` + : `http://localhost:${WEBSOCKET_PROXY_PORT}`; + const createRequest = websocketTarget.startsWith('https') + ? https.request + : http.request; - if (isCCS) { - websocketTarget = `wss://cluster-connection.${CLOUD_HOST}${ - req.url?.replace('/ccs', '') ?? '' - }`; - } else { - websocketTarget = `ws://localhost:${WEBSOCKET_PROXY_PORT}`; - } - - const targetWebsocketURL = new URL(websocketTarget); - const isSecure = targetWebsocketURL.protocol === 'wss:'; - const createRequest = isSecure ? https.request : http.request; - - targetWebsocketURL.protocol = isSecure ? 'https:' : 'http:'; prepareSocket(socket, head); let extraHeaders = {}; @@ -420,7 +414,7 @@ proxyServer.on('upgrade', async (req, socket, head) => { ); } - const proxyReq = createRequest(targetWebsocketURL.toString(), { + const proxyReq = createRequest(websocketTarget, { method: req.method, headers: { ...req.headers, ...extraHeaders }, }); @@ -433,7 +427,7 @@ proxyServer.on('upgrade', async (req, socket, head) => { proxyReq.on('response', (proxyRes) => { // We only get response back if upgrade didn't happen, so stream back // whatever server responded (it's probably an error) - socket.write(createHeader(proxyRes)); + socket.write(createHeaderStringFromResponse(proxyRes)); proxyRes.pipe(socket); }); @@ -444,7 +438,13 @@ proxyServer.on('upgrade', async (req, socket, head) => { socket.end(); }); prepareSocket(proxySocket, proxyHead); - socket.write(createHeader(proxyRes)); + socket.write( + createHeaderStringFromResponse(proxyRes, { + 'access-control-allow-credentials': false, + 'access-control-allow-origin': false, + 'set-cookie': false, + }) + ); proxySocket.pipe(socket).pipe(proxySocket); }); From 159f8fb407078ffed784f3ea18ecd6b5a099a84c Mon Sep 17 00:00:00 2001 From: Sergey Petushkov Date: Thu, 9 Jan 2025 09:59:33 +0100 Subject: [PATCH 3/3] chore(web): listen to socket error to avoid uncaught exceptions --- packages/compass-web/scripts/electron-proxy.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/compass-web/scripts/electron-proxy.js b/packages/compass-web/scripts/electron-proxy.js index 679654cec22..5481fa54b83 100644 --- a/packages/compass-web/scripts/electron-proxy.js +++ b/packages/compass-web/scripts/electron-proxy.js @@ -404,6 +404,10 @@ proxyServer.on('upgrade', async (req, socket, head) => { ? https.request : http.request; + socket.on('error', (err) => { + logger.error(err); + }); + prepareSocket(socket, head); let extraHeaders = {}; @@ -430,7 +434,6 @@ proxyServer.on('upgrade', async (req, socket, head) => { socket.write(createHeaderStringFromResponse(proxyRes)); proxyRes.pipe(socket); }); - proxyReq.on('upgrade', async (proxyRes, proxySocket, proxyHead) => { // Will not be piped, so we do this manually proxySocket.on('error', (err) => { @@ -512,6 +515,14 @@ function cleanupAndExit() { }); } +process.on('uncaughtExceptionMonitor', (err, origin) => { + logger.error( + 'Uncaught exception (caused by "%s"): ', + origin, + err.stack ?? err + ); +}); + electronApp.whenReady().then(async () => { // Create an empty browser window so that webdriver session can be // immediately get attached to something without failing