UNPKG

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