-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #106 from amtrack/fix/frontdoor-workaround
fix: workaround for timeouts and redirect issues on frontdoor
- Loading branch information
Showing
21 changed files
with
364 additions
and
237 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,71 +1,181 @@ | ||
import { core } from '@salesforce/command'; | ||
import * as puppeteer from 'puppeteer'; | ||
import { URL } from 'url'; | ||
import { retry } from './plugins/utils'; | ||
|
||
class FrontdoorRedirectError extends Error {} | ||
class LoginError extends Error {} | ||
class RetryableError extends Error {} | ||
|
||
const PERSONAL_INFORMATION_PATH = | ||
'setup/personalInformationSetup.apexp?nooverride=1'; | ||
|
||
const ERROR_DIV_SELECTOR = '#errorTitle'; | ||
const ERROR_DIVS_SELECTOR = 'div.errorMsg'; | ||
|
||
export default class Browserforce { | ||
public org: core.Org; | ||
public logger: core.Logger; | ||
public browser: puppeteer.Browser; | ||
public page: puppeteer.Page; | ||
constructor(org) { | ||
constructor(org, logger?) { | ||
this.org = org; | ||
this.logger = logger; | ||
} | ||
|
||
public async login() { | ||
this.browser = await puppeteer.launch({ | ||
args: ['--no-sandbox', '--disable-setuid-sandbox'], | ||
headless: !(process.env.BROWSER_DEBUG === 'true') | ||
}); | ||
this.page = await this.browser.newPage(); | ||
this.page.setDefaultNavigationTimeout( | ||
parseInt(process.env.BROWSERFORCE_NAVIGATION_TIMEOUT_MS, 10) || 90000 | ||
); | ||
await this.page.setViewport({ width: 1024, height: 768 }); | ||
const instanceUrl = this.getInstanceUrl(); | ||
const response = await this.page.goto( | ||
`${instanceUrl}/secur/frontdoor.jsp?sid=${ | ||
await this.retryLogin( | ||
`secur/frontdoor.jsp?sid=${ | ||
this.org.getConnection().accessToken | ||
}&retURL=${encodeURIComponent(PERSONAL_INFORMATION_PATH)}`, | ||
{ waitUntil: ['load', 'domcontentloaded', 'networkidle0'] } | ||
}&retURL=${encodeURIComponent(PERSONAL_INFORMATION_PATH)}` | ||
); | ||
if (response) { | ||
if (response.status() === 500) { | ||
const errorHandle = await this.page.$(ERROR_DIV_SELECTOR); | ||
if (errorHandle) { | ||
const errorMsg = this.page.evaluate( | ||
div => div.innerText, | ||
errorHandle | ||
); | ||
throw new Error(`login failed [500]: ${errorMsg}`); | ||
} else { | ||
throw new Error(`login failed [500]: ${response.statusText()}`); | ||
} | ||
} | ||
if (response.url().indexOf('/?ec=302') > 0) { | ||
throw new Error( | ||
`login failed [302]: {"instanceUrl": "${instanceUrl}, "url": "${response | ||
.url() | ||
.split('/') | ||
.slice(0, 3) | ||
.join('/')}"}"` | ||
); | ||
} | ||
} else { | ||
throw new Error('login failed'); | ||
} | ||
return this; | ||
} | ||
|
||
public async logout() { | ||
await this.page.close(); | ||
await this.browser.close(); | ||
return this; | ||
} | ||
|
||
public async resolveDomains() { | ||
// resolve ip addresses of both LEX and classic domains | ||
for (const url of [this.getInstanceUrl(), this.getLightningUrl()]) { | ||
const resolver = await core.MyDomainResolver.create({ | ||
url: new URL(url) | ||
}); | ||
await resolver.resolve(); | ||
} | ||
} | ||
|
||
public async throwResponseErrors(response) { | ||
if (!response) { | ||
throw new Error('no response'); | ||
} | ||
if (!response.ok()) { | ||
throw new RetryableError(`${response.status()}: ${response.statusText()}`); | ||
} | ||
if (response.url().indexOf('/?ec=302') > 0) { | ||
if ( | ||
!response.url().startsWith(this.getInstanceUrl()) && | ||
!response.url().startsWith(this.getLightningUrl()) | ||
) { | ||
const redactedUrl = response | ||
.url() | ||
.replace(/sid=(.*)/, 'sid=<REDACTED>') | ||
.replace(/sid%3D(.*)/, 'sid=<REDACTED>'); | ||
throw new FrontdoorRedirectError( | ||
`expected instance or lightning URL but got: ${redactedUrl}` | ||
); | ||
} | ||
throw new LoginError('login failed'); | ||
} | ||
} | ||
|
||
public async throwPageErrors(page) { | ||
const errorHandle = await page.$(ERROR_DIV_SELECTOR); | ||
if (errorHandle) { | ||
const errorMsg = await page.evaluate( | ||
(div: HTMLDivElement) => div.innerText, | ||
errorHandle | ||
); | ||
await errorHandle.dispose(); | ||
if (errorMsg && errorMsg.trim()) { | ||
throw new Error(errorMsg.trim()); | ||
} | ||
} | ||
const errorElements = await page.$$(ERROR_DIVS_SELECTOR); | ||
if (errorElements.length) { | ||
const errorMessages = await page.evaluate((...errorDivs) => { | ||
return errorDivs.map((div: HTMLDivElement) => div.innerText); | ||
}, ...errorElements); | ||
const errorMsg = errorMessages | ||
.map(m => m.trim()) | ||
.join(' ') | ||
.trim(); | ||
if (errorMsg) { | ||
throw new Error(errorMsg); | ||
} | ||
} | ||
} | ||
|
||
// path instead of url | ||
public async openPage(urlPath, options?) { | ||
return await retry( | ||
async () => { | ||
try { | ||
await this.resolveDomains(); | ||
} catch (error) { | ||
throw new RetryableError(error); | ||
} | ||
const page = await this.browser.newPage(); | ||
page.setDefaultNavigationTimeout( | ||
parseInt(process.env.BROWSERFORCE_NAVIGATION_TIMEOUT_MS, 10) || 90000 | ||
); | ||
await page.setViewport({ width: 1024, height: 768 }); | ||
const url = `${this.getInstanceUrl()}/${urlPath}`; | ||
const response = await page.goto(url, options); | ||
await this.throwResponseErrors(response); | ||
// await this.throwPageErrors(page); | ||
return page; | ||
}, | ||
5, | ||
4000, | ||
true, | ||
RetryableError.prototype, | ||
this.logger | ||
); | ||
} | ||
|
||
public getInstanceUrl() { | ||
return this.org.getConnection().instanceUrl; | ||
} | ||
|
||
private getLightningUrl() { | ||
const myDomain = this.getInstanceUrl().match(/https?\:\/\/([^.]*)/)[1]; | ||
return `https://${myDomain}.lightning.force.com`; | ||
} | ||
|
||
private async retryLogin( | ||
loginUrl, | ||
refreshAuthLeft = 1, | ||
frontdoorWorkaroundLeft = 1 | ||
) { | ||
try { | ||
await this.openPage(loginUrl, { | ||
waitUntil: ['load', 'domcontentloaded', 'networkidle0'] | ||
}); | ||
} catch (err) { | ||
if (err instanceof LoginError && refreshAuthLeft > 0) { | ||
if (this.logger) { | ||
this.logger.warn(`retrying with refreshed auth because of "${err}"`); | ||
} | ||
await this.org.refreshAuth(); | ||
await this.retryLogin( | ||
loginUrl, | ||
refreshAuthLeft - 1, | ||
frontdoorWorkaroundLeft | ||
); | ||
} else if ( | ||
err instanceof FrontdoorRedirectError && | ||
frontdoorWorkaroundLeft > 0 | ||
) { | ||
if (this.logger) { | ||
this.logger.warn( | ||
`retrying open page with instance url because of "${err}"` | ||
); | ||
} | ||
await this.retryLogin( | ||
PERSONAL_INFORMATION_PATH, | ||
refreshAuthLeft, | ||
frontdoorWorkaroundLeft - 1 | ||
); | ||
} else { | ||
throw err; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.