UNPKG

22.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, assert, 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, !Protocol.Network.requestWillBeSentPayload>} */
32 this._requestIdToRequestWillBeSentEvent = 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<string, string>} */
45 this._requestHashToRequestIds = new Multimap();
46 /** @type {!Multimap<string, string>} */
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.requestServedFromCache', this._onRequestServedFromCache.bind(this));
52 this._client.on('Network.responseReceived', this._onResponseReceived.bind(this));
53 this._client.on('Network.loadingFinished', this._onLoadingFinished.bind(this));
54 this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this));
55 }
56
57 /**
58 * @param {?{username: string, password: string}} credentials
59 */
60 async authenticate(credentials) {
61 this._credentials = credentials;
62 await this._updateProtocolRequestInterception();
63 }
64
65 /**
66 * @param {!Object<string, string>} extraHTTPHeaders
67 */
68 async setExtraHTTPHeaders(extraHTTPHeaders) {
69 this._extraHTTPHeaders = {};
70 for (const key of Object.keys(extraHTTPHeaders)) {
71 const value = extraHTTPHeaders[key];
72 assert(helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
73 this._extraHTTPHeaders[key.toLowerCase()] = value;
74 }
75 await this._client.send('Network.setExtraHTTPHeaders', { headers: this._extraHTTPHeaders });
76 }
77
78 /**
79 * @return {!Object<string, string>}
80 */
81 extraHTTPHeaders() {
82 return Object.assign({}, this._extraHTTPHeaders);
83 }
84
85 /**
86 * @param {boolean} value
87 */
88 async setOfflineMode(value) {
89 if (this._offline === value)
90 return;
91 this._offline = value;
92 await this._client.send('Network.emulateNetworkConditions', {
93 offline: this._offline,
94 // values of 0 remove any active throttling. crbug.com/456324#c9
95 latency: 0,
96 downloadThroughput: -1,
97 uploadThroughput: -1
98 });
99 }
100
101 /**
102 * @param {string} userAgent
103 */
104 async setUserAgent(userAgent) {
105 await this._client.send('Network.setUserAgentOverride', { userAgent });
106 }
107
108 /**
109 * @param {boolean} value
110 */
111 async setRequestInterception(value) {
112 this._userRequestInterceptionEnabled = value;
113 await this._updateProtocolRequestInterception();
114 }
115
116 async _updateProtocolRequestInterception() {
117 const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
118 if (enabled === this._protocolRequestInterceptionEnabled)
119 return;
120 this._protocolRequestInterceptionEnabled = enabled;
121 const patterns = enabled ? [{urlPattern: '*'}] : [];
122 await Promise.all([
123 this._client.send('Network.setCacheDisabled', {cacheDisabled: enabled}),
124 this._client.send('Network.setRequestInterception', {patterns})
125 ]);
126 }
127
128 /**
129 * @param {!Protocol.Network.requestWillBeSentPayload} event
130 */
131 _onRequestWillBeSent(event) {
132 if (this._protocolRequestInterceptionEnabled) {
133 const requestHash = generateRequestHash(event.request);
134 const interceptionId = this._requestHashToInterceptionIds.firstValue(requestHash);
135 if (interceptionId) {
136 this._onRequest(event, interceptionId);
137 this._requestHashToInterceptionIds.delete(requestHash, interceptionId);
138 } else {
139 this._requestHashToRequestIds.set(requestHash, event.requestId);
140 this._requestIdToRequestWillBeSentEvent.set(event.requestId, event);
141 }
142 return;
143 }
144 this._onRequest(event, null);
145 }
146
147 /**
148 * @param {!Protocol.Network.requestInterceptedPayload} event
149 */
150 _onRequestIntercepted(event) {
151 if (event.authChallenge) {
152 /** @type {"Default"|"CancelAuth"|"ProvideCredentials"} */
153 let response = 'Default';
154 if (this._attemptedAuthentications.has(event.interceptionId)) {
155 response = 'CancelAuth';
156 } else if (this._credentials) {
157 response = 'ProvideCredentials';
158 this._attemptedAuthentications.add(event.interceptionId);
159 }
160 const {username, password} = this._credentials || {username: undefined, password: undefined};
161 this._client.send('Network.continueInterceptedRequest', {
162 interceptionId: event.interceptionId,
163 authChallengeResponse: { response, username, password }
164 }).catch(debugError);
165 return;
166 }
167 if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) {
168 this._client.send('Network.continueInterceptedRequest', {
169 interceptionId: event.interceptionId
170 }).catch(debugError);
171 }
172
173 const requestHash = generateRequestHash(event.request);
174 const requestId = this._requestHashToRequestIds.firstValue(requestHash);
175 if (requestId) {
176 const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(requestId);
177 this._onRequest(requestWillBeSentEvent, event.interceptionId);
178 this._requestHashToRequestIds.delete(requestHash, requestId);
179 this._requestIdToRequestWillBeSentEvent.delete(requestId);
180 } else {
181 this._requestHashToInterceptionIds.set(requestHash, event.interceptionId);
182 }
183 }
184
185 /**
186 * @param {!Protocol.Network.requestWillBeSentPayload} event
187 * @param {?string} interceptionId
188 */
189 _onRequest(event, interceptionId) {
190 let redirectChain = [];
191 if (event.redirectResponse) {
192 const request = this._requestIdToRequest.get(event.requestId);
193 // If we connect late to the target, we could have missed the requestWillBeSent event.
194 if (request) {
195 this._handleRequestRedirect(request, event.redirectResponse);
196 redirectChain = request._redirectChain;
197 }
198 }
199 const frame = event.frameId ? this._frameManager.frame(event.frameId) : null;
200 const request = new Request(this._client, frame, interceptionId, this._userRequestInterceptionEnabled, event, redirectChain);
201 this._requestIdToRequest.set(event.requestId, request);
202 this.emit(NetworkManager.Events.Request, request);
203 }
204
205
206 /**
207 * @param {!Protocol.Network.requestServedFromCachePayload} event
208 */
209 _onRequestServedFromCache(event) {
210 const request = this._requestIdToRequest.get(event.requestId);
211 if (request)
212 request._fromMemoryCache = true;
213 }
214
215 /**
216 * @param {!Request} request
217 * @param {!Protocol.Network.Response} responsePayload
218 */
219 _handleRequestRedirect(request, responsePayload) {
220 const response = new Response(this._client, request, responsePayload);
221 request._response = response;
222 request._redirectChain.push(request);
223 response._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses'));
224 this._requestIdToRequest.delete(request._requestId);
225 this._attemptedAuthentications.delete(request._interceptionId);
226 this.emit(NetworkManager.Events.Response, response);
227 this.emit(NetworkManager.Events.RequestFinished, request);
228 }
229
230 /**
231 * @param {!Protocol.Network.responseReceivedPayload} event
232 */
233 _onResponseReceived(event) {
234 const request = this._requestIdToRequest.get(event.requestId);
235 // FileUpload sends a response without a matching request.
236 if (!request)
237 return;
238 const response = new Response(this._client, request, event.response);
239 request._response = response;
240 this.emit(NetworkManager.Events.Response, response);
241 }
242
243 /**
244 * @param {!Protocol.Network.loadingFinishedPayload} event
245 */
246 _onLoadingFinished(event) {
247 const request = this._requestIdToRequest.get(event.requestId);
248 // For certain requestIds we never receive requestWillBeSent event.
249 // @see https://crbug.com/750469
250 if (!request)
251 return;
252 request.response()._bodyLoadedPromiseFulfill.call(null);
253 this._requestIdToRequest.delete(request._requestId);
254 this._attemptedAuthentications.delete(request._interceptionId);
255 this.emit(NetworkManager.Events.RequestFinished, request);
256 }
257
258 /**
259 * @param {!Protocol.Network.loadingFailedPayload} event
260 */
261 _onLoadingFailed(event) {
262 const request = this._requestIdToRequest.get(event.requestId);
263 // For certain requestIds we never receive requestWillBeSent event.
264 // @see https://crbug.com/750469
265 if (!request)
266 return;
267 request._failureText = event.errorText;
268 const response = request.response();
269 if (response)
270 response._bodyLoadedPromiseFulfill.call(null);
271 this._requestIdToRequest.delete(request._requestId);
272 this._attemptedAuthentications.delete(request._interceptionId);
273 this.emit(NetworkManager.Events.RequestFailed, request);
274 }
275}
276
277class Request {
278 /**
279 * @param {!Puppeteer.CDPSession} client
280 * @param {?Puppeteer.Frame} frame
281 * @param {string} interceptionId
282 * @param {boolean} allowInterception
283 * @param {!Protocol.Network.requestWillBeSentPayload} event
284 * @param {!Array<!Request>} redirectChain
285 */
286 constructor(client, frame, interceptionId, allowInterception, event, redirectChain) {
287 this._client = client;
288 this._requestId = event.requestId;
289 this._isNavigationRequest = event.requestId === event.loaderId && event.type === 'Document';
290 this._interceptionId = interceptionId;
291 this._allowInterception = allowInterception;
292 this._interceptionHandled = false;
293 this._response = null;
294 this._failureText = null;
295
296 this._url = event.request.url;
297 this._resourceType = event.type.toLowerCase();
298 this._method = event.request.method;
299 this._postData = event.request.postData;
300 this._headers = {};
301 this._frame = frame;
302 this._redirectChain = redirectChain;
303 for (const key of Object.keys(event.request.headers))
304 this._headers[key.toLowerCase()] = event.request.headers[key];
305
306 this._fromMemoryCache = false;
307 }
308
309 /**
310 * @return {string}
311 */
312 url() {
313 return this._url;
314 }
315
316 /**
317 * @return {string}
318 */
319 resourceType() {
320 return this._resourceType;
321 }
322
323 /**
324 * @return {string}
325 */
326 method() {
327 return this._method;
328 }
329
330 /**
331 * @return {string|undefined}
332 */
333 postData() {
334 return this._postData;
335 }
336
337 /**
338 * @return {!Object}
339 */
340 headers() {
341 return this._headers;
342 }
343
344 /**
345 * @return {?Response}
346 */
347 response() {
348 return this._response;
349 }
350
351 /**
352 * @return {?Puppeteer.Frame}
353 */
354 frame() {
355 return this._frame;
356 }
357
358 /**
359 * @return {boolean}
360 */
361 isNavigationRequest() {
362 return this._isNavigationRequest;
363 }
364
365 /**
366 * @return {!Array<!Request>}
367 */
368 redirectChain() {
369 return this._redirectChain.slice();
370 }
371
372 /**
373 * @return {?{errorText: string}}
374 */
375 failure() {
376 if (!this._failureText)
377 return null;
378 return {
379 errorText: this._failureText
380 };
381 }
382
383 /**
384 * @param {!Object=} overrides
385 */
386 async continue(overrides = {}) {
387 assert(this._allowInterception, 'Request Interception is not enabled!');
388 assert(!this._interceptionHandled, 'Request is already handled!');
389 this._interceptionHandled = true;
390 await this._client.send('Network.continueInterceptedRequest', {
391 interceptionId: this._interceptionId,
392 url: overrides.url,
393 method: overrides.method,
394 postData: overrides.postData,
395 headers: overrides.headers,
396 }).catch(error => {
397 // In certain cases, protocol will return error if the request was already canceled
398 // or the page was closed. We should tolerate these errors.
399 debugError(error);
400 });
401 }
402
403 /**
404 * @param {!{status: number, headers: Object, contentType: string, body: (string|Buffer)}} response
405 */
406 async respond(response) {
407 // Mocking responses for dataURL requests is not currently supported.
408 if (this._url.startsWith('data:'))
409 return;
410 assert(this._allowInterception, 'Request Interception is not enabled!');
411 assert(!this._interceptionHandled, 'Request is already handled!');
412 this._interceptionHandled = true;
413
414 const responseBody = response.body && helper.isString(response.body) ? Buffer.from(/** @type {string} */(response.body)) : /** @type {?Buffer} */(response.body || null);
415
416 const responseHeaders = {};
417 if (response.headers) {
418 for (const header of Object.keys(response.headers))
419 responseHeaders[header.toLowerCase()] = response.headers[header];
420 }
421 if (response.contentType)
422 responseHeaders['content-type'] = response.contentType;
423 if (responseBody && !('content-length' in responseHeaders)) {
424 // @ts-ignore
425 responseHeaders['content-length'] = Buffer.byteLength(responseBody);
426 }
427
428 const statusCode = response.status || 200;
429 const statusText = statusTexts[statusCode] || '';
430 const statusLine = `HTTP/1.1 ${statusCode} ${statusText}`;
431
432 const CRLF = '\r\n';
433 let text = statusLine + CRLF;
434 for (const header of Object.keys(responseHeaders))
435 text += header + ': ' + responseHeaders[header] + CRLF;
436 text += CRLF;
437 let responseBuffer = Buffer.from(text, 'utf8');
438 if (responseBody)
439 responseBuffer = Buffer.concat([responseBuffer, responseBody]);
440
441 await this._client.send('Network.continueInterceptedRequest', {
442 interceptionId: this._interceptionId,
443 rawResponse: responseBuffer.toString('base64')
444 }).catch(error => {
445 // In certain cases, protocol will return error if the request was already canceled
446 // or the page was closed. We should tolerate these errors.
447 debugError(error);
448 });
449 }
450
451 /**
452 * @param {string=} errorCode
453 */
454 async abort(errorCode = 'failed') {
455 const errorReason = errorReasons[errorCode];
456 assert(errorReason, 'Unknown error code: ' + errorCode);
457 assert(this._allowInterception, 'Request Interception is not enabled!');
458 assert(!this._interceptionHandled, 'Request is already handled!');
459 this._interceptionHandled = true;
460 await this._client.send('Network.continueInterceptedRequest', {
461 interceptionId: this._interceptionId,
462 errorReason
463 }).catch(error => {
464 // In certain cases, protocol will return error if the request was already canceled
465 // or the page was closed. We should tolerate these errors.
466 debugError(error);
467 });
468 }
469}
470
471const errorReasons = {
472 'aborted': 'Aborted',
473 'accessdenied': 'AccessDenied',
474 'addressunreachable': 'AddressUnreachable',
475 'blockedbyclient': 'BlockedByClient',
476 'blockedbyresponse': 'BlockedByResponse',
477 'connectionaborted': 'ConnectionAborted',
478 'connectionclosed': 'ConnectionClosed',
479 'connectionfailed': 'ConnectionFailed',
480 'connectionrefused': 'ConnectionRefused',
481 'connectionreset': 'ConnectionReset',
482 'internetdisconnected': 'InternetDisconnected',
483 'namenotresolved': 'NameNotResolved',
484 'timedout': 'TimedOut',
485 'failed': 'Failed',
486};
487
488helper.tracePublicAPI(Request);
489
490class Response {
491 /**
492 * @param {!Puppeteer.CDPSession} client
493 * @param {!Request} request
494 * @param {!Protocol.Network.Response} responsePayload
495 */
496 constructor(client, request, responsePayload) {
497 this._client = client;
498 this._request = request;
499 this._contentPromise = null;
500
501 this._bodyLoadedPromise = new Promise(fulfill => {
502 this._bodyLoadedPromiseFulfill = fulfill;
503 });
504
505 this._remoteAddress = {
506 ip: responsePayload.remoteIPAddress,
507 port: responsePayload.remotePort,
508 };
509 this._status = responsePayload.status;
510 this._statusText = responsePayload.statusText;
511 this._url = request.url();
512 this._fromDiskCache = !!responsePayload.fromDiskCache;
513 this._fromServiceWorker = !!responsePayload.fromServiceWorker;
514 this._headers = {};
515 for (const key of Object.keys(responsePayload.headers))
516 this._headers[key.toLowerCase()] = responsePayload.headers[key];
517 this._securityDetails = responsePayload.securityDetails ? new SecurityDetails(responsePayload.securityDetails) : null;
518 }
519
520 /**
521 * @return {{ip: string, port: number}}
522 */
523 remoteAddress() {
524 return this._remoteAddress;
525 }
526
527 /**
528 * @return {string}
529 */
530 url() {
531 return this._url;
532 }
533
534 /**
535 * @return {boolean}
536 */
537 ok() {
538 return this._status === 0 || (this._status >= 200 && this._status <= 299);
539 }
540
541 /**
542 * @return {number}
543 */
544 status() {
545 return this._status;
546 }
547
548 /**
549 * @return {string}
550 */
551 statusText() {
552 return this._statusText;
553 }
554
555 /**
556 * @return {!Object}
557 */
558 headers() {
559 return this._headers;
560 }
561
562 /**
563 * @return {?SecurityDetails}
564 */
565 securityDetails() {
566 return this._securityDetails;
567 }
568
569 /**
570 * @return {!Promise<!Buffer>}
571 */
572 buffer() {
573 if (!this._contentPromise) {
574 this._contentPromise = this._bodyLoadedPromise.then(async error => {
575 if (error)
576 throw error;
577 const response = await this._client.send('Network.getResponseBody', {
578 requestId: this._request._requestId
579 });
580 return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
581 });
582 }
583 return this._contentPromise;
584 }
585
586 /**
587 * @return {!Promise<string>}
588 */
589 async text() {
590 const content = await this.buffer();
591 return content.toString('utf8');
592 }
593
594 /**
595 * @return {!Promise<!Object>}
596 */
597 async json() {
598 const content = await this.text();
599 return JSON.parse(content);
600 }
601
602 /**
603 * @return {!Request}
604 */
605 request() {
606 return this._request;
607 }
608
609 /**
610 * @return {boolean}
611 */
612 fromCache() {
613 return this._fromDiskCache || this._request._fromMemoryCache;
614 }
615
616 /**
617 * @return {boolean}
618 */
619 fromServiceWorker() {
620 return this._fromServiceWorker;
621 }
622}
623helper.tracePublicAPI(Response);
624
625/**
626 * @param {!Protocol.Network.Request} request
627 * @return {string}
628 */
629function generateRequestHash(request) {
630 let normalizedURL = request.url;
631 try {
632 // Decoding is necessary to normalize URLs. @see crbug.com/759388
633 // The method will throw if the URL is malformed. In this case,
634 // consider URL to be normalized as-is.
635 normalizedURL = decodeURI(request.url);
636 } catch (e) {
637 }
638 const hash = {
639 url: normalizedURL,
640 method: request.method,
641 postData: request.postData,
642 headers: {},
643 };
644
645 if (!normalizedURL.startsWith('data:')) {
646 const headers = Object.keys(request.headers);
647 headers.sort();
648 for (let header of headers) {
649 const headerValue = request.headers[header];
650 header = header.toLowerCase();
651 if (header === 'accept' || header === 'referer' || header === 'x-devtools-emulate-network-conditions-client-id' || header === 'cookie')
652 continue;
653 hash.headers[header] = headerValue;
654 }
655 }
656 return JSON.stringify(hash);
657}
658
659class SecurityDetails {
660 /**
661 * @param {!Protocol.Network.SecurityDetails} securityPayload
662 */
663 constructor(securityPayload) {
664 this._subjectName = securityPayload['subjectName'];
665 this._issuer = securityPayload['issuer'];
666 this._validFrom = securityPayload['validFrom'];
667 this._validTo = securityPayload['validTo'];
668 this._protocol = securityPayload['protocol'];
669 }
670
671 /**
672 * @return {string}
673 */
674 subjectName() {
675 return this._subjectName;
676 }
677
678 /**
679 * @return {string}
680 */
681 issuer() {
682 return this._issuer;
683 }
684
685 /**
686 * @return {number}
687 */
688 validFrom() {
689 return this._validFrom;
690 }
691
692 /**
693 * @return {number}
694 */
695 validTo() {
696 return this._validTo;
697 }
698
699 /**
700 * @return {string}
701 */
702 protocol() {
703 return this._protocol;
704 }
705}
706
707NetworkManager.Events = {
708 Request: 'request',
709 Response: 'response',
710 RequestFailed: 'requestfailed',
711 RequestFinished: 'requestfinished',
712};
713
714const statusTexts = {
715 '100': 'Continue',
716 '101': 'Switching Protocols',
717 '102': 'Processing',
718 '200': 'OK',
719 '201': 'Created',
720 '202': 'Accepted',
721 '203': 'Non-Authoritative Information',
722 '204': 'No Content',
723 '206': 'Partial Content',
724 '207': 'Multi-Status',
725 '208': 'Already Reported',
726 '209': 'IM Used',
727 '300': 'Multiple Choices',
728 '301': 'Moved Permanently',
729 '302': 'Found',
730 '303': 'See Other',
731 '304': 'Not Modified',
732 '305': 'Use Proxy',
733 '306': 'Switch Proxy',
734 '307': 'Temporary Redirect',
735 '308': 'Permanent Redirect',
736 '400': 'Bad Request',
737 '401': 'Unauthorized',
738 '402': 'Payment Required',
739 '403': 'Forbidden',
740 '404': 'Not Found',
741 '405': 'Method Not Allowed',
742 '406': 'Not Acceptable',
743 '407': 'Proxy Authentication Required',
744 '408': 'Request Timeout',
745 '409': 'Conflict',
746 '410': 'Gone',
747 '411': 'Length Required',
748 '412': 'Precondition Failed',
749 '413': 'Payload Too Large',
750 '414': 'URI Too Long',
751 '415': 'Unsupported Media Type',
752 '416': 'Range Not Satisfiable',
753 '417': 'Expectation Failed',
754 '418': 'I\'m a teapot',
755 '421': 'Misdirected Request',
756 '422': 'Unprocessable Entity',
757 '423': 'Locked',
758 '424': 'Failed Dependency',
759 '426': 'Upgrade Required',
760 '428': 'Precondition Required',
761 '429': 'Too Many Requests',
762 '431': 'Request Header Fields Too Large',
763 '451': 'Unavailable For Legal Reasons',
764 '500': 'Internal Server Error',
765 '501': 'Not Implemented',
766 '502': 'Bad Gateway',
767 '503': 'Service Unavailable',
768 '504': 'Gateway Timeout',
769 '505': 'HTTP Version Not Supported',
770 '506': 'Variant Also Negotiates',
771 '507': 'Insufficient Storage',
772 '508': 'Loop Detected',
773 '510': 'Not Extended',
774 '511': 'Network Authentication Required',
775};
776
777module.exports = {Request, Response, NetworkManager};