mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(har): record remote IP:PORT and SSL details (#6631)
This commit is contained in:
parent
742cce3a1d
commit
195eab8787
@ -296,7 +296,22 @@ export class CRNetworkManager {
|
|||||||
responseStart: -1,
|
responseStart: -1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), timing, getResponseBody);
|
const response = new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), timing, getResponseBody);
|
||||||
|
if (responsePayload?.remoteIPAddress && typeof responsePayload?.remotePort === 'number')
|
||||||
|
response._serverAddrFinished({
|
||||||
|
ipAddress: responsePayload.remoteIPAddress,
|
||||||
|
port: responsePayload.remotePort,
|
||||||
|
});
|
||||||
|
else
|
||||||
|
response._serverAddrFinished();
|
||||||
|
response._securityDetailsFinished({
|
||||||
|
protocol: responsePayload?.securityDetails?.protocol,
|
||||||
|
subjectName: responsePayload?.securityDetails?.subjectName,
|
||||||
|
issuer: responsePayload?.securityDetails?.issuer,
|
||||||
|
validFrom: responsePayload?.securityDetails?.validFrom,
|
||||||
|
validTo: responsePayload?.securityDetails?.validTo,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response, timestamp: number) {
|
_handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response, timestamp: number) {
|
||||||
|
@ -89,6 +89,20 @@ export class FFNetworkManager {
|
|||||||
responseStart: this._relativeTiming(event.timing.responseStart),
|
responseStart: this._relativeTiming(event.timing.responseStart),
|
||||||
};
|
};
|
||||||
const response = new network.Response(request.request, event.status, event.statusText, event.headers, timing, getResponseBody);
|
const response = new network.Response(request.request, event.status, event.statusText, event.headers, timing, getResponseBody);
|
||||||
|
if (event?.remoteIPAddress && typeof event?.remotePort === 'number')
|
||||||
|
response._serverAddrFinished({
|
||||||
|
ipAddress: event.remoteIPAddress,
|
||||||
|
port: event.remotePort,
|
||||||
|
});
|
||||||
|
else
|
||||||
|
response._serverAddrFinished()
|
||||||
|
response._securityDetailsFinished({
|
||||||
|
protocol: event?.securityDetails?.protocol,
|
||||||
|
subjectName: event?.securityDetails?.subjectName,
|
||||||
|
issuer: event?.securityDetails?.issuer,
|
||||||
|
validFrom: event?.securityDetails?.validFrom,
|
||||||
|
validTo: event?.securityDetails?.validTo,
|
||||||
|
});
|
||||||
this._page._frameManager.requestReceivedResponse(response);
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,6 +263,19 @@ export type ResourceTiming = {
|
|||||||
responseStart: number;
|
responseStart: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RemoteAddr = {
|
||||||
|
ipAddress: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SecurityDetails = {
|
||||||
|
protocol?: string;
|
||||||
|
subjectName?: string;
|
||||||
|
issuer?: string;
|
||||||
|
validFrom?: number;
|
||||||
|
validTo?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class Response extends SdkObject {
|
export class Response extends SdkObject {
|
||||||
private _request: Request;
|
private _request: Request;
|
||||||
private _contentPromise: Promise<Buffer> | null = null;
|
private _contentPromise: Promise<Buffer> | null = null;
|
||||||
@ -275,6 +288,10 @@ export class Response extends SdkObject {
|
|||||||
private _headersMap = new Map<string, string>();
|
private _headersMap = new Map<string, string>();
|
||||||
private _getResponseBodyCallback: GetResponseBodyCallback;
|
private _getResponseBodyCallback: GetResponseBodyCallback;
|
||||||
private _timing: ResourceTiming;
|
private _timing: ResourceTiming;
|
||||||
|
private _serverAddrPromise: Promise<RemoteAddr|undefined>;
|
||||||
|
private _serverAddrPromiseCallback: (arg?: RemoteAddr) => void = () => {};
|
||||||
|
private _securityDetailsPromise: Promise<SecurityDetails|undefined>;
|
||||||
|
private _securityDetailsPromiseCallback: (arg?: SecurityDetails) => void = () => {};
|
||||||
|
|
||||||
constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback) {
|
constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback) {
|
||||||
super(request.frame(), 'response');
|
super(request.frame(), 'response');
|
||||||
@ -287,12 +304,26 @@ export class Response extends SdkObject {
|
|||||||
for (const { name, value } of this._headers)
|
for (const { name, value } of this._headers)
|
||||||
this._headersMap.set(name.toLowerCase(), value);
|
this._headersMap.set(name.toLowerCase(), value);
|
||||||
this._getResponseBodyCallback = getResponseBodyCallback;
|
this._getResponseBodyCallback = getResponseBodyCallback;
|
||||||
|
this._serverAddrPromise = new Promise(f => {
|
||||||
|
this._serverAddrPromiseCallback = f;
|
||||||
|
});
|
||||||
|
this._securityDetailsPromise = new Promise(f => {
|
||||||
|
this._securityDetailsPromiseCallback = f;
|
||||||
|
});
|
||||||
this._finishedPromise = new Promise(f => {
|
this._finishedPromise = new Promise(f => {
|
||||||
this._finishedPromiseCallback = f;
|
this._finishedPromiseCallback = f;
|
||||||
});
|
});
|
||||||
this._request._setResponse(this);
|
this._request._setResponse(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_serverAddrFinished(addr?: RemoteAddr) {
|
||||||
|
this._serverAddrPromiseCallback(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
_securityDetailsFinished(securityDetails?: SecurityDetails) {
|
||||||
|
this._securityDetailsPromiseCallback(securityDetails);
|
||||||
|
}
|
||||||
|
|
||||||
_requestFinished(responseEndTiming: number, error?: string) {
|
_requestFinished(responseEndTiming: number, error?: string) {
|
||||||
this._request._responseEndTiming = Math.max(responseEndTiming, this._timing.responseStart);
|
this._request._responseEndTiming = Math.max(responseEndTiming, this._timing.responseStart);
|
||||||
this._finishedPromiseCallback({ error });
|
this._finishedPromiseCallback({ error });
|
||||||
@ -326,6 +357,14 @@ export class Response extends SdkObject {
|
|||||||
return this._timing;
|
return this._timing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async serverAddr(): Promise<RemoteAddr|null> {
|
||||||
|
return await this._serverAddrPromise || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async securityDetails(): Promise<SecurityDetails|null> {
|
||||||
|
return await this._securityDetailsPromise || null;
|
||||||
|
}
|
||||||
|
|
||||||
body(): Promise<Buffer> {
|
body(): Promise<Buffer> {
|
||||||
if (!this._contentPromise) {
|
if (!this._contentPromise) {
|
||||||
this._contentPromise = this._finishedPromise.then(async ({ error }) => {
|
this._contentPromise = this._finishedPromise.then(async ({ error }) => {
|
||||||
|
@ -55,6 +55,8 @@ export type Entry = {
|
|||||||
timings: Timings;
|
timings: Timings;
|
||||||
serverIPAddress?: string;
|
serverIPAddress?: string;
|
||||||
connection?: string;
|
connection?: string;
|
||||||
|
_serverPort?: number;
|
||||||
|
_securityDetails?: SecurityDetails;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Request = {
|
export type Request = {
|
||||||
@ -144,3 +146,11 @@ export type Timings = {
|
|||||||
receive: number;
|
receive: number;
|
||||||
ssl?: number;
|
ssl?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SecurityDetails = {
|
||||||
|
protocol?: string;
|
||||||
|
subjectName?: string;
|
||||||
|
issuer?: string;
|
||||||
|
validFrom?: number;
|
||||||
|
validTo?: number;
|
||||||
|
};
|
||||||
|
@ -209,6 +209,18 @@ export class HarTracer {
|
|||||||
receive,
|
receive,
|
||||||
};
|
};
|
||||||
harEntry.time = [dns, connect, ssl, wait, receive].reduce((pre, cur) => cur > 0 ? cur + pre : pre, 0);
|
harEntry.time = [dns, connect, ssl, wait, receive].reduce((pre, cur) => cur > 0 ? cur + pre : pre, 0);
|
||||||
|
|
||||||
|
this._addBarrier(page, response.serverAddr().then(server => {
|
||||||
|
if (server?.ipAddress)
|
||||||
|
harEntry.serverIPAddress = server.ipAddress;
|
||||||
|
if (server?.port)
|
||||||
|
harEntry._serverPort = server.port;
|
||||||
|
}));
|
||||||
|
this._addBarrier(page, response.securityDetails().then(details => {
|
||||||
|
if (details)
|
||||||
|
harEntry._securityDetails = details;
|
||||||
|
}));
|
||||||
|
|
||||||
if (!this._options.omitContent && response.status() === 200) {
|
if (!this._options.omitContent && response.status() === 200) {
|
||||||
const promise = response.body().then(buffer => {
|
const promise = response.body().then(buffer => {
|
||||||
harEntry.response.content.text = buffer.toString('base64');
|
harEntry.response.content.text = buffer.toString('base64');
|
||||||
|
@ -69,6 +69,7 @@ export class WKPage implements PageDelegate {
|
|||||||
_firstNonInitialNavigationCommittedReject = (e: Error) => {};
|
_firstNonInitialNavigationCommittedReject = (e: Error) => {};
|
||||||
private _lastConsoleMessage: { derivedType: string, text: string, handles: JSHandle[]; count: number, location: types.ConsoleMessageLocation; } | null = null;
|
private _lastConsoleMessage: { derivedType: string, text: string, handles: JSHandle[]; count: number, location: types.ConsoleMessageLocation; } | null = null;
|
||||||
|
|
||||||
|
private readonly _requestIdToResponseReceivedPayloadEvent = new Map<string, Protocol.Network.responseReceivedPayload>();
|
||||||
// Holds window features for the next popup being opened via window.open,
|
// Holds window features for the next popup being opened via window.open,
|
||||||
// until the popup page proxy arrives.
|
// until the popup page proxy arrives.
|
||||||
private _nextWindowOpenPopupFeatures?: string[];
|
private _nextWindowOpenPopupFeatures?: string[];
|
||||||
@ -953,6 +954,8 @@ export class WKPage implements PageDelegate {
|
|||||||
|
|
||||||
private _handleRequestRedirect(request: WKInterceptableRequest, responsePayload: Protocol.Network.Response, timestamp: number) {
|
private _handleRequestRedirect(request: WKInterceptableRequest, responsePayload: Protocol.Network.Response, timestamp: number) {
|
||||||
const response = request.createResponse(responsePayload);
|
const response = request.createResponse(responsePayload);
|
||||||
|
response._securityDetailsFinished();
|
||||||
|
response._serverAddrFinished();
|
||||||
response._requestFinished(responsePayload.timing ? helper.secondsToRoundishMillis(timestamp - request._timestamp) : -1, 'Response body is unavailable for redirect responses');
|
response._requestFinished(responsePayload.timing ? helper.secondsToRoundishMillis(timestamp - request._timestamp) : -1, 'Response body is unavailable for redirect responses');
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._page._frameManager.requestReceivedResponse(response);
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
@ -979,6 +982,7 @@ export class WKPage implements PageDelegate {
|
|||||||
// FileUpload sends a response without a matching request.
|
// FileUpload sends a response without a matching request.
|
||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
|
this._requestIdToResponseReceivedPayloadEvent.set(request._requestId, event);
|
||||||
const response = request.createResponse(event.response);
|
const response = request.createResponse(event.response);
|
||||||
if (event.response.requestHeaders && Object.keys(event.response.requestHeaders).length)
|
if (event.response.requestHeaders && Object.keys(event.response.requestHeaders).length)
|
||||||
request.request.updateWithRawHeaders(headersObjectToArray(event.response.requestHeaders));
|
request.request.updateWithRawHeaders(headersObjectToArray(event.response.requestHeaders));
|
||||||
@ -1003,8 +1007,19 @@ export class WKPage implements PageDelegate {
|
|||||||
// Under certain conditions we never get the Network.responseReceived
|
// Under certain conditions we never get the Network.responseReceived
|
||||||
// event from protocol. @see https://crbug.com/883475
|
// event from protocol. @see https://crbug.com/883475
|
||||||
const response = request.request._existingResponse();
|
const response = request.request._existingResponse();
|
||||||
if (response)
|
if (response) {
|
||||||
|
const responseReceivedPayload = this._requestIdToResponseReceivedPayloadEvent.get(request._requestId);
|
||||||
|
response._serverAddrFinished(parseRemoteAddress(event?.metrics?.remoteAddress));
|
||||||
|
response._securityDetailsFinished({
|
||||||
|
protocol: isLoadedSecurely(response.url(), response.timing()) ? event.metrics?.securityConnection?.protocol : undefined,
|
||||||
|
subjectName: responseReceivedPayload?.response.security?.certificate?.subject,
|
||||||
|
validFrom: responseReceivedPayload?.response.security?.certificate?.validFrom,
|
||||||
|
validTo: responseReceivedPayload?.response.security?.certificate?.validUntil,
|
||||||
|
});
|
||||||
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
|
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
this._requestIdToResponseReceivedPayloadEvent.delete(request._requestId);
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._page._frameManager.requestFinished(request.request);
|
this._page._frameManager.requestFinished(request.request);
|
||||||
}
|
}
|
||||||
@ -1016,8 +1031,11 @@ export class WKPage implements PageDelegate {
|
|||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
const response = request.request._existingResponse();
|
const response = request.request._existingResponse();
|
||||||
if (response)
|
if (response) {
|
||||||
|
response._serverAddrFinished();
|
||||||
|
response._securityDetailsFinished();
|
||||||
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
|
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
|
||||||
|
}
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
request.request._setFailureText(event.errorText);
|
request.request._setFailureText(event.errorText);
|
||||||
this._page._frameManager.requestFailed(request.request, event.errorText.includes('cancelled'));
|
this._page._frameManager.requestFailed(request.request, event.errorText.includes('cancelled'));
|
||||||
@ -1047,3 +1065,63 @@ function webkitWorldName(world: types.World) {
|
|||||||
case 'utility': return UTILITY_WORLD_NAME;
|
case 'utility': return UTILITY_WORLD_NAME;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebKit Remote Addresses look like:
|
||||||
|
*
|
||||||
|
* macOS:
|
||||||
|
* ::1.8911
|
||||||
|
* 2606:2800:220:1:248:1893:25c8:1946.443
|
||||||
|
* 127.0.0.1:8000
|
||||||
|
*
|
||||||
|
* ubuntu:
|
||||||
|
* ::1:8907
|
||||||
|
* 127.0.0.1:8000
|
||||||
|
*
|
||||||
|
* NB: They look IPv4 and IPv6's with ports but use an alternative notation.
|
||||||
|
*/
|
||||||
|
function parseRemoteAddress(value?: string) {
|
||||||
|
if (!value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const colon = value.lastIndexOf(':');
|
||||||
|
const dot = value.lastIndexOf('.');
|
||||||
|
if (dot < 0) { // IPv6ish:port
|
||||||
|
return {
|
||||||
|
ipAddress: `[${value.slice(0, colon)}]`,
|
||||||
|
port: +value.slice(colon + 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colon > dot) { // IPv4:port
|
||||||
|
const [address, port] = value.split(':');
|
||||||
|
return {
|
||||||
|
ipAddress: address,
|
||||||
|
port: +port,
|
||||||
|
};
|
||||||
|
} else { // IPv6ish.port
|
||||||
|
const [address, port] = value.split('.');
|
||||||
|
return {
|
||||||
|
ipAddress: `[${address}]`,
|
||||||
|
port: +port,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapted from Source/WebInspectorUI/UserInterface/Models/Resource.js in
|
||||||
|
* WebKit codebase.
|
||||||
|
*/
|
||||||
|
function isLoadedSecurely(url: string, timing: network.ResourceTiming) {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
if (u.protocol !== 'https:' && u.protocol !== 'wss:' && u.protocol !== 'sftp:')
|
||||||
|
return false;
|
||||||
|
if (timing.secureConnectionStart === -1 && timing.connectStart !== -1)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
@ -301,3 +301,65 @@ it('should not contain internal pages', async ({ browserName, contextFactory, se
|
|||||||
const log = await getLog();
|
const log = await getLog();
|
||||||
expect(log.pages.length).toBe(1);
|
expect(log.pages.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have connection details', async ({ contextFactory, server, browserName, platform }, testInfo) => {
|
||||||
|
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
const log = await getLog();
|
||||||
|
const { serverIPAddress, _serverPort: port, _securityDetails: securityDetails } = log.entries[0];
|
||||||
|
expect(serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/);
|
||||||
|
expect(port).toBe(server.PORT);
|
||||||
|
expect(securityDetails).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have security details', async ({ contextFactory, httpsServer, browserName, platform }, testInfo) => {
|
||||||
|
it.fail(browserName === 'webkit' && platform === 'linux', 'https://github.com/microsoft/playwright/issues/6759');
|
||||||
|
|
||||||
|
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
||||||
|
await page.goto(httpsServer.EMPTY_PAGE);
|
||||||
|
const log = await getLog();
|
||||||
|
const { serverIPAddress, _serverPort: port, _securityDetails: securityDetails } = log.entries[0];
|
||||||
|
expect(serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/);
|
||||||
|
expect(port).toBe(httpsServer.PORT);
|
||||||
|
if (browserName === 'webkit' && platform === 'win32')
|
||||||
|
expect(securityDetails).toEqual({subjectName: 'puppeteer-tests', validFrom: 1550084863, validTo: -1});
|
||||||
|
else if (browserName === 'webkit')
|
||||||
|
expect(securityDetails).toEqual({protocol: 'TLS 1.3', subjectName: 'puppeteer-tests', validFrom: 1550084863, validTo: 33086084863});
|
||||||
|
else
|
||||||
|
expect(securityDetails).toEqual({issuer: 'puppeteer-tests', protocol: 'TLS 1.3', subjectName: 'puppeteer-tests', validFrom: 1550084863, validTo: 33086084863});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have connection details for redirects', async ({ contextFactory, server, browserName }, testInfo) => {
|
||||||
|
server.setRedirect('/foo.html', '/empty.html');
|
||||||
|
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
||||||
|
await page.goto(server.PREFIX + '/foo.html');
|
||||||
|
const log = await getLog();
|
||||||
|
expect(log.entries.length).toBe(2);
|
||||||
|
|
||||||
|
const detailsFoo = log.entries[0];
|
||||||
|
|
||||||
|
if (browserName === 'webkit') {
|
||||||
|
expect(detailsFoo.serverIPAddress).toBeUndefined();
|
||||||
|
expect(detailsFoo._serverPort).toBeUndefined();
|
||||||
|
} else {
|
||||||
|
expect(detailsFoo.serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/);
|
||||||
|
expect(detailsFoo._serverPort).toBe(server.PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailsEmpty = log.entries[1];
|
||||||
|
expect(detailsEmpty.serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/);
|
||||||
|
expect(detailsEmpty._serverPort).toBe(server.PORT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have connection details for failed requests', async ({ contextFactory, server, browserName, platform }, testInfo) => {
|
||||||
|
server.setRoute('/one-style.css', (_, res) => {
|
||||||
|
res.setHeader('Content-Type', 'text/css');
|
||||||
|
res.connection.destroy();
|
||||||
|
});
|
||||||
|
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
||||||
|
await page.goto(server.PREFIX + '/one-style.html');
|
||||||
|
const log = await getLog();
|
||||||
|
const { serverIPAddress, _serverPort: port } = log.entries[0];
|
||||||
|
expect(serverIPAddress).toMatch(/^127\.0\.0\.1|\[::1\]/);
|
||||||
|
expect(port).toBe(server.PORT);
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user