From fb058ffe0dc8456dd797be03040a10c63d9b6b6f Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 5 Jun 2020 13:50:15 -0700 Subject: [PATCH] feat(proxy): allow specifying proxy (#2485) --- browsers.json | 4 +- docs/api.md | 15 +++ package-lock.json | 115 +++++++++++++++++++ package.json | 1 + src/browser.ts | 2 + src/browserContext.ts | 34 ++++++ src/chromium/crBrowser.ts | 1 + src/firefox/ffBrowser.ts | 1 + src/server/browserType.ts | 6 +- src/server/chromium.ts | 16 ++- src/server/firefox.ts | 19 +++- src/server/webkit.ts | 17 ++- src/types.ts | 7 ++ src/webkit/wkBrowser.ts | 1 + test/proxy.spec.js | 118 ++++++++++++++++++++ test/test.config.js | 1 + utils/doclint/check_public_api/MDBuilder.js | 2 + 17 files changed, 354 insertions(+), 6 deletions(-) create mode 100644 test/proxy.spec.js diff --git a/browsers.json b/browsers.json index a2271dc5da..da0af913e4 100644 --- a/browsers.json +++ b/browsers.json @@ -6,11 +6,11 @@ }, { "name": "firefox", - "revision": "1101" + "revision": "1103" }, { "name": "webkit", - "revision": "1263" + "revision": "1269" } ] } diff --git a/docs/api.md b/docs/api.md index 971d333d75..3d7e39fc18 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3997,6 +3997,11 @@ This methods attaches Playwright to an existing browser instance. - `executablePath` <[string]> Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). Note that Playwright only works with the bundled Chromium, Firefox or WebKit, use at your own risk. - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). - `ignoreDefaultArgs` <[boolean]|[Array]<[string]>> If `true`, Playwright does not pass its own configurations args and only uses the ones from `args`. If an array is given, then filters out the given default arguments. Dangerous option; use with care. Defaults to `false`. + - `proxy` <[Object]> Network proxy settings. + - `server` <[string]> Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example `http://myproxy.com:3128` or `socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP proxy. + - `bypass` <[string]> Optional coma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`. + - `username` <[string]> Optional username to use if HTTP proxy requires authentication. + - `password` <[string]> Optional password to use if HTTP proxy requires authentication. - `firefoxUserPrefs` <[Object]> Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. - `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`. @@ -4031,6 +4036,11 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'. - `executablePath` <[string]> Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Playwright is only guaranteed to work with the bundled Chromium, Firefox or WebKit, use at your own risk. - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). - `ignoreDefaultArgs` <[boolean]|[Array]<[string]>> If `true`, then do not use any of the default arguments. If an array is given, then filter out the given default arguments. Dangerous option; use with care. Defaults to `false`. + - `proxy` <[Object]> Network proxy settings. + - `server` <[string]> Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example `http://myproxy.com:3128` or `socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP proxy. + - `bypass` <[string]> Optional coma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`. + - `username` <[string]> Optional username to use if HTTP proxy requires authentication. + - `password` <[string]> Optional password to use if HTTP proxy requires authentication. - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. - `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`. - `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`. @@ -4073,6 +4083,11 @@ Launches browser that uses persistent storage located at `userDataDir` and retur - `executablePath` <[string]> Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Playwright is only guaranteed to work with the bundled Chromium, Firefox or WebKit, use at your own risk. - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). - `ignoreDefaultArgs` <[boolean]|[Array]<[string]>> If `true`, then do not use any of the default arguments. If an array is given, then filter out the given default arguments. Dangerous option; use with care. Defaults to `false`. + - `proxy` <[Object]> Network proxy settings. + - `server` <[string]> Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example `http://myproxy.com:3128` or `socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP proxy. + - `bypass` <[string]> Optional coma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`. + - `username` <[string]> Optional username to use if HTTP proxy requires authentication. + - `password` <[string]> Optional password to use if HTTP proxy requires authentication. - `firefoxUserPrefs` <[Object]> Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. - `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`. diff --git a/package-lock.json b/package-lock.json index 9e7a790674..b6b2c79387 100644 --- a/package-lock.json +++ b/package-lock.json @@ -725,6 +725,12 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, "async-each": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", @@ -1261,6 +1267,15 @@ } } }, + "cli": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/cli/-/cli-0.4.5.tgz", + "integrity": "sha1-ePlIXNFhtWbppsctcXDEJw6B22E=", + "dev": true, + "requires": { + "glob": ">= 3.1.4" + } + }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -1276,6 +1291,25 @@ "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", "dev": true }, + "cliff": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/cliff/-/cliff-0.1.10.tgz", + "integrity": "sha1-U74z6p9ZvshWCe4wCsQgdgPlIBM=", + "dev": true, + "requires": { + "colors": "~1.0.3", + "eyes": "~0.1.8", + "winston": "0.8.x" + }, + "dependencies": { + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + } + } + }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", @@ -1560,6 +1594,12 @@ "randomfill": "^1.0.3" } }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "dev": true + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -2238,6 +2278,12 @@ "yauzl": "^2.10.0" } }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "dev": true + }, "fast-deep-equal": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", @@ -3199,6 +3245,12 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, "jpeg-js": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.3.7.tgz", @@ -4042,6 +4094,12 @@ "find-up": "^3.0.0" } }, + "pkginfo": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", + "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=", + "dev": true + }, "pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -4800,6 +4858,34 @@ } } }, + "socksv5": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/socksv5/-/socksv5-0.0.6.tgz", + "integrity": "sha1-EycjX/fo3iGsQ0oKV53GnD8HEGE=", + "dev": true, + "requires": { + "ipv6": "*" + }, + "dependencies": { + "ipv6": { + "version": "3.1.1", + "bundled": true, + "dev": true, + "requires": { + "cli": "0.4.x", + "cliff": "0.1.x", + "sprintf": "0.1.x" + }, + "dependencies": { + "sprintf": { + "version": "0.1.3", + "bundled": true, + "dev": true + } + } + } + } + }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -4867,6 +4953,12 @@ "figgy-pudding": "^3.5.1" } }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "dev": true + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -5725,6 +5817,29 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, + "winston": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-0.8.3.tgz", + "integrity": "sha1-ZLar9M0Brcrv1QCTk7HY6L7BnbA=", + "dev": true, + "requires": { + "async": "0.2.x", + "colors": "0.6.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "pkginfo": "0.3.x", + "stack-trace": "0.0.x" + }, + "dependencies": { + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + } + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index 72ad2c014d..ef5ec78ed8 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "ncp": "^2.0.0", "node-stream-zip": "^1.8.2", "pixelmatch": "^4.0.2", + "socksv5": "0.0.6", "text-diff": "^1.0.1", "ts-loader": "^6.1.2", "typescript": "^3.8.3", diff --git a/src/browser.ts b/src/browser.ts index 36e3e06936..2ecfd25323 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -21,6 +21,7 @@ import { Download } from './download'; import type { BrowserServer } from './server/browserServer'; import { Events } from './events'; import { InnerLogger, Log } from './logger'; +import { ProxySettings } from './types'; export type BrowserOptions = { logger: InnerLogger, @@ -29,6 +30,7 @@ export type BrowserOptions = { persistent?: PersistentContextOptions, // Undefined means no persistent context. slowMo?: number, ownedServer?: BrowserServer, + proxy?: ProxySettings, }; export interface Browser extends EventEmitter { diff --git a/src/browserContext.ts b/src/browserContext.ts index 6b32bfb26c..9fb004ea01 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -207,6 +207,26 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements await oldPage.close(); } } + + protected _authenticateProxyViaHeader() { + const proxy = this._browserBase._options.proxy || { username: undefined, password: undefined }; + const { username, password } = proxy; + if (username) { + this._options.httpCredentials = { username, password: password! }; + this._options.extraHTTPHeaders = this._options.extraHTTPHeaders || {}; + const token = Buffer.from(`${username}:${password}`).toString('base64'); + this._options.extraHTTPHeaders['Proxy-Authorization'] = `Basic ${token}`; + } + } + + protected _authenticateProxyViaCredentials() { + const proxy = this._browserBase._options.proxy; + if (!proxy) + return; + const { username, password } = proxy; + if (username && password) + this._options.httpCredentials = { username, password }; + } } export function assertBrowserContextIsNotOwned(context: BrowserContextBase) { @@ -272,3 +292,17 @@ export function verifyGeolocation(geolocation: types.Geolocation): types.Geoloca throw new Error(`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`); return result; } + +export function verifyProxySettings(proxy: types.ProxySettings): types.ProxySettings { + let { server, bypass } = proxy; + if (!helper.isString(server)) + throw new Error(`Invalid proxy.server: ` + server); + let url = new URL(server); + if (!['http:', 'https:', 'socks5:'].includes(url.protocol)) { + url = new URL('http://' + server); + server = `${url.protocol}//${url.host}`; + } + if (bypass) + bypass = bypass.split(',').map(t => t.trim()).join(','); + return { ...proxy, server, bypass }; +} diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index eb146aedec..8e23b711d2 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -286,6 +286,7 @@ export class CRBrowserContext extends BrowserContextBase { this._browser = browser; this._browserContextId = browserContextId; this._evaluateOnNewDocumentSources = []; + this._authenticateProxyViaCredentials(); } async _initialize() { diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 9e3b404a83..85b3b211ca 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -152,6 +152,7 @@ export class FFBrowserContext extends BrowserContextBase { this._browser = browser; this._browserContextId = browserContextId; this._evaluateOnNewDocumentSources = []; + this._authenticateProxyViaHeader(); } async _initialize() { diff --git a/src/server/browserType.ts b/src/server/browserType.ts index 1bd1dba157..40dd892d1d 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -18,7 +18,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import * as util from 'util'; -import { BrowserContext, PersistentContextOptions, validatePersistentContextOptions } from '../browserContext'; +import { BrowserContext, PersistentContextOptions, validatePersistentContextOptions, verifyProxySettings } from '../browserContext'; import { BrowserServer, WebSocketWrapper } from './browserServer'; import * as browserPaths from '../install/browserPaths'; import { Logger, RootLogger, InnerLogger } from '../logger'; @@ -29,11 +29,13 @@ import { launchProcess, Env, waitForLine } from './processLauncher'; import { Events } from '../events'; import { PipeTransport } from './pipeTransport'; import { Progress, runAbortableTask } from '../progress'; +import { ProxySettings } from '../types'; export type BrowserArgOptions = { headless?: boolean, args?: string[], devtools?: boolean, + proxy?: ProxySettings, }; export type FirefoxUserPrefsOptions = { @@ -120,6 +122,7 @@ export abstract class BrowserTypeBase implements BrowserType { } async _innerLaunch(progress: Progress, options: LaunchOptions, logger: RootLogger, persistent: PersistentContextOptions | undefined, userDataDir?: string): Promise { + options.proxy = options.proxy ? verifyProxySettings(options.proxy) : undefined; const { browserServer, downloadsPath, transport } = await this._launchServer(progress, options, !!persistent, logger, userDataDir); if ((options as any).__testHookBeforeCreateBrowser) await (options as any).__testHookBeforeCreateBrowser(); @@ -130,6 +133,7 @@ export abstract class BrowserTypeBase implements BrowserType { logger, downloadsPath, ownedServer: browserServer, + proxy: options.proxy, }; copyTestHooks(options, browserOptions); const browser = await this._connectToTransport(transport, browserOptions); diff --git a/src/server/chromium.ts b/src/server/chromium.ts index 8dd8774124..bef71ba4b1 100644 --- a/src/server/chromium.ts +++ b/src/server/chromium.ts @@ -79,7 +79,7 @@ export class Chromium extends BrowserTypeBase { _defaultArgs(options: BrowserArgOptions, isPersistent: boolean, userDataDir: string): string[] { const { devtools, headless } = processBrowserArgOptions(options); - const { args = [] } = options; + const { args = [], proxy } = options; const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); if (userDataDirArg) throw new Error('Pass userDataDir parameter instead of specifying --user-data-dir argument'); @@ -102,6 +102,20 @@ export class Chromium extends BrowserTypeBase { '--mute-audio' ); } + if (proxy) { + const proxyURL = new URL(proxy.server); + const isSocks = proxyURL.protocol === 'socks5:'; + // https://www.chromium.org/developers/design-documents/network-settings + if (isSocks) { + // https://www.chromium.org/developers/design-documents/network-stack/socks-proxy + chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`); + } + chromeArguments.push(`--proxy-server=${proxy.server}`); + if (proxy.bypass) { + const patterns = proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t); + chromeArguments.push(`--proxy-bypass-list=${patterns.join(';')}`); + } + } chromeArguments.push(...args); if (isPersistent) chromeArguments.push('about:blank'); diff --git a/src/server/firefox.ts b/src/server/firefox.ts index bd0f2fb823..b0121749c1 100644 --- a/src/server/firefox.ts +++ b/src/server/firefox.ts @@ -59,7 +59,7 @@ export class Firefox extends BrowserTypeBase { _defaultArgs(options: BrowserArgOptions & FirefoxUserPrefsOptions, isPersistent: boolean, userDataDir: string): string[] { const { devtools, headless } = processBrowserArgOptions(options); - const { args = [] } = options; + const { args = [], proxy } = 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')); @@ -67,6 +67,23 @@ 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'); + if (proxy) { + options.firefoxUserPrefs = options.firefoxUserPrefs || {}; + options.firefoxUserPrefs['network.proxy.type'] = 1; + const proxyServer = new URL(proxy.server); + const isSocks = proxyServer.protocol === 'socks5:'; + if (isSocks) { + options.firefoxUserPrefs['network.proxy.socks'] = proxyServer.hostname; + options.firefoxUserPrefs['network.proxy.socks_port'] = parseInt(proxyServer.port, 10); + } else { + options.firefoxUserPrefs['network.proxy.http'] = proxyServer.hostname; + options.firefoxUserPrefs['network.proxy.http_port'] = parseInt(proxyServer.port, 10); + options.firefoxUserPrefs['network.proxy.ssl'] = proxyServer.hostname; + options.firefoxUserPrefs['network.proxy.ssl_port'] = parseInt(proxyServer.port, 10); + } + if (proxy.bypass) + options.firefoxUserPrefs['network.proxy.no_proxies_on'] = proxy.bypass; + } if (options.firefoxUserPrefs) { const lines: string[] = []; for (const [name, value] of Object.entries(options.firefoxUserPrefs)) diff --git a/src/server/webkit.ts b/src/server/webkit.ts index ddd45a121d..683c2ca1d1 100644 --- a/src/server/webkit.ts +++ b/src/server/webkit.ts @@ -51,7 +51,7 @@ export class WebKit extends BrowserTypeBase { _defaultArgs(options: BrowserArgOptions, isPersistent: boolean, userDataDir: string): string[] { const { devtools, headless } = processBrowserArgOptions(options); - const { args = [] } = options; + const { args = [], proxy } = options; if (devtools) console.warn('devtools parameter as a launch argument in WebKit is not supported. Also starting Web Inspector manually will terminate the execution in WebKit.'); const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir=')); @@ -66,6 +66,21 @@ export class WebKit extends BrowserTypeBase { webkitArguments.push(`--user-data-dir=${userDataDir}`); else webkitArguments.push(`--no-startup-window`); + if (proxy) { + if (process.platform === 'darwin') { + webkitArguments.push(`--proxy=${proxy.server}`); + if (proxy.bypass) + webkitArguments.push(`--proxy-bypass-list=${proxy.bypass}`); + } else if (process.platform === 'linux') { + webkitArguments.push(`--proxy=${proxy.server}`); + if (proxy.bypass) + webkitArguments.push(...proxy.bypass.split(',').map(t => `--ignore-host=${t}`)); + } else if (process.platform === 'win32') { + webkitArguments.push(`--curl-proxy=${proxy.server}`); + if (proxy.bypass) + webkitArguments.push(`--curl-noproxy=${proxy.bypass}`); + } + } webkitArguments.push(...args); if (isPersistent) webkitArguments.push('about:blank'); diff --git a/src/types.ts b/src/types.ts index df69579197..efc777eeac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -178,3 +178,10 @@ export type InjectedScriptPoll = { logs: Promise, cancel: () => void, }; + +export type ProxySettings = { + server: string, + bypass?: string, + username?: string, + password?: string +} diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index bfdff190b5..a78fa75b6f 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -208,6 +208,7 @@ export class WKBrowserContext extends BrowserContextBase { this._browser = browser; this._browserContextId = browserContextId; this._evaluateOnNewDocumentSources = []; + this._authenticateProxyViaHeader(); } async _initialize() { diff --git a/test/proxy.spec.js b/test/proxy.spec.js new file mode 100644 index 0000000000..bc2e96bdcc --- /dev/null +++ b/test/proxy.spec.js @@ -0,0 +1,118 @@ +/** + * 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. + */ + +const socks = require('socksv5'); +const utils = require('./utils'); +const {FFOX, CHROMIUM, WEBKIT, MAC} = utils.testOptions(browserType); + +describe('HTTP Proxy', () => { + it('should use proxy', async ({browserType, defaultBrowserOptions, server}) => { + server.setRoute('/target.html', async (req, res) => { + res.end('Served by the proxy'); + }); + const browser = await browserType.launch({ + ...defaultBrowserOptions, + proxy: { server: `localhost:${server.PORT}` } + }); + const page = await browser.newPage(); + await page.goto('http://non-existent.com/target.html'); + expect(await page.title()).toBe('Served by the proxy'); + await browser.close(); + }); + + it('should authenticate', async ({browserType, defaultBrowserOptions, 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(); + } + res.end(`${auth}`); + }); + const browser = await browserType.launch({ + ...defaultBrowserOptions, + proxy: { server: `localhost:${server.PORT}`, username: 'user', password: 'secret' } + }); + const page = await browser.newPage(); + await page.goto('http://non-existent.com/target.html'); + expect(await page.title()).toBe('Basic ' + Buffer.from('user:secret').toString('base64')); + await browser.close(); + }); + + it('should exclude patterns', async ({browserType, defaultBrowserOptions, server}) => { + server.setRoute('/target.html', async (req, res) => { + res.end('Served by the proxy'); + }); + const browser = await browserType.launch({ + ...defaultBrowserOptions, + proxy: { server: `localhost:${server.PORT}`, bypass: 'non-existent1.com, .non-existent2.com, .zone' } + }); + + const page = await browser.newPage(); + await page.goto('http://non-existent.com/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); + expect(error.message).toBeTruthy(); + } + + { + const error = await page.goto('http://sub.non-existent2.com/target.html').catch(e => e); + expect(error.message).toBeTruthy(); + } + + { + const error = await page.goto('http://foo.zone/target.html').catch(e => e); + expect(error.message).toBeTruthy(); + } + + await browser.close(); + }); +}); + +describe('SOCKS Proxy', () => { + it('should use proxy', async ({ browserType, defaultBrowserOptions, parallelIndex }) => { + const server = socks.createServer((info, accept, deny) => { + if (socket = accept(true)) { + const body = 'Served by the SOCKS proxy'; + socket.end([ + 'HTTP/1.1 200 OK', + 'Connection: close', + 'Content-Type: text/html', + 'Content-Length: ' + Buffer.byteLength(body), + '', + body + ].join('\r\n')); + } + }); + const socksPort = 9107 + parallelIndex * 2; + server.listen(socksPort, 'localhost'); + server.useAuth(socks.auth.None()); + + const browser = await browserType.launch({ + ...defaultBrowserOptions, + proxy: { server: `socks5://localhost:${socksPort}` } + }); + const page = await browser.newPage(); + await page.goto('http://non-existent.com'); + expect(await page.title()).toBe('Served by the SOCKS proxy'); + await browser.close(); + server.close(); + }); +}); diff --git a/test/test.config.js b/test/test.config.js index 4a8d5f833a..54a3a875c9 100644 --- a/test/test.config.js +++ b/test/test.config.js @@ -228,6 +228,7 @@ module.exports = { './logger.spec.js', './headful.spec.js', './multiclient.spec.js', + './proxy.spec.js', ], environments: [customEnvironment], }, diff --git a/utils/doclint/check_public_api/MDBuilder.js b/utils/doclint/check_public_api/MDBuilder.js index b3b5645b5a..7899b93e4c 100644 --- a/utils/doclint/check_public_api/MDBuilder.js +++ b/utils/doclint/check_public_api/MDBuilder.js @@ -70,6 +70,8 @@ class MDOutline { property.required = defaultRequired; if (property.comment.toLowerCase().includes('defaults to ')) property.required = false; + if (property.comment.startsWith('Optional ')) + property.required = false; if (property.comment.toLowerCase().includes('if applicable.')) property.required = false; if (property.comment.toLowerCase().includes('if available.'))