Implement Page.setRequestInterceptor

This patch:
- introduces Request class.
- implements Page.setRequestInterceptor method. The method
  allows to install a callback which will be called for every request
  with a |Request| object as a single parameter. The callback is free
  to override certain request's properties and then either continue or
  abort it.
- implements request interception api for phantom-shim and unskips the
  module/webpage/abort-network-request.js phantomjs test

References #1
This commit is contained in:
Andrey Lushnikov 2017-06-15 00:20:37 -07:00
parent 9bdf9ed5de
commit e274c26e8b
6 changed files with 196 additions and 2 deletions

View File

@ -18,6 +18,7 @@ var fs = require('fs');
var EventEmitter = require('events'); var EventEmitter = require('events');
var helpers = require('./helpers'); var helpers = require('./helpers');
var mime = require('mime'); var mime = require('mime');
var Request = require('./Request');
class Page extends EventEmitter { class Page extends EventEmitter {
/** /**
@ -56,16 +57,35 @@ class Page extends EventEmitter {
this._scriptIdToPageCallback = new Map(); this._scriptIdToPageCallback = new Map();
/** @type {!Array<string>} */ /** @type {!Array<string>} */
this._blockedURLs = []; this._blockedURLs = [];
/** @type {?function(!Request)} */
this._requestInterceptor = null;
client.on('Debugger.paused', event => this._onDebuggerPaused(event)); client.on('Debugger.paused', event => this._onDebuggerPaused(event));
client.on('Debugger.scriptParsed', event => this._onScriptParsed(event)); client.on('Debugger.scriptParsed', event => this._onScriptParsed(event));
client.on('Network.responseReceived', event => this.emit(Page.Events.ResponseReceived, event.response)); client.on('Network.responseReceived', event => this.emit(Page.Events.ResponseReceived, event.response));
client.on('Network.loadingFailed', event => this.emit(Page.Events.ResourceLoadingFailed, event)); client.on('Network.loadingFailed', event => this.emit(Page.Events.ResourceLoadingFailed, event));
client.on('Network.requestIntercepted', event => this._onRequestIntercepted(event));
client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event)); client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event));
client.on('Page.javascriptDialogOpening', event => this._onDialog(event)); client.on('Page.javascriptDialogOpening', event => this._onDialog(event));
client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails)); client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails));
} }
/**
* @param {?function(!Request)} interceptor
*/
async setRequestInterceptor(interceptor) {
this._requestInterceptor = interceptor;
await this._client.send('Network.enableRequestInterception', {enabled: !!interceptor});
}
/**
* @param {!Object} event
*/
_onRequestIntercepted(event) {
var request = new Request(this._client, event.InterceptionId, event.request);
this._requestInterceptor(request);
}
/** /**
* @param {!Array<string>} patterns * @param {!Array<string>} patterns
* @return {!Promise} * @return {!Promise}

123
lib/Request.js Normal file
View File

@ -0,0 +1,123 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class Request {
/**
* @param {!Connection} client
* @param {string} interceptionId
* @param {!Object} payload
*/
constructor(client, interceptionId, payload) {
this._client = client;
this._interceptionId = interceptionId;
this._url = payload.url;
this._method = payload.method;
this._headers = payload.headers;
this._postData = payload.postData;
this._urlOverride = undefined;
this._methodOverride = undefined;
this._postDataOverride = undefined;
this._handled = false;
}
/**
* @return {string}
*/
url() {
return this._urlOverride || this._url;
}
/**
* @param {string} url
*/
setUrl(url) {
this._urlOverride = url;
}
/**
* @return {string}
*/
method() {
return this._methodOverride || this._method;
}
/**
* @param {string} method
*/
setMethod(method) {
this._methodOverride = method;
}
/**
* @return {!Object}
*/
headers() {
return Object.assign({}, this._headersOverride || this._headers);
}
/**
* @param {!Object} headers
*/
setHeaders(headers) {
this._headersOverride = headers;
}
/**
* @return {(string|undefined)}
*/
postData() {
return this._postDataOverride || this._postData;
}
/**
* @return {(string|undefined)}
*/
setPostData(data) {
this._postDataOverride = data;
}
abort() {
console.assert(!this._handled, 'This request is already handled!');
this._handled = true;
this._client.send('Network.continueInterceptedRequest', {
interceptionId: this._interceptionId,
errorReason: 'Aborted'
});
}
continue() {
console.assert(!this._handled, 'This request is already handled!');
this._handled = true;
this._client.send('Network.continueInterceptedRequest', {
interceptionId: this._interceptionId,
url: this._urlOverride,
method: this._methodOverride,
postData: this._postDataOverride,
headers: this._headersOverride
});
}
/**
* @return {boolean}
*/
handled() {
return this._handled;
}
}
module.exports = Request;

View File

@ -52,6 +52,7 @@ class WebPage {
this.libraryPath = path.dirname(scriptPath); this.libraryPath = path.dirname(scriptPath);
this._onResourceRequestedCallback = undefined;
this._onConfirmCallback = undefined; this._onConfirmCallback = undefined;
this._onAlertCallback = undefined; this._onAlertCallback = undefined;
this._onError = noop; this._onError = noop;
@ -65,6 +66,34 @@ class WebPage {
this._pageEvents.on(PageEvents.Exception, (exception, stack) => (this._onError || noop).call(null, exception, stack)); this._pageEvents.on(PageEvents.Exception, (exception, stack) => (this._onError || noop).call(null, exception, stack));
} }
/**
* @return {?function(!Object, !Request)}
*/
get onResourceRequested() {
return this._onResourceRequestedCallback;
}
/**
* @return {?function(!Object, !Request)} callback
*/
set onResourceRequested(callback) {
this._onResourceRequestedCallback = callback;
this._page.setRequestInterceptor(callback ? resourceInterceptor : null);
/**
* @param {!Request} request
*/
function resourceInterceptor(request) {
var requestData = {
url: request.url(),
headers: request.headers()
};
callback(requestData, request);
if (!request.handled())
request.continue();
}
}
_onResponseReceived(response) { _onResponseReceived(response) {
if (!this.onResourceReceived) if (!this.onResourceReceived)
return; return;

View File

@ -84,6 +84,29 @@ describe('Puppeteer', function() {
expect(result).toBe(15); expect(result).toBe(15);
})); }));
}); });
describe('Page.setRequestInterceptor', function() {
it('should work', SX(async function() {
page.setRequestInterceptor(request => {
expect(request.url()).toContain('empty.html');
expect(request.headers()['User-Agent']).toBeTruthy();
expect(request.method()).toBe('GET');
expect(request.postData()).toBe(undefined);
request.continue();
});
var success = await page.navigate(EMPTY_PAGE);
}));
it('should show extraHTTPHeaders', SX(async function() {
await page.setExtraHTTPHeaders({
foo: 'bar'
});
page.setRequestInterceptor(request => {
expect(request.headers()['foo']).toBe('bar');
request.continue();
});
var success = await page.navigate(EMPTY_PAGE);
}));
});
}); });
// Since Jasmine doesn't like async functions, they should be wrapped // Since Jasmine doesn't like async functions, they should be wrapped

View File

@ -23,4 +23,4 @@ page.open(address, function(status) {
console.log('Unable to load the address!'); console.log('Unable to load the address!');
phantom.exit(); phantom.exit();
} }
}); });

View File

@ -1,4 +1,3 @@
//! unsupported
var webpage = require('webpage'); var webpage = require('webpage');
async_test(function () { async_test(function () {