Skip to content

Commit 6f8aff5

Browse files
committed
feat: support proxy in connect/connectOverCDP
1 parent 8938662 commit 6f8aff5

File tree

20 files changed

+357
-95
lines changed

20 files changed

+357
-95
lines changed

docs/src/api/class-browsertype.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ Some common examples:
144144
1. `"<loopback>"` to expose localhost network.
145145
1. `"*.test.internal-domain,*.staging.internal-domain,<loopback>"` to expose test/staging deployments and localhost.
146146

147+
### option: BrowserType.connect.proxy
148+
* since: v1.52
149+
- `proxy` <[Object]>
150+
- `server` <[string]> Proxy to be used for the remote connection. 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.
151+
- `bypass` ?<[string]> Optional comma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
152+
- `username` ?<[string]> Optional username to use if HTTP proxy requires authentication.
153+
- `password` ?<[string]> Optional password to use if HTTP proxy requires authentication.
154+
155+
Proxy settings to use for the connection between the client and the remote browser. Note this proxy **is not** used by the browser to load web pages.
156+
147157
## async method: BrowserType.connectOverCDP
148158
* since: v1.9
149159
- returns: <[Browser]>
@@ -232,6 +242,16 @@ Logger sink for Playwright logging. Optional.
232242
Maximum time in milliseconds to wait for the connection to be established. Defaults to
233243
`30000` (30 seconds). Pass `0` to disable timeout.
234244

245+
### option: BrowserType.connectOverCDP.proxy
246+
* since: v1.52
247+
- `proxy` <[Object]>
248+
- `server` <[string]> Proxy to be used for the remote connection. 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.
249+
- `bypass` ?<[string]> Optional comma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
250+
- `username` ?<[string]> Optional username to use if HTTP proxy requires authentication.
251+
- `password` ?<[string]> Optional password to use if HTTP proxy requires authentication.
252+
253+
Proxy settings to use for the connection between the client and the remote browser. Note this proxy **is not** used by the browser to load web pages.
254+
235255
## method: BrowserType.executablePath
236256
* since: v1.8
237257
- returns: <[string]>

docs/src/api/params.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,8 @@ Dangerous option; use with care. Defaults to `false`.
226226
## browser-option-proxy
227227
- `proxy` <[Object]>
228228
- `server` <[string]> Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example
229-
`http://myproxy.com:3128` or `socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP
230-
proxy.
231-
- `bypass` ?<[string]> Optional comma-separated domains to bypass proxy, for example `".com, chromium.org,
232-
.domain.com"`.
229+
`http://myproxy.com:3128` or `socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP proxy.
230+
- `bypass` ?<[string]> Optional comma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
233231
- `username` ?<[string]> Optional username to use if HTTP proxy requires authentication.
234232
- `password` ?<[string]> Optional password to use if HTTP proxy requires authentication.
235233

packages/playwright-client/types/types.d.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21793,6 +21793,34 @@ export interface ConnectOverCDPOptions {
2179321793
*/
2179421794
logger?: Logger;
2179521795

21796+
/**
21797+
* Proxy settings to use for the connection between the client and the remote browser. Note this proxy **is not** used
21798+
* by the browser to load web pages.
21799+
*/
21800+
proxy?: {
21801+
/**
21802+
* Proxy to be used for the remote connection. HTTP and SOCKS proxies are supported, for example
21803+
* `http://myproxy.com:3128` or `socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP
21804+
* proxy.
21805+
*/
21806+
server: string;
21807+
21808+
/**
21809+
* Optional comma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
21810+
*/
21811+
bypass?: string;
21812+
21813+
/**
21814+
* Optional username to use if HTTP proxy requires authentication.
21815+
*/
21816+
username?: string;
21817+
21818+
/**
21819+
* Optional password to use if HTTP proxy requires authentication.
21820+
*/
21821+
password?: string;
21822+
};
21823+
2179621824
/**
2179721825
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going
2179821826
* on. Defaults to 0.
@@ -21834,6 +21862,34 @@ export interface ConnectOptions {
2183421862
*/
2183521863
logger?: Logger;
2183621864

21865+
/**
21866+
* Proxy settings to use for the connection between the client and the remote browser. Note this proxy **is not** used
21867+
* by the browser to load web pages.
21868+
*/
21869+
proxy?: {
21870+
/**
21871+
* Proxy to be used for the remote connection. HTTP and SOCKS proxies are supported, for example
21872+
* `http://myproxy.com:3128` or `socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP
21873+
* proxy.
21874+
*/
21875+
server: string;
21876+
21877+
/**
21878+
* Optional comma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
21879+
*/
21880+
bypass?: string;
21881+
21882+
/**
21883+
* Optional username to use if HTTP proxy requires authentication.
21884+
*/
21885+
username?: string;
21886+
21887+
/**
21888+
* Optional password to use if HTTP proxy requires authentication.
21889+
*/
21890+
password?: string;
21891+
};
21892+
2183721893
/**
2183821894
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going
2183921895
* on. Defaults to 0.

packages/playwright-core/src/client/browserType.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
129129
exposeNetwork: params.exposeNetwork ?? params._exposeNetwork,
130130
slowMo: params.slowMo,
131131
timeout: params.timeout,
132+
proxy: params.proxy,
132133
};
133134
if ((params as any).__testHookRedirectPortForwarding)
134135
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
@@ -188,7 +189,8 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
188189
endpointURL,
189190
headers,
190191
slowMo: params.slowMo,
191-
timeout: params.timeout
192+
timeout: params.timeout,
193+
proxy: params.proxy,
192194
});
193195
const browser = Browser.from(result.browser);
194196
this._didLaunchBrowser(browser, {}, params.logger);

packages/playwright-core/src/client/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ export type ConnectOptions = {
103103
slowMo?: number,
104104
timeout?: number,
105105
logger?: Logger,
106+
proxy?: {
107+
server: string,
108+
bypass?: string,
109+
username?: string,
110+
password?: string
111+
},
106112
};
107113
export type LaunchServerOptions = {
108114
channel?: channels.BrowserTypeLaunchOptions['channel'],

packages/playwright-core/src/protocol/validator.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,12 @@ scheme.LocalUtilsConnectParams = tObject({
327327
exposeNetwork: tOptional(tString),
328328
slowMo: tOptional(tNumber),
329329
timeout: tOptional(tNumber),
330+
proxy: tOptional(tObject({
331+
server: tString,
332+
bypass: tOptional(tString),
333+
username: tOptional(tString),
334+
password: tOptional(tString),
335+
})),
330336
socksProxyRedirectPortForTest: tOptional(tNumber),
331337
});
332338
scheme.LocalUtilsConnectResult = tObject({
@@ -650,6 +656,12 @@ scheme.BrowserTypeConnectOverCDPParams = tObject({
650656
headers: tOptional(tArray(tType('NameValue'))),
651657
slowMo: tOptional(tNumber),
652658
timeout: tOptional(tNumber),
659+
proxy: tOptional(tObject({
660+
server: tString,
661+
bypass: tOptional(tString),
662+
username: tOptional(tString),
663+
password: tOptional(tString),
664+
})),
653665
});
654666
scheme.BrowserTypeConnectOverCDPResult = tObject({
655667
browser: tChannel(['Browser']),

packages/playwright-core/src/server/browserType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ export abstract class BrowserType extends SdkObject {
292292
await fs.promises.mkdir(options.tracesDir, { recursive: true });
293293
}
294294

295-
async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number }, timeout?: number): Promise<Browser> {
295+
async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, proxy?: types.ProxySettings, timeout?: number, headers?: types.HeadersArray }): Promise<Browser> {
296296
throw new Error('CDP connections are only supported by Chromium');
297297
}
298298

packages/playwright-core/src/server/chromium/chromium.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,15 @@ export class Chromium extends BrowserType {
6464
this._devtools = this._createDevTools();
6565
}
6666

67-
override async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray }, timeout?: number) {
67+
override async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, proxy?: types.ProxySettings, timeout?: number, headers?: types.HeadersArray }) {
6868
const controller = new ProgressController(metadata, this);
6969
controller.setLogName('browser');
7070
return controller.run(async progress => {
7171
return await this._connectOverCDPInternal(progress, endpointURL, options);
72-
}, TimeoutSettings.timeout({ timeout }));
72+
}, TimeoutSettings.timeout(options));
7373
}
7474

75-
async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: types.LaunchOptions & { headers?: types.HeadersArray }, onClose?: () => Promise<void>) {
75+
async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: types.LaunchOptions & { headers?: types.HeadersArray, proxy?: types.ProxySettings }, onClose?: () => Promise<void>) {
7676
let headersMap: { [key: string]: string; } | undefined;
7777
if (options.headers)
7878
headersMap = headersArrayToObject(options.headers, false);
@@ -84,10 +84,10 @@ export class Chromium extends BrowserType {
8484

8585
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
8686

87-
const wsEndpoint = await urlToWSEndpoint(progress, endpointURL, headersMap);
87+
const wsEndpoint = await urlToWSEndpoint(progress, endpointURL, headersMap, options.proxy);
8888
progress.throwIfAborted();
8989

90-
const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, headersMap);
90+
const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, { headers: headersMap, proxy: options.proxy });
9191
const cleanedUp = new ManualPromise<void>();
9292
const doCleanup = async () => {
9393
await removeFolders([artifactsDir]);
@@ -365,18 +365,30 @@ class ChromiumReadyState extends BrowserReadyState {
365365
}
366366
}
367367

368-
async function urlToWSEndpoint(progress: Progress, endpointURL: string, headers: { [key: string]: string; }) {
368+
async function urlToWSEndpoint(progress: Progress, endpointURL: string, headers: { [key: string]: string; }, proxy?: types.ProxySettings) {
369369
if (endpointURL.startsWith('ws'))
370370
return endpointURL;
371371
progress.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
372372
const httpURL = endpointURL.endsWith('/') ? `${endpointURL}json/version/` : `${endpointURL}/json/version/`;
373+
// Chromium insists on localhost "Host" header for security reasons, and in the case of proxy
374+
// we end up with the remote host instead of localhost.
375+
const extraHeaders = proxy ? { Host: `localhost:9222` } : {};
373376
const json = await fetchData({
374377
url: httpURL,
375-
headers,
378+
headers: { ...headers, ...extraHeaders },
379+
proxy,
376380
}, async (_, resp) => new Error(`Unexpected status ${resp.statusCode} when connecting to ${httpURL}.\n` +
377381
`This does not look like a DevTools server, try connecting via ws://.`)
378382
);
379-
return JSON.parse(json).webSocketDebuggerUrl;
383+
const wsUrl = JSON.parse(json).webSocketDebuggerUrl;
384+
if (proxy) {
385+
// webSocketDebuggerUrl will be a localhost URL, accessible from the browser's computer.
386+
// When using a proxy, assume we need to connect through the original endpointURL.
387+
const url = new URL(endpointURL);
388+
url.pathname += (url.pathname.endsWith('/') ? '' : '/') + new URL(wsUrl).pathname.substring(1);
389+
return url.toString();
390+
}
391+
return wsUrl;
380392
}
381393

382394
async function seleniumErrorHandler(params: HTTPRequestParams, response: http.IncomingMessage) {

packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
4343
}
4444

4545
async connectOverCDP(params: channels.BrowserTypeConnectOverCDPParams, metadata: CallMetadata): Promise<channels.BrowserTypeConnectOverCDPResult> {
46-
const browser = await this._object.connectOverCDP(metadata, params.endpointURL, params, params.timeout);
46+
const browser = await this._object.connectOverCDP(metadata, params.endpointURL, params);
4747
const browserDispatcher = new BrowserDispatcher(this, browser);
4848
return {
4949
browser: browserDispatcher,

packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type { RootDispatcher } from './dispatcher';
3333
import type * as channels from '@protocol/channels';
3434
import type * as http from 'http';
3535
import type { HTTPRequestParams } from '../utils/network';
36+
import type { ProxySettings } from '../types';
3637

3738
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
3839
_type_LocalUtils: boolean;
@@ -90,9 +91,9 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
9091
'x-playwright-proxy': params.exposeNetwork ?? '',
9192
...params.headers,
9293
};
93-
const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint);
94+
const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint, params.proxy);
9495

95-
const transport = await WebSocketTransport.connect(progress, wsEndpoint, wsHeaders, true, 'x-playwright-debug-log');
96+
const transport = await WebSocketTransport.connect(progress, wsEndpoint, { headers: wsHeaders, followRedirects: true, debugLogHeader: 'x-playwright-debug-log', proxy: params.proxy });
9697
const socksInterceptor = new SocksInterceptor(transport, params.exposeNetwork, params.socksProxyRedirectPortForTest);
9798
const pipe = new JsonPipeDispatcher(this);
9899
transport.onmessage = json => {
@@ -128,7 +129,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
128129
}
129130
}
130131

131-
async function urlToWSEndpoint(progress: Progress | undefined, endpointURL: string): Promise<string> {
132+
async function urlToWSEndpoint(progress: Progress | undefined, endpointURL: string, proxy?: ProxySettings): Promise<string> {
132133
if (endpointURL.startsWith('ws'))
133134
return endpointURL;
134135

@@ -142,6 +143,7 @@ async function urlToWSEndpoint(progress: Progress | undefined, endpointURL: stri
142143
method: 'GET',
143144
timeout: progress?.timeUntilDeadline() ?? 30_000,
144145
headers: { 'User-Agent': getUserAgent() },
146+
proxy,
145147
}, async (params: HTTPRequestParams, response: http.IncomingMessage) => {
146148
return new Error(`Unexpected status ${response.statusCode} when connecting to ${fetchUrl.toString()}.\n` +
147149
`This does not look like a Playwright server, try connecting via ws://.`);

0 commit comments

Comments
 (0)