Skip to content

Commit

Permalink
fix: qualify a sandbox url via cname lookup (#385)
Browse files Browse the repository at this point in the history
  • Loading branch information
peternhale authored Mar 8, 2021
1 parent 5f17bf5 commit 3e27623
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 22 deletions.
46 changes: 33 additions & 13 deletions src/authInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<boolean> {
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;
Expand All @@ -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';
}

Expand Down Expand Up @@ -925,7 +945,7 @@ export class AuthInfo extends AsyncCreatable<AuthInfo.Options> {
// Build OAuth config for a JWT auth flow
private async buildJwtConfig(options: OAuth2Options): Promise<AuthFields> {
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,
Expand Down
13 changes: 12 additions & 1 deletion src/status/myDomainResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -108,6 +108,17 @@ export class MyDomainResolver extends AsyncOptionalCreatable<MyDomainResolver.Op
return ensureString(await client.subscribe());
}

public async getCnames(): Promise<string[]> {
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.
*/
Expand Down
33 changes: 25 additions & 8 deletions test/unit/authInfoTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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');
});
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -2009,7 +2026,7 @@ describe('AuthInfo', () => {
});
});
describe('Handle User Get Errors', () => {
let authCodeConfig;
let authCodeConfig: any;
beforeEach(async () => {
authCodeConfig = {
authCode: testMetadata.authCode,
Expand Down
65 changes: 65 additions & 0 deletions test/unit/status/myDomainResolverTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});
});

0 comments on commit 3e27623

Please sign in to comment.