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
10 changes: 10 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
+ [page.$$(selector)](#pageselector)
+ [page.$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args)
+ [page.addScriptTag(url)](#pageaddscripttagurl)
+ [page.authenticate(credentials)](#pageauthenticatecredentials)
+ [page.click(selector[, options])](#pageclickselector-options)
+ [page.close()](#pageclose)
+ [page.content()](#pagecontent)
Expand Down Expand Up @@ -356,6 +357,15 @@ Adds a `<script>` tag into the page with the desired url. Alternatively, a local

Shortcut for [page.mainFrame().addScriptTag(url)](#frameaddscripttagurl).

#### page.authenticate(credentials)
- `credentials` <[Object]>
- `username` <[string]>
- `password` <[string]>
- returns: <[Promise]>

Provide credentials for [http authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).

To disable authentication, pass `null`.

#### page.click(selector[, options])
- `selector` <[string]> A [selector] to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.
Expand Down
71 changes: 59 additions & 12 deletions lib/NetworkManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ class NetworkManager extends EventEmitter {
/** @type {!Object<string, string>} */
this._extraHTTPHeaders = {};

this._requestInterceptionEnabled = false;
/** @type {?{username: string, password: string}} */
this._credentials = null;
/** @type {!Set<string>} */
this._attemptedAuthentications = new Set();
this._userRequestInterceptionEnabled = false;
this._protocolRequestInterceptionEnabled = false;
/** @type {!Multimap<string, string>} */
this._requestHashToRequestIds = new Multimap();
/** @type {!Multimap<string, !Object>} */
Expand All @@ -45,6 +50,14 @@ class NetworkManager extends EventEmitter {
this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this));
}

/**
* @param {?{username: string, password: string}} credentials
*/
async authenticate(credentials) {
this._credentials = credentials;
await this._updateProtocolRequestInterception();
}

/**
* @param {!Object<string, string>} extraHTTPHeaders
*/
Expand Down Expand Up @@ -73,8 +86,16 @@ class NetworkManager extends EventEmitter {
* @param {boolean} value
*/
async setRequestInterceptionEnabled(value) {
await this._client.send('Network.setRequestInterceptionEnabled', {enabled: !!value});
this._requestInterceptionEnabled = value;
this._userRequestInterceptionEnabled = value;
await this._updateProtocolRequestInterception();
}

async _updateProtocolRequestInterception() {
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
if (enabled === this._protocolRequestInterceptionEnabled)
return;
this._protocolRequestInterceptionEnabled = enabled;
await this._client.send('Network.setRequestInterceptionEnabled', {enabled});
}

/**
Expand All @@ -84,6 +105,27 @@ class NetworkManager extends EventEmitter {
// Strip out url hash to be consistent with requestWillBeSent. @see crbug.com/755456
event.request.url = removeURLHash(event.request.url);

if (event.authChallenge) {
let response = 'Default';
if (this._attemptedAuthentications.has(event.interceptionId)) {
response = 'CancelAuth';
} else if (this._credentials) {
response = 'ProvideCredentials';
this._attemptedAuthentications.add(event.interceptionId);
}
const {username, password} = this._credentials || {};
this._client.send('Network.continueInterceptedRequest', {
interceptionId: event.interceptionId,
authChallengeResponse: { response, username, password }
}).catch(debugError);
return;
}
if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) {
this._client.send('Network.continueInterceptedRequest', {
interceptionId: event.interceptionId
}).catch(debugError);
}

if (event.redirectStatusCode) {
const request = this._interceptionIdToRequest.get(event.interceptionId);
console.assert(request, 'INTERNAL ERROR: failed to find request for interception redirect.');
Expand All @@ -106,6 +148,7 @@ class NetworkManager extends EventEmitter {
request._response = response;
this._requestIdToRequest.delete(request._requestId);
this._interceptionIdToRequest.delete(request._interceptionId);
this._attemptedAuthentications.delete(request._interceptionId);
this.emit(NetworkManager.Events.Response, response);
this.emit(NetworkManager.Events.RequestFinished, request);
}
Expand All @@ -118,7 +161,7 @@ class NetworkManager extends EventEmitter {
* @param {!Object} requestPayload
*/
_handleRequestStart(requestId, interceptionId, url, resourceType, requestPayload) {
const request = new Request(this._client, requestId, interceptionId, url, resourceType, requestPayload);
const request = new Request(this._client, requestId, interceptionId, this._userRequestInterceptionEnabled, url, resourceType, requestPayload);
this._requestIdToRequest.set(requestId, request);
this._interceptionIdToRequest.set(interceptionId, request);
this.emit(NetworkManager.Events.Request, request);
Expand All @@ -128,7 +171,7 @@ class NetworkManager extends EventEmitter {
* @param {!Object} event
*/
_onRequestWillBeSent(event) {
if (this._requestInterceptionEnabled && !event.request.url.startsWith('data:')) {
if (this._protocolRequestInterceptionEnabled && !event.request.url.startsWith('data:')) {
// All redirects are handled in requestIntercepted.
if (event.redirectResponse)
return;
Expand Down Expand Up @@ -181,8 +224,9 @@ class NetworkManager extends EventEmitter {
if (!request)
return;
request._completePromiseFulfill.call(null);
this._requestIdToRequest.delete(event.requestId);
this._interceptionIdToRequest.delete(event.interceptionId);
this._requestIdToRequest.delete(request._requestId);
this._interceptionIdToRequest.delete(request._interceptionId);
this._attemptedAuthentications.delete(request._interceptionId);
this.emit(NetworkManager.Events.RequestFinished, request);
}

Expand All @@ -196,8 +240,9 @@ class NetworkManager extends EventEmitter {
if (!request)
return;
request._completePromiseFulfill.call(null);
this._requestIdToRequest.delete(event.requestId);
this._interceptionIdToRequest.delete(event.interceptionId);
this._requestIdToRequest.delete(request._requestId);
this._interceptionIdToRequest.delete(request._interceptionId);
this._attemptedAuthentications.delete(request._interceptionId);
this.emit(NetworkManager.Events.RequestFailed, request);
}
}
Expand All @@ -207,14 +252,16 @@ class Request {
* @param {!Connection} client
* @param {string} requestId
* @param {string} interceptionId
* @param {string} allowInterception
* @param {string} url
* @param {string} resourceType
* @param {!Object} payload
*/
constructor(client, requestId, interceptionId, url, resourceType, payload) {
constructor(client, requestId, interceptionId, allowInterception, url, resourceType, payload) {
this._client = client;
this._requestId = requestId;
this._interceptionId = interceptionId;
this._allowInterception = allowInterception;
this._interceptionHandled = false;
this._response = null;
this._completePromise = new Promise(fulfill => {
Expand Down Expand Up @@ -244,7 +291,7 @@ class Request {
// DataURL's are not interceptable. In this case, do nothing.
if (this.url.startsWith('data:'))
return;
console.assert(this._interceptionId, 'Request Interception is not enabled!');
console.assert(this._allowInterception, 'Request Interception is not enabled!');
console.assert(!this._interceptionHandled, 'Request is already handled!');
this._interceptionHandled = true;
await this._client.send('Network.continueInterceptedRequest', {
Expand All @@ -264,7 +311,7 @@ class Request {
// DataURL's are not interceptable. In this case, do nothing.
if (this.url.startsWith('data:'))
return;
console.assert(this._interceptionId, 'Request Interception is not enabled!');
console.assert(this._allowInterception, 'Request Interception is not enabled!');
console.assert(!this._interceptionHandled, 'Request is already handled!');
this._interceptionHandled = true;
await this._client.send('Network.continueInterceptedRequest', {
Expand Down
7 changes: 7 additions & 0 deletions lib/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,13 @@ class Page extends EventEmitter {
}
}

/**
* @param {?{username: string, password: string}} credentials
*/
async authenticate(credentials) {
return this._networkManager.authenticate(credentials);
}

/**
* @param {!Object<string, string>} headers
*/
Expand Down
21 changes: 21 additions & 0 deletions test/server/SimpleServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ class SimpleServer {

/** @type {!Map<string, function(!IncomingMessage, !ServerResponse)>} */
this._routes = new Map();
/** @type {!Map<string, !{username:string, password:string}>} */
this._auths = new Map();
/** @type {!Map<string, !Promise>} */
this._requestSubscribers = new Map();
}
Expand All @@ -88,6 +90,15 @@ class SimpleServer {
socket.once('close', () => this._sockets.delete(socket));
}

/**
* @param {string} path
* @param {string} username
* @param {string} password
*/
setAuth(path, username, password) {
this._auths.set(path, {username, password});
}

async stop() {
this.reset();
for (const socket of this._sockets)
Expand Down Expand Up @@ -136,6 +147,7 @@ class SimpleServer {

reset() {
this._routes.clear();
this._auths.clear();
const error = new Error('Static Server has been reset');
for (const subscriber of this._requestSubscribers.values())
subscriber[rejectSymbol].call(null, error);
Expand All @@ -150,6 +162,15 @@ class SimpleServer {
throw error;
});
const pathName = url.parse(request.url).path;
if (this._auths.has(pathName)) {
const auth = this._auths.get(pathName);
const credentials = new Buffer((request.headers.authorization || '').split(' ')[1] || '', 'base64').toString();
if (credentials !== `${auth.username}:${auth.password}`) {
response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Secure Area"' });
response.end('HTTP Error 401 Unauthorized: Access is denied');
return;
}
}
// Notify request subscriber.
if (this._requestSubscribers.has(pathName))
this._requestSubscribers.get(pathName)[fulfillSymbol].call(null, request);
Expand Down
37 changes: 37 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,43 @@ describe('Page', function() {
expect(request.headers['foo']).toBe('bar');
}));
});
describe('Page.authenticate', function() {
it('should work', SX(async function() {
server.setAuth('/empty.html', 'user', 'pass');
let response = await page.goto(EMPTY_PAGE);
expect(response.status).toBe(401);
await page.authenticate({
username: 'user',
password: 'pass'
});
response = await page.reload();
expect(response.status).toBe(200);
}));
it('should fail if wrong credentials', SX(async function() {
// Use unique user/password since Chrome caches credentials per origin.
server.setAuth('/empty.html', 'user2', 'pass2');
await page.authenticate({
username: 'foo',
password: 'bar'
});
const response = await page.goto(EMPTY_PAGE);
expect(response.status).toBe(401);
}));
it('should allow disable authentication', SX(async function() {
// Use unique user/password since Chrome caches credentials per origin.
server.setAuth('/empty.html', 'user3', 'pass3');
await page.authenticate({
username: 'user3',
password: 'pass3'
});
let response = await page.goto(EMPTY_PAGE);
expect(response.status).toBe(200);
await page.authenticate(null);
// Navigate to a different origin to bust Chrome's credential caching.
response = await page.goto(CROSS_PROCESS_PREFIX + '/empty.html');
expect(response.status).toBe(401);
}));
});
describe('Page.setContent', function() {
const expectedOutput = '<html><head></head><body><div>hello</div></body></html>';
it('should work', SX(async function() {
Expand Down