Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core URL class SfdcUrl #420

Merged
merged 41 commits into from
Jul 29, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
779c151
feat: new class SfdcUrl (skeleton)
maggiben Jun 16, 2021
f036457
chore: test emitWarning on insecure url
maggiben Jun 16, 2021
3efcc27
chore: add lookup optionally createdOrgInstance and more tests
maggiben Jun 17, 2021
8133efe
chore: allow URL input in constructor
maggiben Jun 17, 2021
df12ea5
Merge pull request #421 from forcedotcom/bm/W-9457855-implements
maggiben Jun 17, 2021
5398544
chore: add domain cache to warnings
maggiben Jun 17, 2021
745ab20
chore: cache as a module
maggiben Jun 17, 2021
da9731e
chore: add method description
maggiben Jun 17, 2021
193918d
Merge pull request #422 from forcedotcom/bm/W-9457855-implements
maggiben Jun 17, 2021
1b7feb7
chore: refactor with SfdcUrl class
maggiben Jun 17, 2021
01d02a8
Merge pull request #424 from forcedotcom/bm/W-9457855-implements
maggiben Jun 17, 2021
d62b39a
chore: add test for warning signal cache
maggiben Jun 17, 2021
1e2046d
chore: remove unused from sfdc
maggiben Jun 17, 2021
25d9b50
chore: defaults to Prod + use Env from kit in tests
maggiben Jun 21, 2021
d351e23
Merge branch 'main' into bm/W-9457855
mshanemc Jun 25, 2021
548cc29
chore: test cleanup + better comments + typos
maggiben Jun 26, 2021
5f5eea8
chore: fix comment
maggiben Jun 26, 2021
3f4b77c
chore: use type safe functions + add tests for lookup error
maggiben Jun 26, 2021
610dc67
chore: better document methods
maggiben Jun 26, 2021
5390c3e
chore: remove unused stub
maggiben Jun 26, 2021
ce00f82
chore: simplify stub with a throw
maggiben Jun 26, 2021
3ee68e8
chore: use process.emitWarning
maggiben Jun 26, 2021
00ee30e
chore: remove unused + make cache private
maggiben Jun 28, 2021
d7e9f25
Merge branch 'main' into bm/W-9457855
mshanemc Jun 30, 2021
5cad144
Merge branch 'main' into bm/W-9457855
mshanemc Jul 14, 2021
a040d43
chore: cnames don't have protocol add one for a proper url
maggiben Jul 19, 2021
8f7b5b9
Merge branch 'main' into bm/W-9457855
mshanemc Jul 22, 2021
f386e59
fix: keep backward compatibility for getJwtAudienceUrl exported fx
maggiben Jul 27, 2021
596d076
chore: unset env var afterEach
maggiben Jul 27, 2021
53be2a8
Merge branch 'bm/W-9457855' of github.com:forcedotcom/sfdx-core into …
maggiben Jul 27, 2021
e053e0f
chore: add isInternalUrl to sfdc
maggiben Jul 27, 2021
e7c7cfc
test: nuts for core
mshanemc Jul 28, 2021
fd76a91
test: run nuts
mshanemc Jul 28, 2021
a260fb6
test: add nuts
mshanemc Jul 28, 2021
f88ddd2
test: rename required nut
mshanemc Jul 28, 2021
f1d76c5
test: don't require nuts yet
mshanemc Jul 28, 2021
695838e
test: skip config (known messages issue)
mshanemc Jul 28, 2021
687ce59
test: auth env checks
mshanemc Jul 28, 2021
4ba3383
test: nuts will be on their own branch
mshanemc Jul 29, 2021
0f00aad
Merge branch 'bm/W-9457855' of https://github.com/forcedotcom/sfdx-co…
mshanemc Jul 29, 2021
67e2c88
test: no nuts
mshanemc Jul 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions src/util/sfdcUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { URL } from 'url';
import { Env, Duration } from '@salesforce/kit';
import { MyDomainResolver } from '../status/myDomainResolver';

export class SfdcUrl extends URL {
/**
* Salesforce URLs.
*/
public static SANDBOX = 'https://test.salesforce.com';
public static PRODUCTION = 'https://login.salesforce.com';
maggiben marked this conversation as resolved.
Show resolved Hide resolved

public constructor(input: string, base?: string | URL) {
super(input, base);
if (this.protocol !== 'https:') {
maggiben marked this conversation as resolved.
Show resolved Hide resolved
this.emitWarning('Using insecure protocol: ' + this.protocol + ' on url: ' + this.origin);
}
}

/**
* Returns the appropiate jwt audience url for this url.
*/
public async getJwtAudienceUrl(): Promise<string> {
// environment variable is used as an override
if (process.env.SFDX_AUDIENCE_URL) {
return process.env.SFDX_AUDIENCE_URL;
maggiben marked this conversation as resolved.
Show resolved Hide resolved
}

if (this.isInternalUrl()) {
// This is for internal developers when just doing authorize;
return this.origin;
}

if (await this.resolvesToSandbox()) {
return SfdcUrl.SANDBOX;
}

if (/^gs1/gi.test(this.origin) || /(gs1.my.salesforce.com)/gi.test(this.origin ?? '')) {
return 'https://gs1.salesforce.com';
}

return SfdcUrl.PRODUCTION;
}

/**
* Returns `true` if this url is a sandbox url
*/
public isSandboxUrl(): boolean {
return (
/^cs|s$/gi.test(this.origin) ||
/sandbox\.my\.salesforce\.com/gi.test(this.origin) || // enhanced domains >= 230
/(cs[0-9]+(\.my|)\.salesforce\.com)/gi.test(this.origin) || // my domains on CS instance OR CS instance without my domain
/([a-z]{3}[0-9]+s\.sfdc-.+\.salesforce\.com)/gi.test(this.origin) || // falcon sandbox ex: usa2s.sfdc-whatever.salesforce.com
/([a-z]{3}[0-9]+s\.sfdc-.+\.force\.com)/gi.test(this.origin) || // falcon sandbox ex: usa2s.sfdc-whatever.salesforce.com
this.hostname === 'test.salesforce.com'
);
}

/**
* Returns `true` if this url contains a Salesforce owned domain.
*/
public isSalesforceDomain(): boolean {
// Source https://help.salesforce.com/articleView?id=000003652&type=1
const allowlistOfSalesforceDomainPatterns: string[] = [
'.cloudforce.com',
'.content.force.com',
'.force.com',
'.salesforce.com',
'.salesforceliveagent.com',
'.secure.force.com',
];

const allowlistOfSalesforceHosts: string[] = ['developer.salesforce.com', 'trailhead.salesforce.com'];

return allowlistOfSalesforceDomainPatterns.some((pattern) => {
return this.hostname.endsWith(pattern) || allowlistOfSalesforceHosts.includes(this.hostname);
});
}

/**
* Tests whether this url is an internal Salesforce domain
*/
public isInternalUrl(): boolean {
const INTERNAL_URL_PARTS = [
'.vpod.',
'stm.salesforce.com',
'stm.force.com',
'.blitz.salesforce.com',
'.stm.salesforce.ms',
'.pc-rnd.force.com',
'.pc-rnd.salesforce.com',
];
return (
this.origin.startsWith('https://gs1.') ||
this.isLocalUrl() ||
INTERNAL_URL_PARTS.some((part) => this.origin.includes(part))
);
}

/**
* Tests whether this url runs on a local machine
*
* @param url
maggiben marked this conversation as resolved.
Show resolved Hide resolved
*/
public isLocalUrl(): boolean {
const LOCAL_PARTS = ['localhost.sfdcdev.', '.internal.'];
return LOCAL_PARTS.some((part) => this.origin.includes(part));
}

/**
* Tests whether this url has the lightning domain extension
*
* @param url
maggiben marked this conversation as resolved.
Show resolved Hide resolved
*/
public async checkLightningDomain(): Promise<boolean> {
const domain = `https://${/https?:\/\/([^.]*)/.exec(this.origin)?.slice(1, 2).pop()}.lightning.force.com`;
const quantity = new Env().getNumber('SFDX_DOMAIN_RETRY', 240) ?? 0;
const timeout = new Duration(quantity, Duration.Unit.SECONDS);

if (this.isInternalUrl() || timeout.seconds === 0) {
return true;
}

const resolver = await MyDomainResolver.create({
url: new URL(domain),
timeout,
frequency: new Duration(1, Duration.Unit.SECONDS),
});

await resolver.resolve();
return true;
}

/**
* Returns `true` if this url domain is or resolves to a sandbox url.
*/
private async resolvesToSandbox(): Promise<boolean> {
if (this.isSandboxUrl()) {
return true;
}
const myDomainResolver = await MyDomainResolver.create({ url: this });
const cnames: string[] = (await myDomainResolver.getCnames()) ?? [];
return cnames.some((cname) => new SfdcUrl(cname).isSandboxUrl());
}

private emitWarning(warning: string): void {
process.emitWarning(warning);
}
maggiben marked this conversation as resolved.
Show resolved Hide resolved
}
172 changes: 172 additions & 0 deletions test/unit/util/sfdcUrlTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { assert, expect } from 'chai';
import { SinonSpy } from 'sinon';
import { spyMethod } from '@salesforce/ts-sinon';
import { shouldThrow, testSetup } from '../../../src/testSetup';
import { SfdcUrl } from '../../../src/util/sfdcUrl';
import { MyDomainResolver } from '../../../src/status/myDomainResolver';

const $$ = testSetup();
const TEST_IP = '1.1.1.1';
const TEST_CNAMES = ['https://login.salesforce.com', 'https://test.salesforce.com'];

describe('util/sfdcUrl', () => {
describe('isSalesforceDomain', () => {
it('is allowlist domain', () => {
const url = new SfdcUrl('https://www.salesforce.com');
expect(url.isSalesforceDomain()).to.be.true;
});

it('is not allowlist or host', () => {
const url = new SfdcUrl('https://www.ghostbusters.com');
expect(url.isSalesforceDomain()).to.be.false;
});

it('is allowlist host', () => {
const url = new SfdcUrl('https://developer.salesforce.com');
expect(url.isSalesforceDomain()).to.be.true;
});

it('falsy', () => {
maggiben marked this conversation as resolved.
Show resolved Hide resolved
try {
const url = new SfdcUrl(undefined);
assert(url, 'should throw');
expect(url.isSalesforceDomain()).to.be.false;
} catch (e) {
expect(e.name).to.equal('TypeError');
}
});
});

describe('internal domains', () => {
it('stm is internal but not local', () => {
const url = new SfdcUrl('https://inttestdevhub02-dev-ed.lightning.stmfa.stm.force.com/');
expect(url.isInternalUrl()).to.equal(true);
expect(url.isLocalUrl()).to.equal(false);
});
it('pc.rnd is internal but not local', () => {
const url = new SfdcUrl('https://alm-cidevhubora3test1core4.test1.lightning.pc-rnd.force.com/');
expect(url.isInternalUrl()).to.equal(true);
expect(url.isLocalUrl()).to.equal(false);
expect(url.isInternalUrl()).to.equal(true);
expect(url.isLocalUrl()).to.equal(false);
maggiben marked this conversation as resolved.
Show resolved Hide resolved
});
it('localhost domain is both internal and local, and tolerates local host ports', () => {
const url = new SfdcUrl('https://scorpio-ryan-2873-dev-ed.my.localhost.sfdcdev.salesforce.com:6109');
expect(url.isInternalUrl()).to.equal(true);
expect(url.isLocalUrl()).to.equal(true);
});
});

describe('checkLightningDomain', () => {
beforeEach(() => {
$$.SANDBOX.stub(MyDomainResolver.prototype, 'resolve').resolves(TEST_IP);
});

afterEach(() => {
$$.SANDBOX.restore();
maggiben marked this conversation as resolved.
Show resolved Hide resolved
});

it('return true for internal urls', async () => {
const url = new SfdcUrl('https://my-domain.stm.salesforce.com');
const response = await url.checkLightningDomain();
expect(response).to.be.true;
});

it('return true for urls that dns can resolve', async () => {
const url = new SfdcUrl('https://login.salesforce.com');
const respose = await url.checkLightningDomain();
maggiben marked this conversation as resolved.
Show resolved Hide resolved
expect(respose).to.be.true;
});

it('throws on domain resolution failure', async () => {
$$.SANDBOX.restore();
maggiben marked this conversation as resolved.
Show resolved Hide resolved
$$.SANDBOX.stub(MyDomainResolver.prototype, 'resolve').rejects();
const url = new SfdcUrl('https://login.salesforce.com');
try {
await shouldThrow(url.checkLightningDomain());
} catch (e) {
expect(e.name).to.equal('Error');
}
});
});

describe('Insecure HTTP warning', () => {
let emitWarningSpy: SinonSpy;
beforeEach(() => {
$$.SANDBOX.stub(MyDomainResolver.prototype, 'getCnames').resolves(TEST_CNAMES);
emitWarningSpy = spyMethod($$.SANDBOX, SfdcUrl.prototype, 'emitWarning');
});

afterEach(() => {
$$.SANDBOX.restore();
emitWarningSpy.restore();
maggiben marked this conversation as resolved.
Show resolved Hide resolved
});

it('listens for the insecure http signal', () => {
const site = 'http://insecure.website.com';
const url = new SfdcUrl(site);
const { protocol } = url;
expect(protocol).to.equal('http:');
expect(emitWarningSpy.callCount).to.equal(1);
expect(emitWarningSpy.args).to.deep.equal([[`Using insecure protocol: ${protocol} on url: ${site}`]]);
});
});

describe('getJwtAudienceUrl', () => {
beforeEach(() => {
$$.SANDBOX.stub(MyDomainResolver.prototype, 'getCnames').resolves(TEST_CNAMES);
});

afterEach(() => {
$$.SANDBOX.restore();
maggiben marked this conversation as resolved.
Show resolved Hide resolved
});

it('return the jwt audicence url for sandbox domains', async () => {
const url = new SfdcUrl('https://organization.my.salesforce.com');
const response = await url.getJwtAudienceUrl();
expect(response).to.be.equal('https://test.salesforce.com');
});

it('return the jwt audicence url for internal domains (same)', async () => {
const url = new SfdcUrl('https://organization.stm.salesforce.com');
const response = await url.getJwtAudienceUrl();
expect(response).to.be.equal('https://organization.stm.salesforce.com');
});

it('return the jwt audicence url for sandbox domains', async () => {
const url = new SfdcUrl('https://organization.sandbox.my.salesforce.com');
const response = await url.getJwtAudienceUrl();
expect(response).to.be.equal('https://test.salesforce.com');
});
});

describe('isSalesforceDomain', () => {
it('returns true if url is salesforce domain', () => {
const url = new SfdcUrl('https://organization.salesforce.com');
const isSalesforceDomain = url.isSalesforceDomain();
expect(isSalesforceDomain).to.be.true;
});

it('returns false if url is not a salesforce domain', () => {
const url = new SfdcUrl('https://www.ghostbusters.com');
const isSalesforceDomain = url.isSalesforceDomain();
expect(isSalesforceDomain).to.be.false;
});
});

describe('Salesforce standard urls', () => {
it('sandbox url', () => {
expect(SfdcUrl.SANDBOX).to.equal('https://test.salesforce.com');
});

it('production url', () => {
expect(SfdcUrl.PRODUCTION).to.equal('https://login.salesforce.com');
});
});
});