diff --git a/src/authInfo.ts b/src/authInfo.ts index a1f41169fc..cd761eee07 100644 --- a/src/authInfo.ts +++ b/src/authInfo.ts @@ -6,6 +6,7 @@ */ import { createHash, randomBytes } from 'crypto'; +import { URL } from 'url'; import * as dns from 'dns'; import { resolve as pathResolve } from 'path'; import { basename, extname } from 'path'; @@ -45,6 +46,7 @@ import { Logger } from './logger'; import { SfdxError, SfdxErrorConfig } from './sfdxError'; import { fs } from './util/fs'; import { sfdc } from './util/sfdc'; +import { MyDomainResolver } from './status/myDomainResolver'; /** * Fields for authorization, org, and local information. @@ -203,7 +205,32 @@ export enum SfdcUrl { PRODUCTION = 'https://login.salesforce.com', } -function getJwtAudienceUrl(options: OAuth2Options & { createdOrgInstance?: string }) { +function isSandboxUrl(options: OAuth2Options & { createdOrgInstance?: string }): boolean { + const createdOrgInstance = getString(options, 'createdOrgInstance', '').trim().toLowerCase(); + const loginUrl = options.loginUrl ?? ''; + return ( + /^cs|s$/gi.test(createdOrgInstance) || + /sandbox\.my\.salesforce\.com/gi.test(loginUrl) || // enhanced domains >= 230 + /(cs[0-9]+(\.my|)\.salesforce\.com)/gi.test(loginUrl) || // my domains on CS instance OR CS instance without my domain + /([a-z]{3}[0-9]+s\.sfdc-.+\.salesforce\.com)/gi.test(loginUrl) || // falcon sandbox ex: usa2s.sfdc-whatever.salesforce.com + /([a-z]{3}[0-9]+s\.sfdc-.+\.force\.com)/gi.test(loginUrl) || // falcon sandbox ex: usa2s.sfdc-whatever.salesforce.com + urlParse(loginUrl).hostname === 'test.salesforce.com' + ); +} + +async function resolvesToSandbox(options: OAuth2Options & { createdOrgInstance?: string }): Promise { + if (isSandboxUrl(options)) { + return true; + } + let cnames: string[] = []; + if (options.loginUrl) { + const myDomainResolver = await MyDomainResolver.create({ url: new URL(options.loginUrl) }); + cnames = await myDomainResolver.getCnames(); + } + return cnames.some((cname) => isSandboxUrl({ ...options, loginUrl: cname })); +} + +async function getJwtAudienceUrl(options: OAuth2Options & { createdOrgInstance?: string }) { // environment variable is used as an override if (process.env.SFDX_AUDIENCE_URL) { return process.env.SFDX_AUDIENCE_URL; @@ -214,19 +241,12 @@ function getJwtAudienceUrl(options: OAuth2Options & { createdOrgInstance?: strin return options.loginUrl; } - const createdOrgInstance = getString(options, 'createdOrgInstance', '').trim().toLowerCase(); - const loginUrlLowercased = options.loginUrl?.toLowerCase(); - if ( - createdOrgInstance.startsWith('cs') || - createdOrgInstance.endsWith('s') || - loginUrlLowercased?.includes('sandbox.my.salesforce.com') || // enhanced domains >= 230 - loginUrlLowercased?.match(/(cs[0-9]+(\.my|)\.salesforce\.com)/g) || // my domains on CS instance OR CS instance without my domain - loginUrlLowercased?.match(/([a-z]{3}[0-9]+s\.sfdc-.+\.salesforce\.com)/g) || // falcon sandbox ex: usa2s.sfdc-whatever.salesforce.com - (options.loginUrl && urlParse(options.loginUrl).hostname === 'test.salesforce.com') - ) { + if (await resolvesToSandbox(options)) { return SfdcUrl.SANDBOX; } - if (createdOrgInstance.startsWith('gs1') || options.loginUrl?.match(/(gs1.my.salesforce.com)/g)) { + + const createdOrgInstance = getString(options, 'createdOrgInstance', '').trim().toLowerCase(); + if (/^gs1/gi.test(createdOrgInstance) || /(gs1.my.salesforce.com)/gi.test(options.loginUrl ?? '')) { return 'https://gs1.salesforce.com'; } @@ -925,7 +945,7 @@ export class AuthInfo extends AsyncCreatable { // Build OAuth config for a JWT auth flow private async buildJwtConfig(options: OAuth2Options): Promise { const privateKeyContents = await fs.readFile(ensure(options.privateKey), 'utf8'); - const audienceUrl = getJwtAudienceUrl(options); + const audienceUrl = await getJwtAudienceUrl(options); const jwtToken = jwt.sign( { iss: options.clientId, diff --git a/src/status/myDomainResolver.ts b/src/status/myDomainResolver.ts index 453dc0f889..540a040cd5 100644 --- a/src/status/myDomainResolver.ts +++ b/src/status/myDomainResolver.ts @@ -5,7 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { lookup } from 'dns'; +import { lookup, resolveCname } from 'dns'; import { URL } from 'url'; import { promisify } from 'util'; @@ -108,6 +108,17 @@ export class MyDomainResolver extends AsyncOptionalCreatable { + try { + await this.resolve(); + return await promisify(resolveCname)(this.options.url.host); + } catch (e) { + this.logger.debug(`An error occurred trying to resolve: ${this.options.url.host}`); + this.logger.debug(`Error: ${e.message}`); + return []; + } + } + /** * Used to initialize asynchronous components. */ diff --git a/test/unit/authInfoTest.ts b/test/unit/authInfoTest.ts index e5d4ff09ec..d5591e269e 100644 --- a/test/unit/authInfoTest.ts +++ b/test/unit/authInfoTest.ts @@ -6,9 +6,10 @@ */ import * as dns from 'dns'; import * as pathImport from 'path'; +import { URL } from 'url'; import { cloneJson, env, includes, set } from '@salesforce/kit'; import { spyMethod, stubMethod } from '@salesforce/ts-sinon'; -import { AnyJson, ensureString, getJsonMap, getString, JsonMap, toJsonMap } from '@salesforce/ts-types'; +import { AnyFunction, AnyJson, ensureString, getJsonMap, getString, JsonMap, toJsonMap } from '@salesforce/ts-types'; import { assert, expect } from 'chai'; import { OAuth2, OAuth2Options } from 'jsforce'; // @ts-ignore @@ -73,7 +74,7 @@ class MetaAuthDataMock { private _refreshToken = 'authInfoTest_refresh_token'; private _encryptedRefreshToken: string = this._refreshToken; private _clientId = 'authInfoTest_client_id'; - private _loginUrl = 'authInfoTest_login_url'; + private _loginUrl = 'https://foo.bar.baz'; private _jwtUsername = 'authInfoTest_username_JWT'; private _redirectUri = 'http://localhost:1717/OauthRedirect'; private _authCode = 'authInfoTest_authCode'; @@ -763,7 +764,9 @@ describe('AuthInfo', () => { stubMethod($$.SANDBOX, dns, 'lookup').callsFake((url: string | Error, done: (v: Error) => {}) => done(new Error('authInfoTest_ERROR_MSG')) ); - + stubMethod($$.SANDBOX, dns, 'resolveCname').callsFake((host: string, callback: AnyFunction) => { + callback(null, []); + }); // Create the JWT AuthInfo instance const authInfo = await AuthInfo.create({ username, @@ -1405,9 +1408,9 @@ describe('AuthInfo', () => { }); describe('getAuthorizationUrl()', () => { - let scope; + let scope: string; beforeEach(() => { - scope = env.getString('SFDX_AUTH_SCOPES'); + scope = env.getString('SFDX_AUTH_SCOPES', ''); }); afterEach(() => { env.setString('SFDX_AUTH_SCOPES', scope); @@ -1703,8 +1706,6 @@ describe('AuthInfo', () => { 'https://test.salesforce.com' ); }); - it; - it('should use the correct audience URL for scratch orgs without domains (capitalized)', async () => { await runTest({ loginUrl: 'https://CS17.salesforce.com' }, 'https://test.salesforce.com'); }); @@ -1740,6 +1741,22 @@ describe('AuthInfo', () => { it('should use the correct audience URL for production enhanced domains', async () => { await runTest({ loginUrl: 'https://customdomain.my.salesforce.com' }, 'https://login.salesforce.com'); }); + it('should use correct audience url derived from cname in salesforce.com', async () => { + const sandboxNondescriptUrl = new URL('https://efficiency-flow-2380-dev-ed.my.salesforce.com'); + const usa3sVIP = new URL('https://usa3s.sfdc-ypmv18.salesforce.com'); + $$.SANDBOX.stub(dns, 'resolveCname').callsFake((host: string, callback: AnyFunction) => { + callback(null, [usa3sVIP.host]); + }); + await runTest({ loginUrl: sandboxNondescriptUrl.toString() }, 'https://test.salesforce.com'); + }); + it('should use correct audience url derived from cname in force.com', async () => { + const sandboxNondescriptUrl = new URL('https://efficiency-flow-2380-dev-ed.my.salesforce.com'); + const usa3sVIP = new URL('https://usa3s.sfdc-ypmv18.force.com'); + $$.SANDBOX.stub(dns, 'resolveCname').callsFake((host: string, callback: AnyFunction) => { + callback(null, [usa3sVIP.host]); + }); + await runTest({ loginUrl: sandboxNondescriptUrl.toString() }, 'https://test.salesforce.com'); + }); }); describe('getDefaultInstanceUrl', () => { @@ -2009,7 +2026,7 @@ describe('AuthInfo', () => { }); }); describe('Handle User Get Errors', () => { - let authCodeConfig; + let authCodeConfig: any; beforeEach(async () => { authCodeConfig = { authCode: testMetadata.authCode, diff --git a/test/unit/status/myDomainResolverTest.ts b/test/unit/status/myDomainResolverTest.ts index 8b6063dd7e..d07959b971 100644 --- a/test/unit/status/myDomainResolverTest.ts +++ b/test/unit/status/myDomainResolverTest.ts @@ -78,4 +78,69 @@ describe('myDomainResolver', () => { expect(e.name).to.equal('MyDomainResolverTimeoutError'); } }); + + it('should resolve localhost', async () => { + const options: MyDomainResolver.Options = { + url: new URL('http://ghostbusters.internal.salesforce.com'), + }; + const resolver: MyDomainResolver = await MyDomainResolver.create(options); + const ip = await resolver.resolve(); + expect(ip).to.be.equal('127.0.0.1'); + expect(lookupAsyncSpy.callCount).to.be.equal(0); + }); +}); +describe('cname resolver', () => { + const TEST_IP = '1.1.1.1'; + const sandboxCs = 'https://site-ruby-9820-dev-ed.cs50.my.salesforce.com'; + const sandboxNondescript = 'https://efficiency-flow-2380-dev-ed.my.salesforce.com'; + const sandboxCsUrl = new URL(sandboxCs); + const sandboxNondescriptUrl = new URL(sandboxNondescript); + const usa3sVIP = new URL('https://usa3s.sfdc-ypmv18.salesforce.com'); + + beforeEach(() => { + $$.SANDBOX.stub(dns, 'lookup').callsFake((host: string, callback: AnyFunction) => { + const isPositiveComplete = host === sandboxCsUrl.host || host === sandboxNondescriptUrl.host; + if (isPositiveComplete) { + callback(null, { address: TEST_IP }); + } else { + callback(new Error()); + } + }); + $$.SANDBOX.stub(dns, 'resolveCname').callsFake((host: string, callback: AnyFunction) => { + if (host === new URL(sandboxCs).host) { + callback(null, [sandboxCsUrl.host]); + } else if (host === sandboxNondescriptUrl.host) { + callback(null, [usa3sVIP.host]); + } else { + callback(new Error()); + } + }); + }); + + it('should resolve cname to same url', async () => { + const options: MyDomainResolver.Options = { + url: sandboxCsUrl, + }; + const resolver: MyDomainResolver = await MyDomainResolver.create(options); + const cnames = await resolver.getCnames(); + expect(cnames).to.be.deep.equal([sandboxCsUrl.host]); + }); + + it('should resolve cname to usa3s', async () => { + const options: MyDomainResolver.Options = { + url: sandboxNondescriptUrl, + }; + const resolver: MyDomainResolver = await MyDomainResolver.create(options); + const cnames = await resolver.getCnames(); + expect(cnames).to.be.deep.equal([usa3sVIP.host]); + }); + + it('should not resolve cname', async () => { + const options: MyDomainResolver.Options = { + url: new URL('https://foo.bar.baz.com'), + }; + const resolver: MyDomainResolver = await MyDomainResolver.create(options); + const cnames = await resolver.getCnames(); + expect(cnames).to.be.deep.equal([]); + }); });