From d42169aba1a1b61655fe7ce7cff04a180bfcd0c2 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 19 Aug 2021 07:36:03 -0700 Subject: [PATCH] test: proxy server fixture, new test for https via http proxy (#8299) --- package-lock.json | 110 +++++++++++++++++++++++++++++ package.json | 1 + tests/browsercontext-proxy.spec.ts | 20 ++++++ tests/config/baseTest.ts | 14 +++- tests/config/proxy.ts | 72 +++++++++++++++++++ 5 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 tests/config/proxy.ts diff --git a/package-lock.json b/package-lock.json index 4e593d2145..6eb651b259 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,7 @@ "html-webpack-plugin": "^4.4.1", "ncp": "^2.0.0", "node-stream-zip": "^1.11.3", + "proxy": "^1.0.2", "react": "^17.0.1", "react-dom": "^17.0.1", "socksv5": "0.0.6", @@ -2224,6 +2225,21 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "node_modules/args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "dev": true, + "dependencies": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -2586,6 +2602,12 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, + "node_modules/basic-auth-parser": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/basic-auth-parser/-/basic-auth-parser-0.0.2.tgz", + "integrity": "sha1-zp5xp38jwSee7NJlmypGJEwVbkE=", + "dev": true + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -2948,6 +2970,15 @@ "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", "dev": true }, + "node_modules/camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001246", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001246.tgz", @@ -6351,6 +6382,15 @@ "node": ">=0.10.0" } }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6755,6 +6795,15 @@ "rimraf": "bin.js" } }, + "node_modules/mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7588,6 +7637,20 @@ "dev": true, "optional": true }, + "node_modules/proxy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/proxy/-/proxy-1.0.2.tgz", + "integrity": "sha512-KNac2ueWRpjbUh77OAFPZuNdfEqNynm9DD4xHT14CccGpW8wKZwEkN0yjlb7X9G9Z9F55N0Q+1z+WfgAhwYdzQ==", + "dev": true, + "dependencies": { + "args": "5.0.1", + "basic-auth-parser": "0.0.2", + "debug": "^4.1.1" + }, + "bin": { + "proxy": "bin/proxy.js" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -11906,6 +11969,18 @@ } } }, + "args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "dev": true, + "requires": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -12191,6 +12266,12 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, + "basic-auth-parser": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/basic-auth-parser/-/basic-auth-parser-0.0.2.tgz", + "integrity": "sha1-zp5xp38jwSee7NJlmypGJEwVbkE=", + "dev": true + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -12521,6 +12602,12 @@ } } }, + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "dev": true + }, "caniuse-lite": { "version": "1.0.30001246", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001246.tgz", @@ -15307,6 +15394,12 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -15642,6 +15735,12 @@ } } }, + "mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -16315,6 +16414,17 @@ "dev": true, "optional": true }, + "proxy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/proxy/-/proxy-1.0.2.tgz", + "integrity": "sha512-KNac2ueWRpjbUh77OAFPZuNdfEqNynm9DD4xHT14CccGpW8wKZwEkN0yjlb7X9G9Z9F55N0Q+1z+WfgAhwYdzQ==", + "dev": true, + "requires": { + "args": "5.0.1", + "basic-auth-parser": "0.0.2", + "debug": "^4.1.1" + } + }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", diff --git a/package.json b/package.json index 5531fdcb83..0db099bb72 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "html-webpack-plugin": "^4.4.1", "ncp": "^2.0.0", "node-stream-zip": "^1.11.3", + "proxy": "^1.0.2", "react": "^17.0.1", "react-dom": "^17.0.1", "socksv5": "0.0.6", diff --git a/tests/browsercontext-proxy.spec.ts b/tests/browsercontext-proxy.spec.ts index df20765ed5..3edfbe2c1e 100644 --- a/tests/browsercontext-proxy.spec.ts +++ b/tests/browsercontext-proxy.spec.ts @@ -115,6 +115,26 @@ it('should use proxy for second page', async ({contextFactory, server}) => { await context.close(); }); +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; + }); + 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'); + 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'); diff --git a/tests/config/baseTest.ts b/tests/config/baseTest.ts index 770eff2515..d63ae11688 100644 --- a/tests/config/baseTest.ts +++ b/tests/config/baseTest.ts @@ -24,6 +24,7 @@ import * as childProcess from 'child_process'; import { start } from '../../lib/outofprocess'; import { PlaywrightClient } from '../../lib/remote/playwrightClient'; import type { LaunchOptions } from '../../index'; +import { TestProxy } from './proxy'; export type BrowserName = 'chromium' | 'firefox' | 'webkit'; type Mode = 'default' | 'driver' | 'service'; @@ -130,6 +131,7 @@ type ServerFixtures = { server: TestServer; httpsServer: TestServer; socksPort: number; + proxyServer: TestProxy; asset: (p: string) => string; }; @@ -140,7 +142,7 @@ const serverFixtures: Fixtures path.join(__dirname, '..', 'assets', ...p.split('/')), server, httpsServer, socksPort, + proxyServer, socksServer, }); @@ -180,6 +186,7 @@ const serverFixtures: Fixtures { + __servers.proxyServer.reset(); + await run(__servers.proxyServer); + }, + asset: async ({ __servers }, run) => { await run(__servers.asset); }, diff --git a/tests/config/proxy.ts b/tests/config/proxy.ts new file mode 100644 index 0000000000..6abeea4a6b --- /dev/null +++ b/tests/config/proxy.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IncomingMessage, Server } from 'http'; +import { Socket } from 'net'; +import createProxy from 'proxy'; + +export class TestProxy { + readonly PORT: number; + readonly URL: string; + readonly server: Server; + + private readonly _sockets = new Set(); + private _connectHandlers = []; + + static async create(port: number): Promise { + const proxy = new TestProxy(port); + 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)); + } + + async stop(): Promise { + this.reset(); + for (const socket of this._sockets) + socket.destroy(); + this._sockets.clear(); + await new Promise(x => this.server.close(x)); + } + + onConnect(handler: (req: IncomingMessage) => void) { + this._connectHandlers.push(handler); + this.server.prependListener('connect', handler); + } + + reset() { + for (const handler of this._connectHandlers) + this.server.removeListener('connect', handler); + this._connectHandlers = []; + } + + private _onSocket(socket: Socket) { + this._sockets.add(socket); + // ECONNRESET and HPE_INVALID_EOF_STATE are legit errors given + // that tab closing aborts outgoing connections to the server. + socket.on('error', (error: any) => { + if (error.code !== 'ECONNRESET' && error.code !== 'HPE_INVALID_EOF_STATE') + throw error; + }); + socket.once('close', () => this._sockets.delete(socket)); + } + +}