From 2db97e3b2d1f46b93f100213f2779e6c3f8a9ce7 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Tue, 11 Aug 2020 11:36:27 -0700 Subject: [PATCH] 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 --- src/firefox/ffBrowser.ts | 19 ++++++++++++++++++- src/firefox/protocol.ts | 21 +++++++++++++++++---- src/server/firefox.ts | 22 ++-------------------- test/proxy.spec.js | 19 ++++++++++++++----- 4 files changed, 51 insertions(+), 30 deletions(-) diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index bbda5f963e..f4a07ed0d6 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -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() { diff --git a/src/firefox/protocol.ts b/src/firefox/protocol.ts index 6b8a88a354..b521158ab2 100644 --- a/src/firefox/protocol.ts +++ b/src/firefox/protocol.ts @@ -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; diff --git a/src/server/firefox.ts b/src/server/firefox.ts index 8faf95bc1f..9c887c9b52 100644 --- a/src/server/firefox.ts +++ b/src/server/firefox.ts @@ -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)) diff --git a/test/proxy.spec.js b/test/proxy.spec.js index 96b2529168..211cd02aa4 100644 --- a/test/proxy.spec.js +++ b/test/proxy.spec.js @@ -60,30 +60,39 @@ it.fail(CHROMIUM && !HEADLESS)('should exclude patterns', async ({browserType, d server.setRoute('/target.html', async (req, res) => { res.end('Served by the proxy'); }); + // 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/');