Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
392 changes: 380 additions & 12 deletions packages/playwright-core/ThirdPartyNotices.txt

Large diffs are not rendered by default.

351 changes: 22 additions & 329 deletions packages/playwright-core/bundles/utils/package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/playwright-core/bundles/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"diff": "^7.0.0",
"dotenv": "^16.4.5",
"graceful-fs": "4.2.10",
"https-proxy-agent": "7.0.6",
"https-proxy-agent": "5.0.1",
"jpeg-js": "0.4.4",
"mime": "^3.0.0",
"minimatch": "^3.1.2",
Expand All @@ -25,7 +25,7 @@
"proxy-from-env": "1.1.0",
"retry": "0.12.0",
"signal-exit": "3.0.7",
"socks-proxy-agent": "8.0.5",
"socks-proxy-agent": "6.1.1",
"stack-utils": "2.0.5",
"ws": "8.17.1",
"yaml": "^2.6.0"
Expand Down
34 changes: 13 additions & 21 deletions packages/playwright-core/src/server/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import http from 'http';
import https from 'https';
import type { Readable, TransformCallback } from 'stream';
import { pipeline, Transform } from 'stream';
import url from 'url';
import zlib from 'zlib';
import type { HTTPCredentials } from '../../types/types';
import { TimeoutSettings } from '../common/timeoutSettings';
Expand Down Expand Up @@ -499,12 +500,12 @@ export abstract class APIRequestContext extends SdkObject {
// happy eyeballs don't emit lookup and connect events, so we use our custom ones
const happyEyeBallsTimings = timingForSocket(socket);
dnsLookupAt = happyEyeBallsTimings.dnsLookupAt;
tcpConnectionAt ??= happyEyeBallsTimings.tcpConnectionAt;
tcpConnectionAt = happyEyeBallsTimings.tcpConnectionAt;

// non-happy-eyeballs sockets
listeners.push(
eventsHelper.addEventListener(socket, 'lookup', () => { dnsLookupAt = monotonicTime(); }),
eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt ??= monotonicTime(); }),
eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt = monotonicTime(); }),
eventsHelper.addEventListener(socket, 'secureConnect', () => {
tlsHandshakeAt = monotonicTime();

Expand All @@ -521,21 +522,11 @@ export abstract class APIRequestContext extends SdkObject {
}),
);

// when using socks proxy, having the socket means the connection got established
if (agent instanceof SocksProxyAgent)
tcpConnectionAt ??= monotonicTime();

serverIPAddress = socket.remoteAddress;
serverPort = socket.remotePort;
});
request.on('finish', () => { requestFinishAt = monotonicTime(); });

// http proxy
request.on('proxyConnect', () => {
tcpConnectionAt ??= monotonicTime();
});


progress.log(`→ ${options.method} ${url.toString()}`);
if (options.headers) {
for (const [name, value] of Object.entries(options.headers))
Expand Down Expand Up @@ -702,16 +693,17 @@ export class GlobalAPIRequestContext extends APIRequestContext {
}

export function createProxyAgent(proxy: types.ProxySettings) {
const proxyURL = new URL(proxy.server);
if (proxyURL.protocol?.startsWith('socks'))
return new SocksProxyAgent(proxyURL);

const proxyOpts = url.parse(proxy.server);
if (proxyOpts.protocol?.startsWith('socks')) {
return new SocksProxyAgent({
host: proxyOpts.hostname,
port: proxyOpts.port || undefined,
});
}
if (proxy.username)
proxyURL.username = proxy.username;
if (proxy.password)
proxyURL.password = proxy.password;
// TODO: We should use HttpProxyAgent conditional on proxyURL.protocol instead of always using CONNECT method.
return new HttpsProxyAgent(proxyURL);
proxyOpts.auth = `${proxy.username}:${proxy.password || ''}`;
// TODO: We should use HttpProxyAgent conditional on proxyOpts.protocol instead of always using CONNECT method.
return new HttpsProxyAgent(proxyOpts);
}

function toHeadersArray(rawHeaders: string[]): types.HeadersArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class SocksProxyConnection {

async connect() {
if (this.socksProxy.proxyAgentFromOptions)
this.target = await this.socksProxy.proxyAgentFromOptions.connect(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false });
this.target = await this.socksProxy.proxyAgentFromOptions.callback(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false });
else
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);

Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/utils/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function httpRequest(params: HTTPRequestParams, onResponse: (r: http.Inco

const proxyURL = getProxyForUrl(params.url);
if (proxyURL) {
const parsedProxyURL = new URL(proxyURL);
const parsedProxyURL = url.parse(proxyURL);
if (params.url.startsWith('http:')) {
options = {
path: parsedUrl.href,
Expand Down
2 changes: 1 addition & 1 deletion tests/config/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export async function setupSocksForwardingServer({
const socksProxy = new SocksProxy();
socksProxy.setPattern('*');
socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => {
if (!['127.0.0.1', '0:0:0:0:0:0:0:1', 'fake-localhost-127-0-0-1.nip.io', 'localhost'].includes(payload.host) || payload.port !== allowedTargetPort) {
if (!['127.0.0.1', 'fake-localhost-127-0-0-1.nip.io', 'localhost'].includes(payload.host) || payload.port !== allowedTargetPort) {
socksProxy.sendSocketError({ uid: payload.uid, error: 'ECONNREFUSED' });
return;
}
Expand Down
1 change: 0 additions & 1 deletion tests/library/browsertype-connect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -844,7 +844,6 @@ for (const kind of ['launchServer', 'run-server'] as const) {
});

test('should proxy requests from fetch api', async ({ startRemoteServer, server, browserName, connect, channel, platform, dummyServerPort }, workerInfo) => {
test.fixme(true, 'broken because of socks proxy agent error: Socks5 proxy rejected connection - ConnectionRefused');
test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying');

let reachedOriginalTarget = false;
Expand Down
2 changes: 1 addition & 1 deletion tests/library/client-certificates.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ test.describe('browser', () => {
});
expect(connectHosts).toEqual([]);
await page.goto(serverURL);
const host = browserName === 'webkit' && isMac ? '0:0:0:0:0:0:0:1' : '127.0.0.1';
const host = browserName === 'webkit' && isMac ? 'localhost' : '127.0.0.1';
expect(connectHosts).toEqual([`${host}:${serverPort}`]);
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
await page.close();
Expand Down
36 changes: 2 additions & 34 deletions tests/library/har.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import type { Log } from '../../packages/trace/src/har';
import { parseHar } from '../config/utils';
const { createHttp2Server } = require('../../packages/playwright-core/lib/utils');

async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, testInfo: any, options: { outputPath?: string, proxy?: BrowserContextOptions['proxy'] } & Partial<Pick<BrowserContextOptions['recordHar'], 'content' | 'omitContent' | 'mode'>> = {}) {
async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, testInfo: any, options: { outputPath?: string } & Partial<Pick<BrowserContextOptions['recordHar'], 'content' | 'omitContent' | 'mode'>> = {}) {
const harPath = testInfo.outputPath(options.outputPath || 'test.har');
const context = await contextFactory({ recordHar: { path: harPath, ...options }, ignoreHTTPSErrors: true, proxy: options.proxy });
const context = await contextFactory({ recordHar: { path: harPath, ...options }, ignoreHTTPSErrors: true });
const page = await context.newPage();
return {
page,
Expand Down Expand Up @@ -861,38 +861,6 @@ it('should respect minimal mode for API Requests', async ({ contextFactory, serv
expect(entry.response.bodySize).toBe(-1);
});

it('should include timings when using http proxy', async ({ contextFactory, server, proxyServer }, testInfo) => {
proxyServer.forwardTo(server.PORT, { allowConnectRequests: true });
const { page, getLog } = await pageWithHar(contextFactory, testInfo, { proxy: { server: `localhost:${proxyServer.PORT}` } });
const response = await page.request.get(server.EMPTY_PAGE);
expect(proxyServer.connectHosts).toEqual([`localhost:${server.PORT}`]);
await expect(response).toBeOK();
const log = await getLog();
expect(log.entries[0].timings.connect).toBeGreaterThan(0);
});

it('should include timings when using socks proxy', async ({ contextFactory, server, socksPort }, testInfo) => {
const { page, getLog } = await pageWithHar(contextFactory, testInfo, { proxy: { server: `socks5://localhost:${socksPort}` } });
const response = await page.request.get(server.EMPTY_PAGE);
expect(await response.text()).toContain('Served by the SOCKS proxy');
await expect(response).toBeOK();
const log = await getLog();
expect(log.entries[0].timings.connect).toBeGreaterThan(0);
});

it('should not have connect and dns timings when socket is reused', async ({ contextFactory, server }, testInfo) => {
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
await page.request.get(server.EMPTY_PAGE);
await page.request.get(server.EMPTY_PAGE);

const log = await getLog();
expect(log.entries).toHaveLength(2);
const request2 = log.entries[1];
expect.soft(request2.timings.connect).toBe(-1);
expect.soft(request2.timings.dns).toBe(-1);
expect.soft(request2.timings.blocked).toBeGreaterThan(0);
});

it('should include redirects from API request', async ({ contextFactory, server }, testInfo) => {
server.setRedirect('/redirect-me', '/simple.json');
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
Expand Down
Loading