UNPKG

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