mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
[api] Implement page.authenticate method (#729)
This patch implements `page.authenticate` which should cover all cases of HTTP authentication. Fixes #426.
This commit is contained in:
parent
0bea42bd8c
commit
0db6165d73
10
docs/api.md
10
docs/api.md
@ -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)
|
||||
@ -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.
|
||||
|
@ -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>} */
|
||||
@ -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
|
||||
*/
|
||||
@ -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});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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.');
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 => {
|
||||
@ -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', {
|
||||
@ -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', {
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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();
|
||||
}
|
||||
@ -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)
|
||||
@ -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);
|
||||
@ -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);
|
||||
|
37
test/test.js
37
test/test.js
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user