From 356d69ae3f0afc25666c1441488c12a7115d23e2 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 19 Aug 2021 13:34:32 -0700 Subject: [PATCH] test: make borwsercontext-proxy tests use test proxy server (#8318) --- tests/browsercontext-proxy.spec.ts | 153 ++++++++++++++--------------- tests/config/proxy.ts | 53 +++++++--- 2 files changed, 115 insertions(+), 91 deletions(-) diff --git a/tests/browsercontext-proxy.spec.ts b/tests/browsercontext-proxy.spec.ts index 3edfbe2c1e..99dc84e7a8 100644 --- a/tests/browsercontext-proxy.spec.ts +++ b/tests/browsercontext-proxy.spec.ts @@ -18,6 +18,12 @@ import { browserTest as it, expect } from './config/browserTest'; it.use({ proxy: { server: 'per-context' } }); +it.beforeEach(({ server }) => { + server.setRoute('/target.html', async (req, res) => { + res.end('Served by the proxy'); + }); +}); + it('should throw for missing global proxy on Chromium Windows', async ({ browserName, platform, browserType, browserOptions, server }) => { it.skip(browserName !== 'chromium' || platform !== 'win32'); @@ -34,14 +40,12 @@ it('should throw for missing global proxy on Chromium Windows', async ({ browser } }); -it('should work when passing the proxy only on the context level', async ({browserName, platform, browserType, browserOptions, contextOptions, server}) => { +it('should work when passing the proxy only on the context level', async ({browserName, platform, browserType, browserOptions, contextOptions, server, proxyServer}) => { // Currently an upstream bug in the network stack of Chromium which leads that // the wrong proxy gets used in the BrowserContext. it.fixme(browserName === 'chromium' && platform === 'win32'); - server.setRoute('/target.html', async (req, res) => { - res.end('Served by the proxy'); - }); + proxyServer.forwardTo(server.PORT); let browser; try { browser = await browserType.launch({ @@ -50,11 +54,12 @@ it('should work when passing the proxy only on the context level', async ({brows }); const context = await browser.newContext({ ...contextOptions, - proxy: { server: `localhost:${server.PORT}` } + proxy: { server: `localhost:${proxyServer.PORT}` } }); const page = await context.newPage(); await page.goto('http://non-existent.com/target.html'); + expect(proxyServer.requestUrls).toContain('http://non-existent.com/target.html'); expect(await page.title()).toBe('Served by the proxy'); } finally { await browser.close(); @@ -69,47 +74,47 @@ it('should throw for bad server value', async ({ contextFactory }) => { expect(error.message).toContain('proxy.server: expected string, got number'); }); -it('should use proxy', async ({ contextFactory, server }) => { - server.setRoute('/target.html', async (req, res) => { - res.end('Served by the proxy'); - }); +it('should use proxy', async ({ contextFactory, server, proxyServer }) => { + proxyServer.forwardTo(server.PORT); const context = await contextFactory({ - proxy: { server: `localhost:${server.PORT}` } + proxy: { server: `localhost:${proxyServer.PORT}` } }); const page = await context.newPage(); await page.goto('http://non-existent.com/target.html'); + expect(proxyServer.requestUrls).toContain('http://non-existent.com/target.html'); expect(await page.title()).toBe('Served by the proxy'); await context.close(); }); -it('should use proxy twice', async ({ contextFactory, server }) => { - server.setRoute('/target.html', async (req, res) => { - res.end('Served by the proxy'); - }); +it('should use proxy twice', async ({ contextFactory, server, proxyServer }) => { + proxyServer.forwardTo(server.PORT); const context = await contextFactory({ - proxy: { server: `localhost:${server.PORT}` } + proxy: { server: `localhost:${proxyServer.PORT}` } }); const page = await context.newPage(); await page.goto('http://non-existent.com/target.html'); + expect(proxyServer.requestUrls).toContain('http://non-existent.com/target.html'); await page.goto('http://non-existent-2.com/target.html'); + expect(proxyServer.requestUrls).toContain('http://non-existent-2.com/target.html'); expect(await page.title()).toBe('Served by the proxy'); await context.close(); }); -it('should use proxy for second page', async ({contextFactory, server}) => { - server.setRoute('/target.html', async (req, res) => { - res.end('Served by the proxy'); - }); +it('should use proxy for second page', async ({contextFactory, server, proxyServer}) => { + proxyServer.forwardTo(server.PORT); const context = await contextFactory({ - proxy: { server: `localhost:${server.PORT}` } + proxy: { server: `localhost:${proxyServer.PORT}` } }); const page = await context.newPage(); await page.goto('http://non-existent.com/target.html'); + expect(proxyServer.requestUrls).toContain('http://non-existent.com/target.html'); expect(await page.title()).toBe('Served by the proxy'); const page2 = await context.newPage(); + proxyServer.requestUrls = []; await page2.goto('http://non-existent.com/target.html'); + expect(proxyServer.requestUrls).toContain('http://non-existent.com/target.html'); expect(await page2.title()).toBe('Served by the proxy'); await context.close(); @@ -117,33 +122,28 @@ it('should use proxy for second page', async ({contextFactory, server}) => { it('should use proxy for https urls', async ({ contextFactory, server, httpsServer, proxyServer }) => { httpsServer.setRoute('/target.html', async (req, res) => { - res.end('Served by the proxy'); - }); - let connectedToProxy = false; - proxyServer.onConnect(req => { - req.url = `localhost:${httpsServer.PORT}`; - connectedToProxy = true; + res.end('Served by https server via proxy'); }); + proxyServer.forwardTo(httpsServer.PORT); const context = await contextFactory({ ignoreHTTPSErrors: true, proxy: { server: `localhost:${proxyServer.PORT}` } }); const page = await context.newPage(); await page.goto('https://non-existent.com/target.html'); - expect(connectedToProxy).toBeTruthy(); - expect(await page.title()).toBe('Served by the proxy'); + expect(proxyServer.connectHosts).toContain('non-existent.com:443'); + expect(await page.title()).toBe('Served by https server via proxy'); await context.close(); }); -it('should work with IP:PORT notion', async ({contextFactory, server}) => { - server.setRoute('/target.html', async (req, res) => { - res.end('Served by the proxy'); - }); +it('should work with IP:PORT notion', async ({contextFactory, server, proxyServer}) => { + proxyServer.forwardTo(server.PORT); const context = await contextFactory({ - proxy: { server: `127.0.0.1:${server.PORT}` } + proxy: { server: `127.0.0.1:${proxyServer.PORT}` } }); const page = await context.newPage(); await page.goto('http://non-existent.com/target.html'); + expect(proxyServer.requestUrls).toContain('http://non-existent.com/target.html'); expect(await page.title()).toBe('Served by the proxy'); await context.close(); }); @@ -162,117 +162,112 @@ it('should throw for socks4 authentication', async ({contextFactory}) => { expect(error.message).toContain('Socks4 proxy protocol does not support authentication'); }); -it('should authenticate', async ({contextFactory, server}) => { - server.setRoute('/target.html', async (req, res) => { - const auth = req.headers['proxy-authorization']; - if (!auth) { - res.writeHead(407, 'Proxy Authentication Required', { - 'Proxy-Authenticate': 'Basic realm="Access to internal site"' - }); - res.end(); - } else { - res.end(`${auth}`); - } +it('should authenticate', async ({contextFactory, server, proxyServer}) => { + proxyServer.forwardTo(server.PORT); + let auth; + proxyServer.setAuthHandler(req => { + auth = req.headers['proxy-authorization']; + return !!auth; }); const context = await contextFactory({ - proxy: { server: `localhost:${server.PORT}`, username: 'user', password: 'secret' } + proxy: { server: `localhost:${proxyServer.PORT}`, username: 'user', password: 'secret' } }); const page = await context.newPage(); await page.goto('http://non-existent.com/target.html'); - expect(await page.title()).toBe('Basic ' + Buffer.from('user:secret').toString('base64')); + expect(proxyServer.requestUrls).toContain('http://non-existent.com/target.html'); + expect(auth).toBe('Basic ' + Buffer.from('user:secret').toString('base64')); + expect(await page.title()).toBe('Served by the proxy'); await context.close(); }); -it('should authenticate with empty password', async ({contextFactory, server}) => { - server.setRoute('/target.html', async (req, res) => { - const auth = req.headers['proxy-authorization']; - if (!auth) { - res.writeHead(407, 'Proxy Authentication Required', { - 'Proxy-Authenticate': 'Basic realm="Access to internal site"' - }); - res.end(); - } else { - res.end(`${auth}`); - } +it('should authenticate with empty password', async ({contextFactory, server, proxyServer}) => { + proxyServer.forwardTo(server.PORT); + let auth; + proxyServer.setAuthHandler(req => { + auth = req.headers['proxy-authorization']; + return !!auth; }); const context = await contextFactory({ - proxy: { server: `localhost:${server.PORT}`, username: 'user', password: '' } + proxy: { server: `localhost:${proxyServer.PORT}`, username: 'user', password: '' } }); const page = await context.newPage(); await page.goto('http://non-existent.com/target.html'); - expect(await page.title()).toBe('Basic ' + Buffer.from('user:').toString('base64')); + expect(auth).toBe('Basic ' + Buffer.from('user:').toString('base64')); + expect(await page.title()).toBe('Served by the proxy'); await context.close(); }); -it('should isolate proxy credentials between contexts', async ({contextFactory, server, browserName}) => { +it('should isolate proxy credentials between contexts', async ({contextFactory, server, browserName, proxyServer}) => { it.fixme(browserName === 'firefox', 'Credentials from the first context stick around'); - server.setRoute('/target.html', async (req, res) => { - const auth = req.headers['proxy-authorization']; - if (!auth) { - res.writeHead(407, 'Proxy Authentication Required', { - 'Proxy-Authenticate': 'Basic realm="Access to internal site"' - }); - res.end(); - } else { - res.end(`${auth}`); - } + proxyServer.forwardTo(server.PORT); + let auth; + proxyServer.setAuthHandler(req => { + auth = req.headers['proxy-authorization']; + return !!auth; }); { const context = await contextFactory({ - proxy: { server: `localhost:${server.PORT}`, username: 'user1', password: 'secret1' } + proxy: { server: `localhost:${proxyServer.PORT}`, username: 'user1', password: 'secret1' } }); const page = await context.newPage(); await page.goto('http://non-existent.com/target.html'); - expect(await page.title()).toBe('Basic ' + Buffer.from('user1:secret1').toString('base64')); + expect(auth).toBe('Basic ' + Buffer.from('user1:secret1').toString('base64')); + expect(await page.title()).toBe('Served by the proxy'); await context.close(); } + auth = undefined; { const context = await contextFactory({ - proxy: { server: `localhost:${server.PORT}`, username: 'user2', password: 'secret2' } + proxy: { server: `localhost:${proxyServer.PORT}`, username: 'user2', password: 'secret2' } }); const page = await context.newPage(); await page.goto('http://non-existent.com/target.html'); - expect(await page.title()).toBe('Basic ' + Buffer.from('user2:secret2').toString('base64')); + expect(await page.title()).toBe('Served by the proxy'); + expect(auth).toBe('Basic ' + Buffer.from('user2:secret2').toString('base64')); await context.close(); } }); -it('should exclude patterns', async ({contextFactory, server, browserName, headless}) => { +it('should exclude patterns', async ({contextFactory, server, browserName, headless, proxyServer}) => { it.fixme(browserName === 'chromium' && !headless, 'Chromium headed crashes with CHECK(!in_frame_tree_) in RenderFrameImpl::OnDeleteFrame.'); - server.setRoute('/target.html', async (req, res) => { - res.end('Served by the proxy'); - }); + proxyServer.forwardTo(server.PORT); // 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 context = await contextFactory({ - proxy: { server: `localhost:${server.PORT}`, bypass: '1.non.existent.domain.for.the.test, 2.non.existent.domain.for.the.test, .another.test' } + proxy: { server: `localhost:${proxyServer.PORT}`, bypass: '1.non.existent.domain.for.the.test, 2.non.existent.domain.for.the.test, .another.test' } }); const page = await context.newPage(); await page.goto('http://0.non.existent.domain.for.the.test/target.html'); + expect(proxyServer.requestUrls).toContain('http://0.non.existent.domain.for.the.test/target.html'); expect(await page.title()).toBe('Served by the proxy'); + proxyServer.requestUrls = []; { const error = await page.goto('http://1.non.existent.domain.for.the.test/target.html').catch(e => e); + expect(proxyServer.requestUrls).toEqual([]); expect(error.message).toBeTruthy(); } { const error = await page.goto('http://2.non.existent.domain.for.the.test/target.html').catch(e => e); + expect(proxyServer.requestUrls).toEqual([]); expect(error.message).toBeTruthy(); } { const error = await page.goto('http://foo.is.the.another.test/target.html').catch(e => e); + expect(proxyServer.requestUrls).toEqual([]); expect(error.message).toBeTruthy(); } { await page.goto('http://3.non.existent.domain.for.the.test/target.html'); + expect(proxyServer.requestUrls).toContain('http://3.non.existent.domain.for.the.test/target.html'); expect(await page.title()).toBe('Served by the proxy'); } diff --git a/tests/config/proxy.ts b/tests/config/proxy.ts index 6abeea4a6b..8ad66e1f83 100644 --- a/tests/config/proxy.ts +++ b/tests/config/proxy.ts @@ -21,22 +21,25 @@ import createProxy from 'proxy'; export class TestProxy { readonly PORT: number; readonly URL: string; - readonly server: Server; + connectHosts: string[] = []; + requestUrls: string[] = []; + + private readonly _server: Server; private readonly _sockets = new Set(); - private _connectHandlers = []; + private _handlers: { event: string, handler: (...args: any[]) => void }[] = []; static async create(port: number): Promise { const proxy = new TestProxy(port); - await new Promise(f => proxy.server.listen(port, f)); + await new Promise(f => proxy._server.listen(port, f)); return proxy; } private constructor(port: number) { this.PORT = port; this.URL = `http://localhost:${port}`; - this.server = createProxy(); - this.server.on('connection', socket => this._onSocket(socket)); + this._server = createProxy(); + this._server.on('connection', socket => this._onSocket(socket)); } async stop(): Promise { @@ -44,18 +47,44 @@ export class TestProxy { for (const socket of this._sockets) socket.destroy(); this._sockets.clear(); - await new Promise(x => this.server.close(x)); + await new Promise(x => this._server.close(x)); } - onConnect(handler: (req: IncomingMessage) => void) { - this._connectHandlers.push(handler); - this.server.prependListener('connect', handler); + forwardTo(port: number) { + this._prependHandler('request', (req: IncomingMessage) => { + this.requestUrls.push(req.url); + const url = new URL(req.url); + url.host = `localhost:${port}`; + req.url = url.toString(); + }); + this._prependHandler('connect', (req: IncomingMessage) => { + this.connectHosts.push(req.url); + req.url = `localhost:${port}`; + }); + } + + setAuthHandler(handler: (req: IncomingMessage) => boolean) { + (this._server as any).authenticate = (req: IncomingMessage, callback) => { + try { + callback(null, handler(req)); + } catch (e) { + callback(e, false); + } + }; } reset() { - for (const handler of this._connectHandlers) - this.server.removeListener('connect', handler); - this._connectHandlers = []; + this.connectHosts = []; + this.requestUrls = []; + for (const { event, handler } of this._handlers) + this._server.removeListener(event, handler); + this._handlers = []; + (this._server as any).authenticate = undefined; + } + + private _prependHandler(event: string, handler: (...args: any[]) => void) { + this._handlers.push({ event, handler }); + this._server.prependListener(event, handler); } private _onSocket(socket: Socket) {