From 8207eb9f1ae156de31c479986caa4c5df2336580 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 15 Aug 2024 16:08:23 +0200 Subject: [PATCH 1/2] fix(compass,atlas-service): apply proxy to main thread HTTP calls COMPASS-8165 --- package-lock.json | 35 ++----------- packages/atlas-service/package.json | 5 +- packages/atlas-service/src/main.spec.ts | 25 ++-------- packages/atlas-service/src/main.ts | 49 ++++++++----------- .../compass-utils/src/cancellable-promise.ts | 5 +- packages/compass/src/main/application.ts | 25 +++++++++- .../compass/src/main/auto-update-manager.ts | 8 +-- 7 files changed, 62 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index d69a6ee4828..2ba942a37fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44319,18 +44319,17 @@ "@mongodb-js/compass-user-data": "^0.3.3", "@mongodb-js/compass-utils": "^0.6.9", "@mongodb-js/devtools-connect": "^3.2.5", + "@mongodb-js/devtools-proxy-support": "^0.3.5", "@mongodb-js/oidc-plugin": "^1.0.0", "compass-preferences-model": "^2.26.0", "electron": "^29.4.5", "hadron-app-registry": "^9.2.2", "hadron-ipc": "^3.2.20", "lodash": "^4.17.21", - "node-fetch": "^2.7.0", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", - "redux-thunk": "^2.4.2", - "system-ca": "^2.0.0" + "redux-thunk": "^2.4.2" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.1.4", @@ -44360,25 +44359,6 @@ "node": ">=0.3.1" } }, - "packages/atlas-service/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "packages/atlas-service/node_modules/sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", @@ -56160,6 +56140,7 @@ "@mongodb-js/compass-user-data": "^0.3.3", "@mongodb-js/compass-utils": "^0.6.9", "@mongodb-js/devtools-connect": "^3.2.5", + "@mongodb-js/devtools-proxy-support": "^0.3.5", "@mongodb-js/eslint-config-compass": "^1.1.4", "@mongodb-js/mocha-config-compass": "^1.3.10", "@mongodb-js/oidc-plugin": "^1.0.0", @@ -56178,7 +56159,6 @@ "hadron-ipc": "^3.2.20", "lodash": "^4.17.21", "mocha": "^10.2.0", - "node-fetch": "^2.7.0", "nyc": "^15.1.0", "prettier": "^2.7.1", "react": "^17.0.2", @@ -56186,7 +56166,6 @@ "redux": "^4.2.1", "redux-thunk": "^2.4.2", "sinon": "^9.2.3", - "system-ca": "^2.0.0", "typescript": "^5.0.4" }, "dependencies": { @@ -56196,14 +56175,6 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, "sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", diff --git a/packages/atlas-service/package.json b/packages/atlas-service/package.json index b446bf422cc..9f8b4f481a4 100644 --- a/packages/atlas-service/package.json +++ b/packages/atlas-service/package.json @@ -79,17 +79,16 @@ "@mongodb-js/compass-user-data": "^0.3.3", "@mongodb-js/compass-utils": "^0.6.9", "@mongodb-js/devtools-connect": "^3.2.5", + "@mongodb-js/devtools-proxy-support": "^0.3.5", "@mongodb-js/oidc-plugin": "^1.0.0", "hadron-app-registry": "^9.2.2", "compass-preferences-model": "^2.26.0", "electron": "^29.4.5", "hadron-ipc": "^3.2.20", "lodash": "^4.17.21", - "node-fetch": "^2.7.0", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", - "redux-thunk": "^2.4.2", - "system-ca": "^2.0.0" + "redux-thunk": "^2.4.2" } } diff --git a/packages/atlas-service/src/main.spec.ts b/packages/atlas-service/src/main.spec.ts index 8fa186d89d2..cd89272f83d 100644 --- a/packages/atlas-service/src/main.spec.ts +++ b/packages/atlas-service/src/main.spec.ts @@ -85,11 +85,12 @@ describe('CompassAuthServiceMain', function () { createHandle: sandbox.stub(), }; CompassAuthService['fetch'] = mockFetch as any; + CompassAuthService['httpClient'] = { fetch: mockFetch } as any; CompassAuthService['createMongoDBOIDCPlugin'] = () => mockOidcPlugin; CompassAuthService['config'] = defaultConfig; - await CompassAuthService['setupPlugin'](); + CompassAuthService['setupPlugin'](); CompassAuthService['attachOidcPluginLoggerEvents'](); preferences = await createSandboxFromDefaultPreferences(); @@ -289,27 +290,9 @@ describe('CompassAuthServiceMain', function () { CompassAuthService as any, 'setupPlugin' ); - await CompassAuthService.init(preferences); + await CompassAuthService.init(preferences, {} as any); expect(setupPluginSpy).to.have.been.calledOnce; }); - - it('should pass the system ca to the plugin as a custom http option', async function () { - const createOIDCPluginSpy = sandbox.spy( - CompassAuthService as any, - 'createMongoDBOIDCPlugin' - ); - await CompassAuthService.init(preferences); - expect(createOIDCPluginSpy).to.have.been.calledOnce; - try { - expect( - createOIDCPluginSpy.firstCall.args[0].customHttpOptions.ca - ).to.include('-----BEGIN CERTIFICATE-----'); - } catch (e) { - throw new Error( - 'Expected ca to be included in the customHttpOptions, but it was not.' - ); - } - }); }); describe('with networkTraffic turned off', function () { @@ -346,7 +329,7 @@ describe('CompassAuthServiceMain', function () { CompassAuthService['currentUser'] = { sub: '1234', } as any; - await CompassAuthService.init(preferences); + await CompassAuthService.init(preferences, {} as any); CompassAuthService['config'] = defaultConfig; expect(getListenerCount(logger)).to.eq(27); // We did all preparations, reset sinon history for easier assertions diff --git a/packages/atlas-service/src/main.ts b/packages/atlas-service/src/main.ts index 61a993e9d08..028a04ece9b 100644 --- a/packages/atlas-service/src/main.ts +++ b/packages/atlas-service/src/main.ts @@ -11,11 +11,7 @@ import { hookLoggerToMongoLogWriter as oidcPluginHookLoggerToMongoLogWriter, } from '@mongodb-js/oidc-plugin'; import { oidcServerRequestHandler } from '@mongodb-js/devtools-connect'; -import { systemCertsAsync } from 'system-ca'; -import type { Options as SystemCAOptions } from 'system-ca'; -import type { RequestInfo, RequestInit, Response } from 'node-fetch'; -import https from 'https'; -import nodeFetch from 'node-fetch'; +import type { Agent } from 'https'; import type { IntrospectInfo, AtlasUserInfo, AtlasServiceConfig } from './util'; import { throwIfAborted } from '@mongodb-js/compass-utils'; import type { HadronIpcMain } from 'hadron-ipc'; @@ -27,6 +23,7 @@ import { OidcPluginLogger } from './oidc-plugin-logger'; import { spawn } from 'child_process'; import { getAtlasConfig } from './util'; import { createIpcTrack } from '@mongodb-js/compass-telemetry'; +import type { RequestInit, Response } from '@mongodb-js/devtools-proxy-support'; const { log } = createLogger('COMPASS-ATLAS-SERVICE'); const track = createIpcTrack(); @@ -36,20 +33,16 @@ const redirectRequestHandler = oidcServerRequestHandler.bind(null, { productDocsLink: 'https://www.mongodb.com/docs/compass', }); -async function getSystemCA() { - // It is possible for OIDC login flow to fail if system CA certs are different from - // the ones packaged with the application. To avoid this, we include the system CA - // certs in the OIDC plugin options. See COMPASS-7950 for more details. - const systemCAOpts: SystemCAOptions = { includeNodeCertificates: true }; - const ca = await systemCertsAsync(systemCAOpts); - return ca.join('\n'); -} - const TOKEN_TYPE_TO_HINT = { accessToken: 'access_token', refreshToken: 'refresh_token', } as const; +interface CompassAuthHTTPClient { + agent: Agent | undefined; + fetch: (url: string, init: RequestInit) => Promise; +} + export class CompassAuthService { private constructor() { // singleton @@ -57,6 +50,8 @@ export class CompassAuthService { private static initPromise: Promise | null = null; + private static httpClient: CompassAuthHTTPClient; + private static oidcPluginLogger = new OidcPluginLogger(); private static plugin: MongoDBOIDCPlugin | null = null; @@ -66,7 +61,7 @@ export class CompassAuthService { private static signInPromise: Promise | null = null; private static fetch = async ( - url: RequestInfo, + url: string, init: RequestInit = {} ): Promise => { await this.initPromise; @@ -79,15 +74,7 @@ export class CompassAuthService { { url } ); try { - const res = await nodeFetch(url, { - // Tests use 'http'. - ...(url.toString().includes('https') - ? { - agent: new https.Agent({ - ca: await getSystemCA(), - }), - } - : {}), + const res = await this.httpClient.fetch(url, { ...init, headers: { ...init.headers, @@ -145,7 +132,7 @@ export class CompassAuthService { private static createMongoDBOIDCPlugin = createMongoDBOIDCPlugin; - private static async setupPlugin(serializedState?: string) { + private static setupPlugin(serializedState?: string) { this.plugin = this.createMongoDBOIDCPlugin({ redirectServerRequestHandler: (data) => { if (data.result === 'redirecting') { @@ -167,7 +154,7 @@ export class CompassAuthService { logger: this.oidcPluginLogger, serializedState, customHttpOptions: { - ca: await getSystemCA(), + agent: this.httpClient.agent, }, }); oidcPluginHookLoggerToMongoLogWriter( @@ -177,7 +164,11 @@ export class CompassAuthService { ); } - static init(preferences: PreferencesAccess): Promise { + static init( + preferences: PreferencesAccess, + httpClient: CompassAuthHTTPClient + ): Promise { + this.httpClient = httpClient; this.preferences = preferences; this.config = getAtlasConfig(preferences); return (this.initPromise ??= (async () => { @@ -199,7 +190,7 @@ export class CompassAuthService { { config: this.config } ); const serializedState = await this.secretStore.getState(); - await this.setupPlugin(serializedState); + this.setupPlugin(serializedState); })()); } @@ -318,7 +309,7 @@ export class CompassAuthService { this.attachOidcPluginLoggerEvents(); // Destroy old plugin and setup new one await this.plugin?.destroy(); - await this.setupPlugin(); + this.setupPlugin(); // Revoke tokens. Revoking refresh token will also revoke associated access // tokens // https://developer.okta.com/docs/guides/revoke-tokens/main/#revoke-an-access-token-or-a-refresh-token diff --git a/packages/compass-utils/src/cancellable-promise.ts b/packages/compass-utils/src/cancellable-promise.ts index 5a8bf689e93..6fe4d4cb702 100644 --- a/packages/compass-utils/src/cancellable-promise.ts +++ b/packages/compass-utils/src/cancellable-promise.ts @@ -5,7 +5,10 @@ class AbortError extends Error { name = 'AbortError'; } -export const throwIfAborted = (signal?: AbortSignal) => { +export const throwIfAborted = (signal?: { + aborted: boolean; + reason?: Error; +}) => { if (signal?.aborted) { throw signal.reason ?? createCancelError(); } diff --git a/packages/compass/src/main/application.ts b/packages/compass/src/main/application.ts index 7fdd9eca885..bb792f8d221 100644 --- a/packages/compass/src/main/application.ts +++ b/packages/compass/src/main/application.ts @@ -28,7 +28,14 @@ import { getCompassMainConnectionStorage, } from '@mongodb-js/connection-storage/main'; import { createIpcTrack } from '@mongodb-js/compass-telemetry'; +import type { + AgentWithInitialize, + RequestInit, + Response, +} from '@mongodb-js/devtools-proxy-support'; import { + createAgent, + createFetch, extractProxySecrets, translateToElectronProxyConfig, } from '@mongodb-js/devtools-proxy-support'; @@ -61,6 +68,12 @@ const hasConfig = ( return !!Object.keys(globalPreferences[source]).length; }; +// The properties of this object are changed when proxy options change +interface CompassProxyClient { + agent: AgentWithInitialize | undefined; + fetch: (url: string, fetchOptions?: RequestInit) => Promise; +} + class CompassApplication { private constructor() { // marking constructor as private to disallow usage @@ -71,6 +84,7 @@ class CompassApplication { private static initPromise: Promise | null = null; private static mode: CompassApplicationMode | null = null; public static preferences: PreferencesAccess; + public static httpClient: CompassProxyClient; private static async _init( mode: CompassApplicationMode, @@ -168,7 +182,7 @@ class CompassApplication { } private static async setupCompassAuthService() { - await CompassAuthService.init(this.preferences); + await CompassAuthService.init(this.preferences, this.httpClient); this.addExitHandler(() => { return CompassAuthService.onExit(); }); @@ -288,6 +302,15 @@ class CompassApplication { const proxyOptions = proxyPreferenceToProxyOptions(value); await app.whenReady(); await target.setProxy(translateToElectronProxyConfig(proxyOptions)); + + const agent = createAgent(proxyOptions); + const fetch = createFetch(agent || {}); + this.httpClient?.agent?.destroy(); + this.httpClient = Object.assign(this.httpClient ?? {}, { + agent, + fetch, + }); + log.info(mongoLogId(1_001_000_327), logContext, 'Configured proxy', { options: extractProxySecrets(proxyOptions).proxyOptions, }); diff --git a/packages/compass/src/main/auto-update-manager.ts b/packages/compass/src/main/auto-update-manager.ts index 1c4f19bd523..bbdd0b4316e 100644 --- a/packages/compass/src/main/auto-update-manager.ts +++ b/packages/compass/src/main/auto-update-manager.ts @@ -5,7 +5,6 @@ import COMPASS_ICON from './icon'; import type { FeedURLOptions } from 'electron'; import { app, dialog, BrowserWindow, autoUpdater, shell } from 'electron'; import { setTimeout as wait } from 'timers/promises'; -import fetch from 'node-fetch'; import path from 'path'; import fs from 'fs'; import dl from 'electron-dl'; @@ -15,6 +14,7 @@ import semver from 'semver'; import type { PreferencesAccess } from 'compass-preferences-model'; import { getOsInfo } from '@mongodb-js/get-os-info'; import { createIpcTrack } from '@mongodb-js/compass-telemetry'; +import type { Response } from '@mongodb-js/devtools-proxy-support'; const { log, mongoLogId, debug } = createLogger('COMPASS-AUTO-UPDATES'); const track = createIpcTrack(); @@ -579,6 +579,7 @@ class CompassAutoUpdateManager { private static initCalled = false; private static state = AutoUpdateManagerState.Initial; + private static fetch: (url: string) => Promise; static autoUpdateOptions: AutoUpdateManagerOptions; static preferences: PreferencesAccess; @@ -618,12 +619,12 @@ class CompassAutoUpdateManager { to: string; } | null> { try { - const response = await fetch(await this.getUpdateCheckURL()); + const response = await this.fetch((await this.getUpdateCheckURL()).href); if (response.status !== 200) { return null; } try { - return await response.json(); + return (await response.json()) as any; } catch (err) { log.warn( mongoLogId(1_001_000_163), @@ -713,6 +714,7 @@ class CompassAutoUpdateManager { compassApp: typeof CompassApplication, options: Partial = {} ): void { + this.fetch = (url: string) => compassApp.httpClient.fetch(url); compassApp.addExitHandler(() => { this.stop(); return Promise.resolve(); From b1359e095ef04abfb649e195a9317c95f9ae5502 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 16 Aug 2024 16:05:26 +0200 Subject: [PATCH 2/2] fixup: depcheck --- package-lock.json | 33 --------------------------------- packages/compass/package.json | 1 - 2 files changed, 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ba942a37fc..170c4e372cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44523,7 +44523,6 @@ "mongodb-instance-model": "^12.23.3", "mongodb-log-writer": "^1.4.2", "mongodb-ns": "^2.4.2", - "node-fetch": "^2.7.0", "react": "^17.0.2", "react-dom": "^17.0.2", "resolve-mongodb-srv": "^1.1.5", @@ -47986,26 +47985,6 @@ "url": "https://opencollective.com/sinon" } }, - "packages/compass/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "packages/connection-form": { "name": "@mongodb-js/connection-form", "version": "1.36.0", @@ -80693,7 +80672,6 @@ "mongodb-instance-model": "^12.23.3", "mongodb-log-writer": "^1.4.2", "mongodb-ns": "^2.4.2", - "node-fetch": "^2.7.0", "os-dns-native": "^1.2.1", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -80706,17 +80684,6 @@ "web-vitals": "^2.1.2", "win-export-certificate-and-key": "^2.0.1", "winreg-ts": "^1.0.4" - }, - "dependencies": { - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - } } }, "mongodb-connection-string-url": { diff --git a/packages/compass/package.json b/packages/compass/package.json index 8081cc1bbf7..3fe3e12b81f 100644 --- a/packages/compass/package.json +++ b/packages/compass/package.json @@ -265,7 +265,6 @@ "mongodb-instance-model": "^12.23.3", "mongodb-log-writer": "^1.4.2", "mongodb-ns": "^2.4.2", - "node-fetch": "^2.7.0", "react": "^17.0.2", "react-dom": "^17.0.2", "resolve-mongodb-srv": "^1.1.5",