browser(firefox): support response interception (#7509)

This commit is contained in:
Yury Semikhatsky 2021-07-09 05:41:53 -07:00 committed by GitHub
parent 70b054d240
commit efb21b9e9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 136 additions and 43 deletions

View File

@ -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

View File

@ -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

View File

@ -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}) {

View File

@ -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': {