Skip to content

Commit

Permalink
feat(proxy): enable per-context http proxy (#4280)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Oct 29, 2020
1 parent ff7d6a2 commit 914f637
Show file tree
Hide file tree
Showing 12 changed files with 327 additions and 71 deletions.
10 changes: 10 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ Indicates that the browser is connected.
- `password` <[string]>
- `colorScheme` <"light"|"dark"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'.
- `logger` <[Logger]> Logger sink for Playwright logging.
- `proxy` <[Object]> Network proxy settings to use with this context. Note that browser needs to be launched with the global proxy for this option to work. If all contexts override the proxy, global proxy will be never used and can be any string, for example `launch({ proxy: { server: 'per-proxy' } })`.
- `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.
- `videosPath` <[string]> Enables video recording for all pages to `videosPath` folder. If not specified, videos are not recorded. Make sure to await [`browserContext.close`](#browsercontextclose) for videos to be saved.
- `videoSize` <[Object]> Specifies dimensions of the automatically recorded video. Can only be used if `videosPath` is set. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size.
- `width` <[number]> Video frame width.
Expand Down Expand Up @@ -272,6 +277,11 @@ Creates a new browser context. It won't share cookies/cache with other browser c
- `password` <[string]>
- `colorScheme` <"light"|"dark"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'.
- `logger` <[Logger]> Logger sink for Playwright logging.
- `proxy` <[Object]> Network proxy settings to use with this context. Note that browser needs to be launched with the global proxy for this option to work. If all contexts override the proxy, global proxy will be never used and can be any string, for example `launch({ proxy: { server: 'per-proxy' } })`.
- `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.
- `videosPath` <[string]> Enables video recording for all pages to `videosPath` folder. If not specified, videos are not recorded. Make sure to await [`page.close`](#pagecloseoptions) for videos to be saved.
- `videoSize` <[Object]> Specifies dimensions of the automatically recorded video. Can only be used if `videosPath` is set. If not specified the size will be equal to `viewport`. If `viewport` is not configured explicitly the video size defaults to 1280x720. Actual picture of the page will be scaled down if necessary to fit specified size.
- `width` <[number]> Video frame width.
Expand Down
12 changes: 12 additions & 0 deletions src/protocol/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,12 @@ export type BrowserNewContextParams = {
omitContent?: boolean,
path: string,
},
proxy?: {
server: string,
bypass?: string,
username?: string,
password?: string,
},
};
export type BrowserNewContextOptions = {
noDefaultViewport?: boolean,
Expand Down Expand Up @@ -442,6 +448,12 @@ export type BrowserNewContextOptions = {
omitContent?: boolean,
path: string,
},
proxy?: {
server: string,
bypass?: string,
username?: string,
password?: string,
},
};
export type BrowserNewContextResult = {
context: BrowserContextChannel,
Expand Down
7 changes: 7 additions & 0 deletions src/protocol/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,13 @@ Browser:
properties:
omitContent: boolean?
path: string
proxy:
type: object?
properties:
server: string
bypass: string?
username: string?
password: string?
returns:
context: BrowserContext

Expand Down
6 changes: 6 additions & 0 deletions src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
omitContent: tOptional(tBoolean),
path: tString,
})),
proxy: tOptional(tObject({
server: tString,
bypass: tOptional(tString),
username: tOptional(tString),
password: tOptional(tString),
})),
});
scheme.BrowserCrNewBrowserCDPSessionParams = tOptional(tObject({}));
scheme.BrowserCrStartTracingParams = tObject({
Expand Down
9 changes: 7 additions & 2 deletions src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export abstract class BrowserContext extends EventEmitter {
}

protected _authenticateProxyViaHeader() {
const proxy = this._browser._options.proxy || { username: undefined, password: undefined };
const proxy = this._options.proxy || this._browser._options.proxy || { username: undefined, password: undefined };
const { username, password } = proxy;
if (username) {
this._options.httpCredentials = { username, password: password! };
Expand All @@ -254,7 +254,7 @@ export abstract class BrowserContext extends EventEmitter {
}

protected _authenticateProxyViaCredentials() {
const proxy = this._browser._options.proxy;
const proxy = this._options.proxy || this._browser._options.proxy;
if (!proxy)
return;
const { username, password } = proxy;
Expand Down Expand Up @@ -322,6 +322,11 @@ export function validateBrowserContextOptions(options: types.BrowserContextOptio
throw new Error(`"isMobile" option is not supported with null "viewport"`);
if (!options.viewport && !options.noDefaultViewport)
options.viewport = { width: 1280, height: 720 };
if (options.proxy) {
if (!browserOptions.proxy)
throw new Error(`Browser needs to be launched with the global proxy. If all contexts override the proxy, global proxy will be never used and can be any string, for example "launch({ proxy: { server: 'per-proxy' } })"`);
options.proxy = normalizeProxySettings(options.proxy);
}
verifyGeolocation(options.geolocation);
if (options.videoSize && !options.videosPath)
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
Expand Down
6 changes: 5 additions & 1 deletion src/server/chromium/crBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ export class CRBrowser extends Browser {

async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
validateBrowserContextOptions(options, this._options);
const { browserContextId } = await this._session.send('Target.createBrowserContext', { disposeOnDetach: true });
const { browserContextId } = await this._session.send('Target.createBrowserContext', {
disposeOnDetach: true,
proxyServer: options.proxy ? options.proxy.server : undefined,
proxyBypassList: options.proxy ? options.proxy.bypass : undefined,
});
const context = new CRBrowserContext(this, browserContextId, options);
await context._initialize();
this._contexts.set(browserContextId, context);
Expand Down
73 changes: 39 additions & 34 deletions src/server/firefox/ffBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import { assert } from '../../utils/utils';
import { Browser, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { helper, RegisteredListener } from '../helper';
import * as network from '../network';
import { Page, PageBinding } from '../page';
import { ConnectionTransport } from '../transport';
Expand All @@ -31,7 +30,6 @@ export class FFBrowser extends Browser {
_connection: FFConnection;
readonly _ffPages: Map<string, FFPage>;
readonly _contexts: Map<string, FFBrowserContext>;
private _eventListeners: RegisteredListener[];
private _version = '';

static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
Expand All @@ -45,31 +43,8 @@ export class FFBrowser extends Browser {
browser._defaultContext = new FFBrowserContext(browser, undefined, options.persistent);
promises.push((browser._defaultContext as FFBrowserContext)._initialize());
}
if (options.proxy) {
const proxyServer = new URL(options.proxy.server);
let proxyPort = parseInt(proxyServer.port, 10);
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';
if (proxyServer.port === '') {
if (proxyServer.protocol === 'http:')
proxyPort = 80;
else if (proxyServer.protocol === 'https:')
proxyPort = 443;
}
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: proxyPort,
username: options.proxy.username,
password: options.proxy.password,
}));
}
if (options.proxy)
promises.push(browser._connection.send('Browser.setBrowserProxy', toJugglerProxyOptions(options.proxy)));
await Promise.all(promises);
return browser;
}
Expand All @@ -80,13 +55,11 @@ export class FFBrowser extends Browser {
this._ffPages = new Map();
this._contexts = new Map();
this._connection.on(ConnectionEvents.Disconnected, () => this._didClose());
this._eventListeners = [
helper.addEventListener(this._connection, 'Browser.attachedToTarget', this._onAttachedToTarget.bind(this)),
helper.addEventListener(this._connection, 'Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this)),
helper.addEventListener(this._connection, 'Browser.downloadCreated', this._onDownloadCreated.bind(this)),
helper.addEventListener(this._connection, 'Browser.downloadFinished', this._onDownloadFinished.bind(this)),
helper.addEventListener(this._connection, 'Browser.screencastFinished', this._onScreencastFinished.bind(this)),
];
this._connection.on('Browser.attachedToTarget', this._onAttachedToTarget.bind(this));
this._connection.on('Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this));
this._connection.on('Browser.downloadCreated', this._onDownloadCreated.bind(this));
this._connection.on('Browser.downloadFinished', this._onDownloadFinished.bind(this));
this._connection.on('Browser.screencastFinished', this._onScreencastFinished.bind(this));
}

async _initVersion() {
Expand Down Expand Up @@ -239,6 +212,12 @@ export class FFBrowserContext extends BrowserContext {
});
}));
}
if (this._options.proxy) {
promises.push(this._browser._connection.send('Browser.setContextProxy', {
browserContextId: this._browserContextId,
...toJugglerProxyOptions(this._options.proxy)
}));
}

await Promise.all(promises);
}
Expand Down Expand Up @@ -350,3 +329,29 @@ export class FFBrowserContext extends BrowserContext {
this._browser._contexts.delete(this._browserContextId);
}
}

function toJugglerProxyOptions(proxy: types.ProxySettings) {
const proxyServer = new URL(proxy.server);
let port = parseInt(proxyServer.port, 10);
let type: 'http' | 'https' | 'socks' | 'socks4' = 'http';
if (proxyServer.protocol === 'socks5:')
type = 'socks';
else if (proxyServer.protocol === 'socks4:')
type = 'socks4';
else if (proxyServer.protocol === 'https:')
type = 'https';
if (proxyServer.port === '') {
if (proxyServer.protocol === 'http:')
port = 80;
else if (proxyServer.protocol === 'https:')
port = 443;
}
return {
type,
bypass: proxy.bypass ? proxy.bypass.split(',').map(domain => domain.trim()) : [],
host: proxyServer.hostname,
port,
username: proxy.username,
password: proxy.password
};
}
1 change: 1 addition & 0 deletions src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ export type BrowserContextOptions = {
omitContent?: boolean,
path: string
},
proxy?: ProxySettings,
_tracePath?: string,
_traceResourcesPath?: string,
};
Expand Down
6 changes: 5 additions & 1 deletion src/server/webkit/wkBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ export class WKBrowser extends Browser {

async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
validateBrowserContextOptions(options, this._options);
const { browserContextId } = await this._browserSession.send('Playwright.createContext');
const createOptions = options.proxy ? {
proxyServer: options.proxy.server,
proxyBypassList: options.proxy.bypass
} : undefined;
const { browserContextId } = await this._browserSession.send('Playwright.createContext', createOptions);
options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
const context = new WKBrowserContext(this, browserContextId, options);
await context._initialize();
Expand Down
Loading

0 comments on commit 914f637

Please sign in to comment.