feat(firefox): migrate to protocol-based proxy implementation (#3362)

This migrates Firefox to the protocol-based proxy implementation.

Benefits:
- supports secure web proxies (already supported by Chromium)
- unlocks support for SOCKS proxies with authentication
This commit is contained in:
Andrey Lushnikov 2020-08-11 11:36:27 -07:00 committed by GitHub
parent bfdb59eada
commit 2db97e3b2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 51 additions and 30 deletions

View File

@ -46,6 +46,24 @@ export class FFBrowser extends BrowserBase {
browser._defaultContext = new FFBrowserContext(browser, null, options.persistent);
promises.push((browser._defaultContext as FFBrowserContext)._initialize());
}
if (options.proxy) {
const proxyServer = new URL(options.proxy.server);
let aType: 'http'|'https'|'socks'|'socks4' = 'http';
if (proxyServer.protocol === 'socks5:')
aType = 'socks';
else if (proxyServer.protocol === 'socks4:')
aType = 'socks4';
else if (proxyServer.protocol === 'https:')
aType = 'https';
promises.push(browser._connection.send('Browser.setBrowserProxy', {
type: aType,
bypass: options.proxy.bypass ? options.proxy.bypass.split(',').map(domain => domain.trim()) : [],
host: proxyServer.hostname,
port: parseInt(proxyServer.port, 10),
username: options.proxy.username,
password: options.proxy.password,
}));
}
await Promise.all(promises);
return browser;
}
@ -159,7 +177,6 @@ export class FFBrowserContext extends BrowserContextBase {
super(browser, options, !browserContextId);
this._browser = browser;
this._browserContextId = browserContextId;
this._authenticateProxyViaHeader();
}
async _initialize() {

View File

@ -100,14 +100,25 @@ export module Protocol {
}[];
};
export type setExtraHTTPHeadersReturnValue = void;
export type setProxyParameters = {
export type setBrowserProxyParameters = {
type: ("http"|"https"|"socks"|"socks4");
bypass: string[];
host: string;
port: number;
username?: string;
password?: string;
};
export type setBrowserProxyReturnValue = void;
export type setContextProxyParameters = {
browserContextId?: string;
type: ("http"|"https"|"socks"|"socks4");
bypass: string[];
host: string;
port: number;
username?: string;
password?: string;
};
export type setProxyReturnValue = void;
export type setContextProxyReturnValue = void;
export type setHTTPCredentialsParameters = {
browserContextId?: string;
credentials: {
@ -940,7 +951,8 @@ export module Protocol {
"Browser.close": Browser.closeParameters;
"Browser.getInfo": Browser.getInfoParameters;
"Browser.setExtraHTTPHeaders": Browser.setExtraHTTPHeadersParameters;
"Browser.setProxy": Browser.setProxyParameters;
"Browser.setBrowserProxy": Browser.setBrowserProxyParameters;
"Browser.setContextProxy": Browser.setContextProxyParameters;
"Browser.setHTTPCredentials": Browser.setHTTPCredentialsParameters;
"Browser.setRequestInterception": Browser.setRequestInterceptionParameters;
"Browser.setGeolocationOverride": Browser.setGeolocationOverrideParameters;
@ -1011,7 +1023,8 @@ export module Protocol {
"Browser.close": Browser.closeReturnValue;
"Browser.getInfo": Browser.getInfoReturnValue;
"Browser.setExtraHTTPHeaders": Browser.setExtraHTTPHeadersReturnValue;
"Browser.setProxy": Browser.setProxyReturnValue;
"Browser.setBrowserProxy": Browser.setBrowserProxyReturnValue;
"Browser.setContextProxy": Browser.setContextProxyReturnValue;
"Browser.setHTTPCredentials": Browser.setHTTPCredentialsReturnValue;
"Browser.setRequestInterception": Browser.setRequestInterceptionReturnValue;
"Browser.setGeolocationOverride": Browser.setGeolocationOverrideReturnValue;

View File

@ -65,7 +65,7 @@ export class Firefox extends BrowserTypeBase {
}
_defaultArgs(options: LaunchNonPersistentOptions, isPersistent: boolean, userDataDir: string): string[] {
const { args = [], proxy, devtools, headless } = options;
const { args = [], devtools, headless } = options;
if (devtools)
console.warn('devtools parameter is not supported as a launch argument in Firefox. You can launch the devtools window manually.');
const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile'));
@ -73,25 +73,7 @@ export class Firefox extends BrowserTypeBase {
throw new Error('Pass userDataDir parameter instead of specifying -profile argument');
if (args.find(arg => arg.startsWith('-juggler')))
throw new Error('Use the port parameter instead of -juggler argument');
let firefoxUserPrefs = isPersistent ? undefined : options.firefoxUserPrefs;
if (proxy) {
// TODO: we should support proxy in persistent context without overriding user prefs.
firefoxUserPrefs = firefoxUserPrefs || {};
firefoxUserPrefs['network.proxy.type'] = 1;
const proxyServer = new URL(proxy.server);
const isSocks = proxyServer.protocol === 'socks5:';
if (isSocks) {
firefoxUserPrefs['network.proxy.socks'] = proxyServer.hostname;
firefoxUserPrefs['network.proxy.socks_port'] = parseInt(proxyServer.port, 10);
} else {
firefoxUserPrefs['network.proxy.http'] = proxyServer.hostname;
firefoxUserPrefs['network.proxy.http_port'] = parseInt(proxyServer.port, 10);
firefoxUserPrefs['network.proxy.ssl'] = proxyServer.hostname;
firefoxUserPrefs['network.proxy.ssl_port'] = parseInt(proxyServer.port, 10);
}
if (proxy.bypass)
firefoxUserPrefs['network.proxy.no_proxies_on'] = proxy.bypass;
}
const firefoxUserPrefs = isPersistent ? undefined : options.firefoxUserPrefs;
if (firefoxUserPrefs) {
const lines: string[] = [];
for (const [name, value] of Object.entries(firefoxUserPrefs))

View File

@ -60,30 +60,39 @@ it.fail(CHROMIUM && !HEADLESS)('should exclude patterns', async ({browserType, d
server.setRoute('/target.html', async (req, res) => {
res.end('<html><title>Served by the proxy</title></html>');
});
// FYI: using long and weird domain names to avoid ATT DNS hijacking
// that resolves everything to some weird search results page.
//
// @see https://gist.github.com/CollinChaffin/24f6c9652efb3d6d5ef2f5502720ef00
const browser = await browserType.launch({
...defaultBrowserOptions,
proxy: { server: `localhost:${server.PORT}`, bypass: 'non-existent1.com, .non-existent2.com, .zone' }
proxy: { server: `localhost:${server.PORT}`, bypass: '1.non.existent.domain.for.the.test, 2.non.existent.domain.for.the.test, .another.test' }
});
const page = await browser.newPage();
await page.goto('http://non-existent.com/target.html');
await page.goto('http://0.non.existent.domain.for.the.test/target.html');
expect(await page.title()).toBe('Served by the proxy');
{
const error = await page.goto('http://non-existent1.com/target.html').catch(e => e);
const error = await page.goto('http://1.non.existent.domain.for.the.test/target.html').catch(e => e);
expect(error.message).toBeTruthy();
}
{
const error = await page.goto('http://sub.non-existent2.com/target.html').catch(e => e);
const error = await page.goto('http://2.non.existent.domain.for.the.test/target.html').catch(e => e);
expect(error.message).toBeTruthy();
}
{
const error = await page.goto('http://foo.zone/target.html').catch(e => e);
const error = await page.goto('http://foo.is.the.another.test/target.html').catch(e => e);
expect(error.message).toBeTruthy();
}
{
await page.goto('http://3.non.existent.domain.for.the.test/target.html');
expect(await page.title()).toBe('Served by the proxy');
}
if (CHROMIUM) {
// Should successfully navigate to the error page.
await page.waitForEvent('framenavigated', frame => frame.url() === 'chrome-error://chromewebdata/');