UNPKG

20.9 kBJavaScriptView Raw
1/**
2 * Copyright 2017 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16const EventEmitter = require('events');
17const {helper, debugError} = require('./helper');
18const Multimap = require('./Multimap');
19
20class NetworkManager extends EventEmitter {
21 /**
22 * @param {!Puppeteer.CDPSession} client
23 * @param {!Puppeteer.FrameManager} frameManager
24 */
25 constructor(client, frameManager) {
26 super();
27 this._client = client;
28 this._frameManager = frameManager;
29 /** @type {!Map<string, !Request>} */
30 this._requestIdToRequest = new Map();
31 /** @type {!Map<string, !Request>} */
32 this._interceptionIdToRequest = new Map();
33 /** @type {!Object<string, string>} */
34 this._extraHTTPHeaders = {};
35
36 this._offline = false;
37
38 /** @type {?{username: string, password: string}} */
39 this._credentials = null;
40 /** @type {!Set<string>} */
41 this._attemptedAuthentications = new Set();
42 this._userRequestInterceptionEnabled = false;
43 this._protocolRequestInterceptionEnabled = false;
44 /** @type {!Multimap} */
45 this._requestHashToRequestIds = new Multimap();
46 /** @type {!Multimap} */
47 this._requestHashToInterceptionIds = new Multimap();
48
49 this._client.on('Network.requestWillBeSent', this._onRequestWillBeSent.bind(this));
50 this._client.on('Network.requestIntercepted', this._onRequestIntercepted.bind(this));
51 this._client.on('Network.responseReceived', this._onResponseReceived.bind(this));
52 this._client.on('Network.loadingFinished', this._onLoadingFinished.bind(this));
53 this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this));
54 }
55
56 /**
57 * @param {?{username: string, password: string}} credentials
58 */
59 async authenticate(credentials) {
60 this._credentials = credentials;
61 await this._updateProtocolRequestInterception();
62 }
63
64 /**
65 * @param {!Object<string, string>} extraHTTPHeaders
66 */
67 async setExtraHTTPHeaders(extraHTTPHeaders) {
68 this._extraHTTPHeaders = {};
69 for (const key of Object.keys(extraHTTPHeaders)) {
70 const value = extraHTTPHeaders[key];
71 console.assert(helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
72 this._extraHTTPHeaders[key.toLowerCase()] = value;
73 }
74 await this._client.send('Network.setExtraHTTPHeaders', { headers: this._extraHTTPHeaders });
75 }
76
77 /**
78 * @return {!Object<string, string>}
79 */
80 extraHTTPHeaders() {
81 return Object.assign({}, this._extraHTTPHeaders);
82 }
83
84 /**
85 * @param {boolean} value
86 */
87 async setOfflineMode(value) {
88 if (this._offline === value)
89 return;
90 this._offline = value;
91 await this._client.send('Network.emulateNetworkConditions', {
92 offline: this._offline,
93 // values of 0 remove any active throttling. crbug.com/456324#c9
94 latency: 0,
95 downloadThroughput: -1,
96 uploadThroughput: -1
97 });
98 }
99
100 /**
101 * @param {string} userAgent
102 */
103 async setUserAgent(userAgent) {
104 await this._client.send('Network.setUserAgentOverride', { userAgent });
105 }
106
107 /**
108 * @param {boolean} value
109 */
110 async setRequestInterception(value) {
111 this._userRequestInterceptionEnabled = value;
112 await this._updateProtocolRequestInterception();
113 }
114
115 async _updateProtocolRequestInterception() {
116 const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
117 if (enabled === this._protocolRequestInterceptionEnabled)
118 return;
119 this._protocolRequestInterceptionEnabled = enabled;
120 const patterns = enabled ? [{urlPattern: '*'}] : [];
121 await Promise.all([
122 this._client.send('Network.setCacheDisabled', {cacheDisabled: enabled}),
123 this._client.send('Network.setRequestInterception', {patterns})
124 ]);
125 }
126
127 /**
128 * @param {!Object} event
129 */
130 _onRequestIntercepted(event) {
131 if (event.authChallenge) {
132 let response = 'Default';
133 if (this._attemptedAuthentications.has(event.interceptionId)) {
134 response = 'CancelAuth';
135 } else if (this._credentials) {
136 response = 'ProvideCredentials';
137 this._attemptedAuthentications.add(event.interceptionId);
138 }
139 const {username, password} = this._credentials || {username: undefined, password: undefined};
140 this._client.send('Network.continueInterceptedRequest', {
141 interceptionId: event.interceptionId,
142 authChallengeResponse: { response, username, password }
143 }).catch(debugError);
144 return;
145 }
146 if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) {
147 this._client.send('Network.continueInterceptedRequest', {
148 interceptionId: event.interceptionId
149 }).catch(debugError);
150 }
151
152 if (event.redirectUrl) {
153 const request = this._interceptionIdToRequest.get(event.interceptionId);
154 if (request) {
155 this._handleRequestRedirect(request, event.responseStatusCode, event.responseHeaders);
156 this._handleRequestStart(request._requestId, event.interceptionId, event.redirectUrl, event.resourceType, event.request, event.frameId);
157 }
158 return;
159 }
160 const requestHash = generateRequestHash(event.request);
161 const requestId = this._requestHashToRequestIds.firstValue(requestHash);
162 if (requestId) {
163 this._requestHashToRequestIds.delete(requestHash, requestId);
164 this._handleRequestStart(requestId, event.interceptionId, event.request.url, event.resourceType, event.request, event.frameId);
165 } else {
166 this._requestHashToInterceptionIds.set(requestHash, event.interceptionId);
167 this._handleRequestStart(null, event.interceptionId, event.request.url, event.resourceType, event.request, event.frameId);
168 }
169 }
170
171 /**
172 * @param {!Request} request
173 * @param {number} redirectStatus
174 * @param {!Object} redirectHeaders
175 */
176 _handleRequestRedirect(request, redirectStatus, redirectHeaders) {
177 const response = new Response(this._client, request, redirectStatus, redirectHeaders);
178 request._response = response;
179 this._requestIdToRequest.delete(request._requestId);
180 this._interceptionIdToRequest.delete(request._interceptionId);
181 this._attemptedAuthentications.delete(request._interceptionId);
182 this.emit(NetworkManager.Events.Response, response);
183 this.emit(NetworkManager.Events.RequestFinished, request);
184 }
185
186 /**
187 * @param {?string} requestId
188 * @param {?string} interceptionId
189 * @param {string} url
190 * @param {string} resourceType
191 * @param {!Object} requestPayload
192 * @param {?string} frameId
193 */
194 _handleRequestStart(requestId, interceptionId, url, resourceType, requestPayload, frameId) {
195 let frame = null;
196 if (frameId)
197 frame = this._frameManager.frame(frameId);
198 const request = new Request(this._client, requestId, interceptionId, this._userRequestInterceptionEnabled, url, resourceType, requestPayload, frame);
199 if (requestId)
200 this._requestIdToRequest.set(requestId, request);
201 if (interceptionId)
202 this._interceptionIdToRequest.set(interceptionId, request);
203 this.emit(NetworkManager.Events.Request, request);
204 }
205
206 /**
207 * @param {!Object} event
208 */
209 _onRequestWillBeSent(event) {
210 if (this._protocolRequestInterceptionEnabled) {
211 // All redirects are handled in requestIntercepted.
212 if (event.redirectResponse)
213 return;
214 const requestHash = generateRequestHash(event.request);
215 const interceptionId = this._requestHashToInterceptionIds.firstValue(requestHash);
216 const request = interceptionId ? this._interceptionIdToRequest.get(interceptionId) : null;
217 if (request) {
218 request._requestId = event.requestId;
219 this._requestIdToRequest.set(event.requestId, request);
220 this._requestHashToInterceptionIds.delete(requestHash, interceptionId);
221 } else {
222 this._requestHashToRequestIds.set(requestHash, event.requestId);
223 }
224 return;
225 }
226 if (event.redirectResponse) {
227 const request = this._requestIdToRequest.get(event.requestId);
228 // If we connect late to the target, we could have missed the requestWillBeSent event.
229 if (request)
230 this._handleRequestRedirect(request, event.redirectResponse.status, event.redirectResponse.headers);
231 }
232 this._handleRequestStart(event.requestId, null, event.request.url, event.type, event.request, event.frameId);
233 }
234
235 /**
236 * @param {!Object} event
237 */
238 _onResponseReceived(event) {
239 const request = this._requestIdToRequest.get(event.requestId);
240 // FileUpload sends a response without a matching request.
241 if (!request)
242 return;
243 const response = new Response(this._client, request, event.response.status, event.response.headers);
244 request._response = response;
245 this.emit(NetworkManager.Events.Response, response);
246 }
247
248 /**
249 * @param {!Object} event
250 */
251 _onLoadingFinished(event) {
252 const request = this._requestIdToRequest.get(event.requestId);
253 // For certain requestIds we never receive requestWillBeSent event.
254 // @see https://crbug.com/750469
255 if (!request)
256 return;
257 request._completePromiseFulfill.call(null);
258 this._requestIdToRequest.delete(request._requestId);
259 this._interceptionIdToRequest.delete(request._interceptionId);
260 this._attemptedAuthentications.delete(request._interceptionId);
261 this.emit(NetworkManager.Events.RequestFinished, request);
262 }
263
264 /**
265 * @param {!Object} event
266 */
267 _onLoadingFailed(event) {
268 const request = this._requestIdToRequest.get(event.requestId);
269 // For certain requestIds we never receive requestWillBeSent event.
270 // @see https://crbug.com/750469
271 if (!request)
272 return;
273 request._failureText = event.errorText;
274 request._completePromiseFulfill.call(null);
275 this._requestIdToRequest.delete(request._requestId);
276 this._interceptionIdToRequest.delete(request._interceptionId);
277 this._attemptedAuthentications.delete(request._interceptionId);
278 this.emit(NetworkManager.Events.RequestFailed, request);
279 }
280}
281
282class Request {
283 /**
284 * @param {!Puppeteer.CDPSession} client
285 * @param {?string} requestId
286 * @param {string} interceptionId
287 * @param {boolean} allowInterception
288 * @param {string} url
289 * @param {string} resourceType
290 * @param {!Object} payload
291 * @param {?Puppeteer.Frame} frame
292 */
293 constructor(client, requestId, interceptionId, allowInterception, url, resourceType, payload, frame) {
294 this._client = client;
295 this._requestId = requestId;
296 this._interceptionId = interceptionId;
297 this._allowInterception = allowInterception;
298 this._interceptionHandled = false;
299 this._response = null;
300 this._failureText = null;
301 this._completePromise = new Promise(fulfill => {
302 this._completePromiseFulfill = fulfill;
303 });
304
305 this._url = url;
306 this._resourceType = resourceType.toLowerCase();
307 this._method = payload.method;
308 this._postData = payload.postData;
309 this._headers = {};
310 this._frame = frame;
311 for (const key of Object.keys(payload.headers))
312 this._headers[key.toLowerCase()] = payload.headers[key];
313 }
314
315 /**
316 * @return {string}
317 */
318 url() {
319 return this._url;
320 }
321
322 /**
323 * @return {string}
324 */
325 resourceType() {
326 return this._resourceType;
327 }
328
329 /**
330 * @return {string}
331 */
332 method() {
333 return this._method;
334 }
335
336 /**
337 * @return {string}
338 */
339 postData() {
340 return this._postData;
341 }
342
343 /**
344 * @return {!Object}
345 */
346 headers() {
347 return this._headers;
348 }
349
350 /**
351 * @return {?Response}
352 */
353 response() {
354 return this._response;
355 }
356
357 /**
358 * @return {?Puppeteer.Frame}
359 */
360 frame() {
361 return this._frame;
362 }
363
364 /**
365 * @return {?{errorText: string}}
366 */
367 failure() {
368 if (!this._failureText)
369 return null;
370 return {
371 errorText: this._failureText
372 };
373 }
374
375 /**
376 * @param {!Object=} overrides
377 */
378 async continue(overrides = {}) {
379 console.assert(this._allowInterception, 'Request Interception is not enabled!');
380 console.assert(!this._interceptionHandled, 'Request is already handled!');
381 this._interceptionHandled = true;
382 await this._client.send('Network.continueInterceptedRequest', {
383 interceptionId: this._interceptionId,
384 url: overrides.url,
385 method: overrides.method,
386 postData: overrides.postData,
387 headers: overrides.headers,
388 }).catch(error => {
389 // In certain cases, protocol will return error if the request was already canceled
390 // or the page was closed. We should tolerate these errors.
391 debugError(error);
392 });
393 }
394
395 /**
396 * @param {!{status: number, headers: Object, contentType: string, body: (string|Buffer)}} response
397 */
398 async respond(response) {
399 // Mocking responses for dataURL requests is not currently supported.
400 if (this._url.startsWith('data:'))
401 return;
402 console.assert(this._allowInterception, 'Request Interception is not enabled!');
403 console.assert(!this._interceptionHandled, 'Request is already handled!');
404 this._interceptionHandled = true;
405
406 const responseBody = response.body && helper.isString(response.body) ? Buffer.from(/** @type {string} */(response.body)) : /** @type {?Buffer} */(response.body || null);
407
408 const responseHeaders = {};
409 if (response.headers) {
410 for (const header of Object.keys(response.headers))
411 responseHeaders[header.toLowerCase()] = response.headers[header];
412 }
413 if (response.contentType)
414 responseHeaders['content-type'] = response.contentType;
415 if (responseBody && !('content-length' in responseHeaders)) {
416 // @ts-ignore
417 responseHeaders['content-length'] = Buffer.byteLength(responseBody);
418 }
419
420 const statusCode = response.status || 200;
421 const statusText = statusTexts[statusCode] || '';
422 const statusLine = `HTTP/1.1 ${statusCode} ${statusText}`;
423
424 const CRLF = '\r\n';
425 let text = statusLine + CRLF;
426 for (const header of Object.keys(responseHeaders))
427 text += header + ': ' + responseHeaders[header] + CRLF;
428 text += CRLF;
429 let responseBuffer = Buffer.from(text, 'utf8');
430 if (responseBody)
431 responseBuffer = Buffer.concat([responseBuffer, responseBody]);
432
433 await this._client.send('Network.continueInterceptedRequest', {
434 interceptionId: this._interceptionId,
435 rawResponse: responseBuffer.toString('base64')
436 }).catch(error => {
437 // In certain cases, protocol will return error if the request was already canceled
438 // or the page was closed. We should tolerate these errors.
439 debugError(error);
440 });
441 }
442
443 /**
444 * @param {string=} errorCode
445 */
446 async abort(errorCode = 'failed') {
447 const errorReason = errorReasons[errorCode];
448 console.assert(errorReason, 'Unknown error code: ' + errorCode);
449 console.assert(this._allowInterception, 'Request Interception is not enabled!');
450 console.assert(!this._interceptionHandled, 'Request is already handled!');
451 this._interceptionHandled = true;
452 await this._client.send('Network.continueInterceptedRequest', {
453 interceptionId: this._interceptionId,
454 errorReason
455 }).catch(error => {
456 // In certain cases, protocol will return error if the request was already canceled
457 // or the page was closed. We should tolerate these errors.
458 debugError(error);
459 });
460 }
461}
462
463const errorReasons = {
464 'aborted': 'Aborted',
465 'accessdenied': 'AccessDenied',
466 'addressunreachable': 'AddressUnreachable',
467 'connectionaborted': 'ConnectionAborted',
468 'connectionclosed': 'ConnectionClosed',
469 'connectionfailed': 'ConnectionFailed',
470 'connectionrefused': 'ConnectionRefused',
471 'connectionreset': 'ConnectionReset',
472 'internetdisconnected': 'InternetDisconnected',
473 'namenotresolved': 'NameNotResolved',
474 'timedout': 'TimedOut',
475 'failed': 'Failed',
476};
477
478helper.tracePublicAPI(Request);
479
480class Response {
481 /**
482 * @param {!Puppeteer.CDPSession} client
483 * @param {!Request} request
484 * @param {number} status
485 * @param {!Object} headers
486 */
487 constructor(client, request, status, headers) {
488 this._client = client;
489 this._request = request;
490 this._contentPromise = null;
491
492 this._status = status;
493 this._url = request.url();
494 this._headers = {};
495 for (const key of Object.keys(headers))
496 this._headers[key.toLowerCase()] = headers[key];
497 }
498
499 /**
500 * @return {string}
501 */
502 url() {
503 return this._url;
504 }
505
506 /**
507 * @return {boolean}
508 */
509 ok() {
510 return this._status >= 200 && this._status <= 299;
511 }
512
513 /**
514 * @return {number}
515 */
516 status() {
517 return this._status;
518 }
519
520 /**
521 * @return {!Object}
522 */
523 headers() {
524 return this._headers;
525 }
526
527 /**
528 * @return {!Promise<!Buffer>}
529 */
530 buffer() {
531 if (!this._contentPromise) {
532 this._contentPromise = this._request._completePromise.then(async() => {
533 const response = await this._client.send('Network.getResponseBody', {
534 requestId: this._request._requestId
535 });
536 return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
537 });
538 }
539 return this._contentPromise;
540 }
541
542 /**
543 * @return {!Promise<string>}
544 */
545 async text() {
546 const content = await this.buffer();
547 return content.toString('utf8');
548 }
549
550 /**
551 * @return {!Promise<!Object>}
552 */
553 async json() {
554 const content = await this.text();
555 return JSON.parse(content);
556 }
557
558 /**
559 * @return {!Request}
560 */
561 request() {
562 return this._request;
563 }
564}
565helper.tracePublicAPI(Response);
566
567/**
568 * @param {!Object} request
569 * @return {string}
570 */
571function generateRequestHash(request) {
572 let normalizedURL = request.url;
573 try {
574 // Decoding is necessary to normalize URLs. @see crbug.com/759388
575 // The method will throw if the URL is malformed. In this case,
576 // consider URL to be normalized as-is.
577 normalizedURL = decodeURI(request.url);
578 } catch (e) {
579 }
580 const hash = {
581 url: normalizedURL,
582 method: request.method,
583 postData: request.postData,
584 headers: {},
585 };
586
587 if (!normalizedURL.startsWith('data:')) {
588 const headers = Object.keys(request.headers);
589 headers.sort();
590 for (let header of headers) {
591 const headerValue = request.headers[header];
592 header = header.toLowerCase();
593 if (header === 'accept' || header === 'referer' || header === 'x-devtools-emulate-network-conditions-client-id')
594 continue;
595 hash.headers[header] = headerValue;
596 }
597 }
598 return JSON.stringify(hash);
599}
600
601NetworkManager.Events = {
602 Request: 'request',
603 Response: 'response',
604 RequestFailed: 'requestfailed',
605 RequestFinished: 'requestfinished',
606};
607
608const statusTexts = {
609 '100': 'Continue',
610 '101': 'Switching Protocols',
611 '102': 'Processing',
612 '200': 'OK',
613 '201': 'Created',
614 '202': 'Accepted',
615 '203': 'Non-Authoritative Information',
616 '204': 'No Content',
617 '206': 'Partial Content',
618 '207': 'Multi-Status',
619 '208': 'Already Reported',
620 '209': 'IM Used',
621 '300': 'Multiple Choices',
622 '301': 'Moved Permanently',
623 '302': 'Found',
624 '303': 'See Other',
625 '304': 'Not Modified',
626 '305': 'Use Proxy',
627 '306': 'Switch Proxy',
628 '307': 'Temporary Redirect',
629 '308': 'Permanent Redirect',
630 '400': 'Bad Request',
631 '401': 'Unauthorized',
632 '402': 'Payment Required',
633 '403': 'Forbidden',
634 '404': 'Not Found',
635 '405': 'Method Not Allowed',
636 '406': 'Not Acceptable',
637 '407': 'Proxy Authentication Required',
638 '408': 'Request Timeout',
639 '409': 'Conflict',
640 '410': 'Gone',
641 '411': 'Length Required',
642 '412': 'Precondition Failed',
643 '413': 'Payload Too Large',
644 '414': 'URI Too Long',
645 '415': 'Unsupported Media Type',
646 '416': 'Range Not Satisfiable',
647 '417': 'Expectation Failed',
648 '418': 'I\'m a teapot',
649 '421': 'Misdirected Request',
650 '422': 'Unprocessable Entity',
651 '423': 'Locked',
652 '424': 'Failed Dependency',
653 '426': 'Upgrade Required',
654 '428': 'Precondition Required',
655 '429': 'Too Many Requests',
656 '431': 'Request Header Fields Too Large',
657 '451': 'Unavailable For Legal Reasons',
658 '500': 'Internal Server Error',
659 '501': 'Not Implemented',
660 '502': 'Bad Gateway',
661 '503': 'Service Unavailable',
662 '504': 'Gateway Timeout',
663 '505': 'HTTP Version Not Supported',
664 '506': 'Variant Also Negotiates',
665 '507': 'Insufficient Storage',
666 '508': 'Loop Detected',
667 '510': 'Not Extended',
668 '511': 'Network Authentication Required',
669};
670
671module.exports = NetworkManager;