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:
parent
bfdb59eada
commit
2db97e3b2d
|
|
@ -46,6 +46,24 @@ export class FFBrowser extends BrowserBase {
|
||||||
browser._defaultContext = new FFBrowserContext(browser, null, options.persistent);
|
browser._defaultContext = new FFBrowserContext(browser, null, options.persistent);
|
||||||
promises.push((browser._defaultContext as FFBrowserContext)._initialize());
|
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);
|
await Promise.all(promises);
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +177,6 @@ export class FFBrowserContext extends BrowserContextBase {
|
||||||
super(browser, options, !browserContextId);
|
super(browser, options, !browserContextId);
|
||||||
this._browser = browser;
|
this._browser = browser;
|
||||||
this._browserContextId = browserContextId;
|
this._browserContextId = browserContextId;
|
||||||
this._authenticateProxyViaHeader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _initialize() {
|
async _initialize() {
|
||||||
|
|
|
||||||
|
|
@ -100,14 +100,25 @@ export module Protocol {
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
export type setExtraHTTPHeadersReturnValue = void;
|
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;
|
browserContextId?: string;
|
||||||
type: ("http"|"https"|"socks"|"socks4");
|
type: ("http"|"https"|"socks"|"socks4");
|
||||||
bypass: string[];
|
bypass: string[];
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
};
|
};
|
||||||
export type setProxyReturnValue = void;
|
export type setContextProxyReturnValue = void;
|
||||||
export type setHTTPCredentialsParameters = {
|
export type setHTTPCredentialsParameters = {
|
||||||
browserContextId?: string;
|
browserContextId?: string;
|
||||||
credentials: {
|
credentials: {
|
||||||
|
|
@ -940,7 +951,8 @@ export module Protocol {
|
||||||
"Browser.close": Browser.closeParameters;
|
"Browser.close": Browser.closeParameters;
|
||||||
"Browser.getInfo": Browser.getInfoParameters;
|
"Browser.getInfo": Browser.getInfoParameters;
|
||||||
"Browser.setExtraHTTPHeaders": Browser.setExtraHTTPHeadersParameters;
|
"Browser.setExtraHTTPHeaders": Browser.setExtraHTTPHeadersParameters;
|
||||||
"Browser.setProxy": Browser.setProxyParameters;
|
"Browser.setBrowserProxy": Browser.setBrowserProxyParameters;
|
||||||
|
"Browser.setContextProxy": Browser.setContextProxyParameters;
|
||||||
"Browser.setHTTPCredentials": Browser.setHTTPCredentialsParameters;
|
"Browser.setHTTPCredentials": Browser.setHTTPCredentialsParameters;
|
||||||
"Browser.setRequestInterception": Browser.setRequestInterceptionParameters;
|
"Browser.setRequestInterception": Browser.setRequestInterceptionParameters;
|
||||||
"Browser.setGeolocationOverride": Browser.setGeolocationOverrideParameters;
|
"Browser.setGeolocationOverride": Browser.setGeolocationOverrideParameters;
|
||||||
|
|
@ -1011,7 +1023,8 @@ export module Protocol {
|
||||||
"Browser.close": Browser.closeReturnValue;
|
"Browser.close": Browser.closeReturnValue;
|
||||||
"Browser.getInfo": Browser.getInfoReturnValue;
|
"Browser.getInfo": Browser.getInfoReturnValue;
|
||||||
"Browser.setExtraHTTPHeaders": Browser.setExtraHTTPHeadersReturnValue;
|
"Browser.setExtraHTTPHeaders": Browser.setExtraHTTPHeadersReturnValue;
|
||||||
"Browser.setProxy": Browser.setProxyReturnValue;
|
"Browser.setBrowserProxy": Browser.setBrowserProxyReturnValue;
|
||||||
|
"Browser.setContextProxy": Browser.setContextProxyReturnValue;
|
||||||
"Browser.setHTTPCredentials": Browser.setHTTPCredentialsReturnValue;
|
"Browser.setHTTPCredentials": Browser.setHTTPCredentialsReturnValue;
|
||||||
"Browser.setRequestInterception": Browser.setRequestInterceptionReturnValue;
|
"Browser.setRequestInterception": Browser.setRequestInterceptionReturnValue;
|
||||||
"Browser.setGeolocationOverride": Browser.setGeolocationOverrideReturnValue;
|
"Browser.setGeolocationOverride": Browser.setGeolocationOverrideReturnValue;
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ export class Firefox extends BrowserTypeBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
_defaultArgs(options: LaunchNonPersistentOptions, isPersistent: boolean, userDataDir: string): string[] {
|
_defaultArgs(options: LaunchNonPersistentOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||||
const { args = [], proxy, devtools, headless } = options;
|
const { args = [], devtools, headless } = options;
|
||||||
if (devtools)
|
if (devtools)
|
||||||
console.warn('devtools parameter is not supported as a launch argument in Firefox. You can launch the devtools window manually.');
|
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'));
|
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');
|
throw new Error('Pass userDataDir parameter instead of specifying -profile argument');
|
||||||
if (args.find(arg => arg.startsWith('-juggler')))
|
if (args.find(arg => arg.startsWith('-juggler')))
|
||||||
throw new Error('Use the port parameter instead of -juggler argument');
|
throw new Error('Use the port parameter instead of -juggler argument');
|
||||||
let firefoxUserPrefs = isPersistent ? undefined : options.firefoxUserPrefs;
|
const 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;
|
|
||||||
}
|
|
||||||
if (firefoxUserPrefs) {
|
if (firefoxUserPrefs) {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
for (const [name, value] of Object.entries(firefoxUserPrefs))
|
for (const [name, value] of Object.entries(firefoxUserPrefs))
|
||||||
|
|
|
||||||
|
|
@ -60,30 +60,39 @@ it.fail(CHROMIUM && !HEADLESS)('should exclude patterns', async ({browserType, d
|
||||||
server.setRoute('/target.html', async (req, res) => {
|
server.setRoute('/target.html', async (req, res) => {
|
||||||
res.end('<html><title>Served by the proxy</title></html>');
|
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({
|
const browser = await browserType.launch({
|
||||||
...defaultBrowserOptions,
|
...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();
|
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');
|
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();
|
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();
|
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();
|
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) {
|
if (CHROMIUM) {
|
||||||
// Should successfully navigate to the error page.
|
// Should successfully navigate to the error page.
|
||||||
await page.waitForEvent('framenavigated', frame => frame.url() === 'chrome-error://chromewebdata/');
|
await page.waitForEvent('framenavigated', frame => frame.url() === 'chrome-error://chromewebdata/');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue