mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
browser(firefox): support response interception (#7509)
This commit is contained in:
parent
70b054d240
commit
efb21b9e9f
@ -1,2 +1,2 @@
|
|||||||
1274
|
1275
|
||||||
Changed: dgozman@gmail.com Tue Jun 30 16:15:40 PDT 2021
|
Changed: yurys@chromium.org Thu Jul 8 13:25:54 MSK 2021
|
||||||
|
@ -73,8 +73,15 @@ class PageNetwork {
|
|||||||
this._interceptedRequests.clear();
|
this._interceptedRequests.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeInterceptedRequest(requestId, url, method, headers, postData) {
|
async resumeInterceptedRequest(requestId, url, method, headers, postData, interceptResponse) {
|
||||||
|
if (interceptResponse) {
|
||||||
|
const intercepted = this._interceptedRequests.get(requestId);
|
||||||
|
if (!intercepted)
|
||||||
|
throw new Error(`Cannot find request "${requestId}"`);
|
||||||
|
return { response: await intercepted.interceptResponse(url, method, headers, postData) };
|
||||||
|
}
|
||||||
this._takeIntercepted(requestId).resume(url, method, headers, postData);
|
this._takeIntercepted(requestId).resume(url, method, headers, postData);
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
fulfillInterceptedRequest(requestId, status, statusText, headers, base64body) {
|
fulfillInterceptedRequest(requestId, status, statusText, headers, base64body) {
|
||||||
@ -180,6 +187,64 @@ class NetworkRequest {
|
|||||||
this._interceptedChannel = undefined;
|
this._interceptedChannel = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async interceptResponse(url, method, headers, postData) {
|
||||||
|
const uri = url ? Services.io.newURI(url) : this.httpChannel.URI;
|
||||||
|
const newChannel = NetUtil.newChannel({
|
||||||
|
uri,
|
||||||
|
loadingNode: this.httpChannel.loadInfo.loadingContext,
|
||||||
|
loadingPrincipal: this.httpChannel.loadInfo.loadingPrincipal || this._interceptedChannel.loadInfo.principalToInherit,
|
||||||
|
triggeringPrincipal: this.httpChannel.loadInfo.triggeringPrincipal,
|
||||||
|
securityFlags: this.httpChannel.loadInfo.securityFlags,
|
||||||
|
contentPolicyType: this.httpChannel.loadInfo.internalContentPolicyType,
|
||||||
|
}).QueryInterface(Ci.nsIRequest).QueryInterface(Ci.nsIHttpChannel);
|
||||||
|
newChannel.loadInfo = this.httpChannel.loadInfo;
|
||||||
|
newChannel.loadGroup = this.httpChannel.loadGroup;
|
||||||
|
|
||||||
|
for (const header of (headers || requestHeaders(this.httpChannel)))
|
||||||
|
newChannel.setRequestHeader(header.name, header.value, false /* merge */);
|
||||||
|
|
||||||
|
if (postData) {
|
||||||
|
setPostData(newChannel, postData, headers);
|
||||||
|
} else if (this.httpChannel instanceof Ci.nsIUploadChannel) {
|
||||||
|
newChannel.QueryInterface(Ci.nsIUploadChannel);
|
||||||
|
newChannel.setUploadStream(this.httpChannel.uploadStream, '', -1);
|
||||||
|
}
|
||||||
|
// We must set this after setting the upload stream, otherwise it
|
||||||
|
// will always be 'PUT'. (from another place in the source base)
|
||||||
|
newChannel.requestMethod = method || this.httpChannel.requestMethod;
|
||||||
|
|
||||||
|
this._networkObserver._responseInterceptionChannels.add(newChannel);
|
||||||
|
const body = await new Promise((resolve, reject) => {
|
||||||
|
NetUtil.asyncFetch(newChannel, (stream, status) => {
|
||||||
|
this._networkObserver._responseInterceptionChannels.delete(newChannel);
|
||||||
|
if (!Components.isSuccessCode(status)) {
|
||||||
|
reject(status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resolve(NetUtil.readInputStreamToString(stream, stream.available()));
|
||||||
|
} catch (e) {
|
||||||
|
if (e.result == Cr.NS_BASE_STREAM_CLOSED) {
|
||||||
|
// The stream was empty.
|
||||||
|
resolve('');
|
||||||
|
} else {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
stream.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageNetwork = this._activePageNetwork();
|
||||||
|
if (pageNetwork)
|
||||||
|
pageNetwork._responseStorage.addResponseBody(this, newChannel, body);
|
||||||
|
|
||||||
|
const response = responseHead(newChannel);
|
||||||
|
this._interceptedResponse = Object.assign({ body }, response);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
// Public interception API.
|
// Public interception API.
|
||||||
abort(errorCode) {
|
abort(errorCode) {
|
||||||
const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE;
|
const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE;
|
||||||
@ -189,6 +254,13 @@ class NetworkRequest {
|
|||||||
|
|
||||||
// Public interception API.
|
// Public interception API.
|
||||||
fulfill(status, statusText, headers, base64body) {
|
fulfill(status, statusText, headers, base64body) {
|
||||||
|
let body = base64body ? atob(base64body) : '';
|
||||||
|
if (this._interceptedResponse) {
|
||||||
|
status = status || this._interceptedResponse.status;
|
||||||
|
statusText = statusText || this._interceptedResponse.statusText;
|
||||||
|
headers = headers || this._interceptedResponse.headers;
|
||||||
|
body = body || this._interceptedResponse.body;
|
||||||
|
}
|
||||||
this._interceptedChannel.synthesizeStatus(status, statusText);
|
this._interceptedChannel.synthesizeStatus(status, statusText);
|
||||||
for (const header of headers) {
|
for (const header of headers) {
|
||||||
this._interceptedChannel.synthesizeHeader(header.name, header.value);
|
this._interceptedChannel.synthesizeHeader(header.name, header.value);
|
||||||
@ -198,7 +270,6 @@ class NetworkRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
||||||
const body = base64body ? atob(base64body) : '';
|
|
||||||
synthesized.data = body;
|
synthesized.data = body;
|
||||||
this._interceptedChannel.startSynthesizedResponse(synthesized, null, null, '', false);
|
this._interceptedChannel.startSynthesizedResponse(synthesized, null, null, '', false);
|
||||||
this._interceptedChannel.finishSynthesizedResponse();
|
this._interceptedChannel.finishSynthesizedResponse();
|
||||||
@ -233,25 +304,8 @@ class NetworkRequest {
|
|||||||
}
|
}
|
||||||
if (method)
|
if (method)
|
||||||
this.httpChannel.requestMethod = method;
|
this.httpChannel.requestMethod = method;
|
||||||
if (postData && this.httpChannel instanceof Ci.nsIUploadChannel2) {
|
if (postData)
|
||||||
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
setPostData(this.httpChannel, postData, headers);
|
||||||
const body = atob(postData);
|
|
||||||
synthesized.setData(body, body.length);
|
|
||||||
|
|
||||||
const overriddenHeader = (lowerCaseName, defaultValue) => {
|
|
||||||
if (headers) {
|
|
||||||
for (const header of headers) {
|
|
||||||
if (header.name.toLowerCase() === lowerCaseName) {
|
|
||||||
return header.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
// Clear content-length, so that upload stream resets it.
|
|
||||||
this.httpChannel.setRequestHeader('content-length', overriddenHeader('content-length', ''), false /* merge */);
|
|
||||||
this.httpChannel.explicitSetUploadStream(synthesized, overriddenHeader('content-type', 'application/octet-stream'), -1, this.httpChannel.requestMethod, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instrumentation called by NetworkObserver.
|
// Instrumentation called by NetworkObserver.
|
||||||
@ -437,7 +491,7 @@ class NetworkRequest {
|
|||||||
const body = this._responseBodyChunks.join('');
|
const body = this._responseBodyChunks.join('');
|
||||||
const pageNetwork = this._activePageNetwork();
|
const pageNetwork = this._activePageNetwork();
|
||||||
if (pageNetwork)
|
if (pageNetwork)
|
||||||
pageNetwork._responseStorage.addResponseBody(this, body);
|
pageNetwork._responseStorage.addResponseBody(this, this.httpChannel, body);
|
||||||
this._sendOnRequestFinished();
|
this._sendOnRequestFinished();
|
||||||
} else {
|
} else {
|
||||||
this._sendOnRequestFailed(aStatusCode);
|
this._sendOnRequestFailed(aStatusCode);
|
||||||
@ -529,20 +583,7 @@ class NetworkRequest {
|
|||||||
responseStart: this.httpChannel.responseStartTime,
|
responseStart: this.httpChannel.responseStartTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
const headers = [];
|
const { status, statusText, headers } = responseHead(this.httpChannel, opt_statusCode, opt_statusText);
|
||||||
let status = opt_statusCode || 0;
|
|
||||||
let statusText = opt_statusText || '';
|
|
||||||
try {
|
|
||||||
status = this.httpChannel.responseStatus;
|
|
||||||
statusText = this.httpChannel.responseStatusText;
|
|
||||||
this.httpChannel.visitResponseHeaders({
|
|
||||||
visitHeader: (name, value) => headers.push({name, value}),
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// Response headers, status and/or statusText are not available
|
|
||||||
// when redirect did not actually hit the network.
|
|
||||||
}
|
|
||||||
|
|
||||||
let remoteIPAddress = undefined;
|
let remoteIPAddress = undefined;
|
||||||
let remotePort = undefined;
|
let remotePort = undefined;
|
||||||
try {
|
try {
|
||||||
@ -602,6 +643,7 @@ class NetworkObserver {
|
|||||||
|
|
||||||
this._channelToRequest = new Map(); // http channel -> network request
|
this._channelToRequest = new Map(); // http channel -> network request
|
||||||
this._expectedRedirect = new Map(); // expected redirect channel id (string) -> network request
|
this._expectedRedirect = new Map(); // expected redirect channel id (string) -> network request
|
||||||
|
this._responseInterceptionChannels = new Set(); // http channels created for response interception
|
||||||
|
|
||||||
const protocolProxyService = Cc['@mozilla.org/network/protocol-proxy-service;1'].getService();
|
const protocolProxyService = Cc['@mozilla.org/network/protocol-proxy-service;1'].getService();
|
||||||
this._channelProxyFilter = {
|
this._channelProxyFilter = {
|
||||||
@ -684,6 +726,8 @@ class NetworkObserver {
|
|||||||
_onRequest(channel, topic) {
|
_onRequest(channel, topic) {
|
||||||
if (!(channel instanceof Ci.nsIHttpChannel))
|
if (!(channel instanceof Ci.nsIHttpChannel))
|
||||||
return;
|
return;
|
||||||
|
if (this._responseInterceptionChannels.has(channel))
|
||||||
|
return;
|
||||||
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
|
const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
|
||||||
const channelId = httpChannel.channelId + '';
|
const channelId = httpChannel.channelId + '';
|
||||||
const redirectedFrom = this._expectedRedirect.get(channelId);
|
const redirectedFrom = this._expectedRedirect.get(channelId);
|
||||||
@ -810,7 +854,7 @@ class ResponseStorage {
|
|||||||
this._responses = new Map();
|
this._responses = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
addResponseBody(request, body) {
|
addResponseBody(request, httpChannel, body) {
|
||||||
if (body.length > this._maxResponseSize) {
|
if (body.length > this._maxResponseSize) {
|
||||||
this._responses.set(request.requestId, {
|
this._responses.set(request.requestId, {
|
||||||
evicted: true,
|
evicted: true,
|
||||||
@ -819,8 +863,8 @@ class ResponseStorage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let encodings = [];
|
let encodings = [];
|
||||||
if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion) {
|
if ((httpChannel instanceof Ci.nsIEncodedChannel) && httpChannel.contentEncodings && !httpChannel.applyConversion) {
|
||||||
const encodingHeader = request.httpChannel.getResponseHeader("Content-Encoding");
|
const encodingHeader = httpChannel.getResponseHeader("Content-Encoding");
|
||||||
encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
|
encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
|
||||||
}
|
}
|
||||||
this._responses.set(request.requestId, {body, encodings});
|
this._responses.set(request.requestId, {body, encodings});
|
||||||
@ -851,6 +895,45 @@ class ResponseStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function responseHead(httpChannel, opt_statusCode, opt_statusText) {
|
||||||
|
const headers = [];
|
||||||
|
let status = opt_statusCode || 0;
|
||||||
|
let statusText = opt_statusText || '';
|
||||||
|
try {
|
||||||
|
status = httpChannel.responseStatus;
|
||||||
|
statusText = httpChannel.responseStatusText;
|
||||||
|
httpChannel.visitResponseHeaders({
|
||||||
|
visitHeader: (name, value) => headers.push({name, value}),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Response headers, status and/or statusText are not available
|
||||||
|
// when redirect did not actually hit the network.
|
||||||
|
}
|
||||||
|
return { status, statusText, headers };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPostData(httpChannel, postData, headers) {
|
||||||
|
if (!(httpChannel instanceof Ci.nsIUploadChannel2))
|
||||||
|
return;
|
||||||
|
const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
||||||
|
const body = atob(postData);
|
||||||
|
synthesized.setData(body, body.length);
|
||||||
|
|
||||||
|
const overriddenHeader = (lowerCaseName, defaultValue) => {
|
||||||
|
if (headers) {
|
||||||
|
for (const header of headers) {
|
||||||
|
if (header.name.toLowerCase() === lowerCaseName) {
|
||||||
|
return header.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
// Clear content-length, so that upload stream resets it.
|
||||||
|
httpChannel.setRequestHeader('content-length', overriddenHeader('content-length', ''), false /* merge */);
|
||||||
|
httpChannel.explicitSetUploadStream(synthesized, overriddenHeader('content-type', 'application/octet-stream'), -1, httpChannel.requestMethod, false);
|
||||||
|
}
|
||||||
|
|
||||||
function convertString(s, source, dest) {
|
function convertString(s, source, dest) {
|
||||||
const is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
|
const is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
|
||||||
Ci.nsIStringInputStream
|
Ci.nsIStringInputStream
|
||||||
|
@ -259,8 +259,8 @@ class PageHandler {
|
|||||||
this._pageNetwork.disableRequestInterception();
|
this._pageNetwork.disableRequestInterception();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Network.resumeInterceptedRequest']({requestId, url, method, headers, postData}) {
|
async ['Network.resumeInterceptedRequest']({requestId, url, method, headers, postData, interceptResponse}) {
|
||||||
this._pageNetwork.resumeInterceptedRequest(requestId, url, method, headers, postData);
|
return await this._pageNetwork.resumeInterceptedRequest(requestId, url, method, headers, postData, interceptResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ['Network.abortInterceptedRequest']({requestId, errorCode}) {
|
async ['Network.abortInterceptedRequest']({requestId, errorCode}) {
|
||||||
|
@ -205,6 +205,12 @@ networkTypes.ResourceTiming = {
|
|||||||
responseStart: t.Number,
|
responseStart: t.Number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
networkTypes.InterceptedResponse = {
|
||||||
|
status: t.Number,
|
||||||
|
statusText: t.String,
|
||||||
|
headers: t.Array(networkTypes.HTTPHeader),
|
||||||
|
};
|
||||||
|
|
||||||
const Browser = {
|
const Browser = {
|
||||||
targets: ['browser'],
|
targets: ['browser'],
|
||||||
|
|
||||||
@ -524,6 +530,10 @@ const Network = {
|
|||||||
method: t.Optional(t.String),
|
method: t.Optional(t.String),
|
||||||
headers: t.Optional(t.Array(networkTypes.HTTPHeader)),
|
headers: t.Optional(t.Array(networkTypes.HTTPHeader)),
|
||||||
postData: t.Optional(t.String),
|
postData: t.Optional(t.String),
|
||||||
|
interceptResponse: t.Optional(t.Boolean),
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
response: t.Optional(networkTypes.InterceptedResponse),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'fulfillInterceptedRequest': {
|
'fulfillInterceptedRequest': {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user