Skip to content

Commit 914f637

Browse files
authored
feat(proxy): enable per-context http proxy (#4280)
1 parent ff7d6a2 commit 914f637

File tree

12 files changed

+327
-71
lines changed

12 files changed

+327
-71
lines changed

docs/api.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@ Indicates that the browser is connected.
223223
- `password` <[string]>
224224
- `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`'.
225225
- `logger` <[Logger]> Logger sink for Playwright logging.
226+
- `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' } })`.
227+
- `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.
228+
- `bypass` <[string]> Optional coma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
229+
- `username` <[string]> Optional username to use if HTTP proxy requires authentication.
230+
- `password` <[string]> Optional password to use if HTTP proxy requires authentication.
226231
- `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.
227232
- `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.
228233
- `width` <[number]> Video frame width.
@@ -272,6 +277,11 @@ Creates a new browser context. It won't share cookies/cache with other browser c
272277
- `password` <[string]>
273278
- `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`'.
274279
- `logger` <[Logger]> Logger sink for Playwright logging.
280+
- `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' } })`.
281+
- `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.
282+
- `bypass` <[string]> Optional coma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
283+
- `username` <[string]> Optional username to use if HTTP proxy requires authentication.
284+
- `password` <[string]> Optional password to use if HTTP proxy requires authentication.
275285
- `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.
276286
- `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.
277287
- `width` <[number]> Video frame width.

src/protocol/channels.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,12 @@ export type BrowserNewContextParams = {
398398
omitContent?: boolean,
399399
path: string,
400400
},
401+
proxy?: {
402+
server: string,
403+
bypass?: string,
404+
username?: string,
405+
password?: string,
406+
},
401407
};
402408
export type BrowserNewContextOptions = {
403409
noDefaultViewport?: boolean,
@@ -442,6 +448,12 @@ export type BrowserNewContextOptions = {
442448
omitContent?: boolean,
443449
path: string,
444450
},
451+
proxy?: {
452+
server: string,
453+
bypass?: string,
454+
username?: string,
455+
password?: string,
456+
},
445457
};
446458
export type BrowserNewContextResult = {
447459
context: BrowserContextChannel,

src/protocol/protocol.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,13 @@ Browser:
396396
properties:
397397
omitContent: boolean?
398398
path: string
399+
proxy:
400+
type: object?
401+
properties:
402+
server: string
403+
bypass: string?
404+
username: string?
405+
password: string?
399406
returns:
400407
context: BrowserContext
401408

src/protocol/validator.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
233233
omitContent: tOptional(tBoolean),
234234
path: tString,
235235
})),
236+
proxy: tOptional(tObject({
237+
server: tString,
238+
bypass: tOptional(tString),
239+
username: tOptional(tString),
240+
password: tOptional(tString),
241+
})),
236242
});
237243
scheme.BrowserCrNewBrowserCDPSessionParams = tOptional(tObject({}));
238244
scheme.BrowserCrStartTracingParams = tObject({

src/server/browserContext.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export abstract class BrowserContext extends EventEmitter {
241241
}
242242

243243
protected _authenticateProxyViaHeader() {
244-
const proxy = this._browser._options.proxy || { username: undefined, password: undefined };
244+
const proxy = this._options.proxy || this._browser._options.proxy || { username: undefined, password: undefined };
245245
const { username, password } = proxy;
246246
if (username) {
247247
this._options.httpCredentials = { username, password: password! };
@@ -254,7 +254,7 @@ export abstract class BrowserContext extends EventEmitter {
254254
}
255255

256256
protected _authenticateProxyViaCredentials() {
257-
const proxy = this._browser._options.proxy;
257+
const proxy = this._options.proxy || this._browser._options.proxy;
258258
if (!proxy)
259259
return;
260260
const { username, password } = proxy;
@@ -322,6 +322,11 @@ export function validateBrowserContextOptions(options: types.BrowserContextOptio
322322
throw new Error(`"isMobile" option is not supported with null "viewport"`);
323323
if (!options.viewport && !options.noDefaultViewport)
324324
options.viewport = { width: 1280, height: 720 };
325+
if (options.proxy) {
326+
if (!browserOptions.proxy)
327+
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' } })"`);
328+
options.proxy = normalizeProxySettings(options.proxy);
329+
}
325330
verifyGeolocation(options.geolocation);
326331
if (options.videoSize && !options.videosPath)
327332
throw new Error(`"videoSize" option requires "videosPath" to be specified`);

src/server/chromium/crBrowser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ export class CRBrowser extends Browser {
9999

100100
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
101101
validateBrowserContextOptions(options, this._options);
102-
const { browserContextId } = await this._session.send('Target.createBrowserContext', { disposeOnDetach: true });
102+
const { browserContextId } = await this._session.send('Target.createBrowserContext', {
103+
disposeOnDetach: true,
104+
proxyServer: options.proxy ? options.proxy.server : undefined,
105+
proxyBypassList: options.proxy ? options.proxy.bypass : undefined,
106+
});
103107
const context = new CRBrowserContext(this, browserContextId, options);
104108
await context._initialize();
105109
this._contexts.set(browserContextId, context);

src/server/firefox/ffBrowser.ts

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import { assert } from '../../utils/utils';
1919
import { Browser, BrowserOptions } from '../browser';
2020
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
21-
import { helper, RegisteredListener } from '../helper';
2221
import * as network from '../network';
2322
import { Page, PageBinding } from '../page';
2423
import { ConnectionTransport } from '../transport';
@@ -31,7 +30,6 @@ export class FFBrowser extends Browser {
3130
_connection: FFConnection;
3231
readonly _ffPages: Map<string, FFPage>;
3332
readonly _contexts: Map<string, FFBrowserContext>;
34-
private _eventListeners: RegisteredListener[];
3533
private _version = '';
3634

3735
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
@@ -45,31 +43,8 @@ export class FFBrowser extends Browser {
4543
browser._defaultContext = new FFBrowserContext(browser, undefined, options.persistent);
4644
promises.push((browser._defaultContext as FFBrowserContext)._initialize());
4745
}
48-
if (options.proxy) {
49-
const proxyServer = new URL(options.proxy.server);
50-
let proxyPort = parseInt(proxyServer.port, 10);
51-
let aType: 'http'|'https'|'socks'|'socks4' = 'http';
52-
if (proxyServer.protocol === 'socks5:')
53-
aType = 'socks';
54-
else if (proxyServer.protocol === 'socks4:')
55-
aType = 'socks4';
56-
else if (proxyServer.protocol === 'https:')
57-
aType = 'https';
58-
if (proxyServer.port === '') {
59-
if (proxyServer.protocol === 'http:')
60-
proxyPort = 80;
61-
else if (proxyServer.protocol === 'https:')
62-
proxyPort = 443;
63-
}
64-
promises.push(browser._connection.send('Browser.setBrowserProxy', {
65-
type: aType,
66-
bypass: options.proxy.bypass ? options.proxy.bypass.split(',').map(domain => domain.trim()) : [],
67-
host: proxyServer.hostname,
68-
port: proxyPort,
69-
username: options.proxy.username,
70-
password: options.proxy.password,
71-
}));
72-
}
46+
if (options.proxy)
47+
promises.push(browser._connection.send('Browser.setBrowserProxy', toJugglerProxyOptions(options.proxy)));
7348
await Promise.all(promises);
7449
return browser;
7550
}
@@ -80,13 +55,11 @@ export class FFBrowser extends Browser {
8055
this._ffPages = new Map();
8156
this._contexts = new Map();
8257
this._connection.on(ConnectionEvents.Disconnected, () => this._didClose());
83-
this._eventListeners = [
84-
helper.addEventListener(this._connection, 'Browser.attachedToTarget', this._onAttachedToTarget.bind(this)),
85-
helper.addEventListener(this._connection, 'Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this)),
86-
helper.addEventListener(this._connection, 'Browser.downloadCreated', this._onDownloadCreated.bind(this)),
87-
helper.addEventListener(this._connection, 'Browser.downloadFinished', this._onDownloadFinished.bind(this)),
88-
helper.addEventListener(this._connection, 'Browser.screencastFinished', this._onScreencastFinished.bind(this)),
89-
];
58+
this._connection.on('Browser.attachedToTarget', this._onAttachedToTarget.bind(this));
59+
this._connection.on('Browser.detachedFromTarget', this._onDetachedFromTarget.bind(this));
60+
this._connection.on('Browser.downloadCreated', this._onDownloadCreated.bind(this));
61+
this._connection.on('Browser.downloadFinished', this._onDownloadFinished.bind(this));
62+
this._connection.on('Browser.screencastFinished', this._onScreencastFinished.bind(this));
9063
}
9164

9265
async _initVersion() {
@@ -239,6 +212,12 @@ export class FFBrowserContext extends BrowserContext {
239212
});
240213
}));
241214
}
215+
if (this._options.proxy) {
216+
promises.push(this._browser._connection.send('Browser.setContextProxy', {
217+
browserContextId: this._browserContextId,
218+
...toJugglerProxyOptions(this._options.proxy)
219+
}));
220+
}
242221

243222
await Promise.all(promises);
244223
}
@@ -350,3 +329,29 @@ export class FFBrowserContext extends BrowserContext {
350329
this._browser._contexts.delete(this._browserContextId);
351330
}
352331
}
332+
333+
function toJugglerProxyOptions(proxy: types.ProxySettings) {
334+
const proxyServer = new URL(proxy.server);
335+
let port = parseInt(proxyServer.port, 10);
336+
let type: 'http' | 'https' | 'socks' | 'socks4' = 'http';
337+
if (proxyServer.protocol === 'socks5:')
338+
type = 'socks';
339+
else if (proxyServer.protocol === 'socks4:')
340+
type = 'socks4';
341+
else if (proxyServer.protocol === 'https:')
342+
type = 'https';
343+
if (proxyServer.port === '') {
344+
if (proxyServer.protocol === 'http:')
345+
port = 80;
346+
else if (proxyServer.protocol === 'https:')
347+
port = 443;
348+
}
349+
return {
350+
type,
351+
bypass: proxy.bypass ? proxy.bypass.split(',').map(domain => domain.trim()) : [],
352+
host: proxyServer.hostname,
353+
port,
354+
username: proxy.username,
355+
password: proxy.password
356+
};
357+
}

src/server/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ export type BrowserContextOptions = {
244244
omitContent?: boolean,
245245
path: string
246246
},
247+
proxy?: ProxySettings,
247248
_tracePath?: string,
248249
_traceResourcesPath?: string,
249250
};

src/server/webkit/wkBrowser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ export class WKBrowser extends Browser {
7575

7676
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
7777
validateBrowserContextOptions(options, this._options);
78-
const { browserContextId } = await this._browserSession.send('Playwright.createContext');
78+
const createOptions = options.proxy ? {
79+
proxyServer: options.proxy.server,
80+
proxyBypassList: options.proxy.bypass
81+
} : undefined;
82+
const { browserContextId } = await this._browserSession.send('Playwright.createContext', createOptions);
7983
options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
8084
const context = new WKBrowserContext(this, browserContextId, options);
8185
await context._initialize();

0 commit comments

Comments
 (0)