UNPKG

23.2 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 responseHeaders['content-length'] = Buffer.byteLength(responseBody);
435
436 const statusCode = response.status || 200;
437 const statusText = statusTexts[statusCode] || '';
438 const statusLine = `HTTP/1.1 ${statusCode} ${statusText}`;
439
440 const CRLF = '\r\n';
441 let text = statusLine + CRLF;
442 for (const header of Object.keys(responseHeaders))
443 text += header + ': ' + responseHeaders[header] + CRLF;
444 text += CRLF;
445 let responseBuffer = Buffer.from(text, 'utf8');
446 if (responseBody)
447 responseBuffer = Buffer.concat([responseBuffer, responseBody]);
448
449 await this._client.send('Network.continueInterceptedRequest', {
450 interceptionId: this._interceptionId,
451 rawResponse: responseBuffer.toString('base64')
452 }).catch(error => {
453 // In certain cases, protocol will return error if the request was already canceled
454 // or the page was closed. We should tolerate these errors.
455 debugError(error);
456 });
457 }
458
459 /**
460 * @param {string=} errorCode
461 */
462 async abort(errorCode = 'failed') {
463 const errorReason = errorReasons[errorCode];
464 assert(errorReason, 'Unknown error code: ' + errorCode);
465 assert(this._allowInterception, 'Request Interception is not enabled!');
466 assert(!this._interceptionHandled, 'Request is already handled!');
467 this._interceptionHandled = true;
468 await this._client.send('Network.continueInterceptedRequest', {
469 interceptionId: this._interceptionId,
470 errorReason
471 }).catch(error => {
472 // In certain cases, protocol will return error if the request was already canceled
473 // or the page was closed. We should tolerate these errors.
474 debugError(error);
475 });
476 }
477}
478
479const errorReasons = {
480 'aborted': 'Aborted',
481 'accessdenied': 'AccessDenied',
482 'addressunreachable': 'AddressUnreachable',
483 'blockedbyclient': 'BlockedByClient',
484 'blockedbyresponse': 'BlockedByResponse',
485 'connectionaborted': 'ConnectionAborted',
486 'connectionclosed': 'ConnectionClosed',
487 'connectionfailed': 'ConnectionFailed',
488 'connectionrefused': 'ConnectionRefused',
489 'connectionreset': 'ConnectionReset',
490 'internetdisconnected': 'InternetDisconnected',
491 'namenotresolved': 'NameNotResolved',
492 'timedout': 'TimedOut',
493 'failed': 'Failed',
494};
495
496helper.tracePublicAPI(Request);
497
498class Response {
499 /**
500 * @param {!Puppeteer.CDPSession} client
501 * @param {!Request} request
502 * @param {!Protocol.Network.Response} responsePayload
503 */
504 constructor(client, request, responsePayload) {
505 this._client = client;
506 this._request = request;
507 this._contentPromise = null;
508
509 this._bodyLoadedPromise = new Promise(fulfill => {
510 this._bodyLoadedPromiseFulfill = fulfill;
511 });
512
513 this._remoteAddress = {
514 ip: responsePayload.remoteIPAddress,
515 port: responsePayload.remotePort,
516 };
517 this._status = responsePayload.status;
518 this._statusText = responsePayload.statusText;
519 this._url = request.url();
520 this._fromDiskCache = !!responsePayload.fromDiskCache;
521 this._fromServiceWorker = !!responsePayload.fromServiceWorker;
522 this._headers = {};
523 for (const key of Object.keys(responsePayload.headers))
524 this._headers[key.toLowerCase()] = responsePayload.headers[key];
525 this._securityDetails = responsePayload.securityDetails ? new SecurityDetails(responsePayload.securityDetails) : null;
526 }
527
528 /**
529 * @return {{ip: string, port: number}}
530 */
531 remoteAddress() {
532 return this._remoteAddress;
533 }
534
535 /**
536 * @return {string}
537 */
538 url() {
539 return this._url;
540 }
541
542 /**
543 * @return {boolean}
544 */
545 ok() {
546 return this._status === 0 || (this._status >= 200 && this._status <= 299);
547 }
548
549 /**
550 * @return {number}
551 */
552 status() {
553 return this._status;
554 }
555
556 /**
557 * @return {string}
558 */
559 statusText() {
560 return this._statusText;
561 }
562
563 /**
564 * @return {!Object}
565 */
566 headers() {
567 return this._headers;
568 }
569
570 /**
571 * @return {?SecurityDetails}
572 */
573 securityDetails() {
574 return this._securityDetails;
575 }
576
577 /**
578 * @return {!Promise<!Buffer>}
579 */
580 buffer() {
581 if (!this._contentPromise) {
582 this._contentPromise = this._bodyLoadedPromise.then(async error => {
583 if (error)
584 throw error;
585 const response = await this._client.send('Network.getResponseBody', {
586 requestId: this._request._requestId
587 });
588 return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
589 });
590 }
591 return this._contentPromise;
592 }
593
594 /**
595 * @return {!Promise<string>}
596 */
597 async text() {
598 const content = await this.buffer();
599 return content.toString('utf8');
600 }
601
602 /**
603 * @return {!Promise<!Object>}
604 */
605 async json() {
606 const content = await this.text();
607 return JSON.parse(content);
608 }
609
610 /**
611 * @return {!Request}
612 */
613 request() {
614 return this._request;
615 }
616
617 /**
618 * @return {boolean}
619 */
620 fromCache() {
621 return this._fromDiskCache || this._request._fromMemoryCache;
622 }
623
624 /**
625 * @return {boolean}
626 */
627 fromServiceWorker() {
628 return this._fromServiceWorker;
629 }
630
631 /**
632 * @return {?Puppeteer.Frame}
633 */
634 frame() {
635 return this._request.frame();
636 }
637}
638helper.tracePublicAPI(Response);
639
640/**
641 * @param {!Protocol.Network.Request} request
642 * @return {string}
643 */
644function generateRequestHash(request) {
645 let normalizedURL = request.url;
646 try {
647 // Decoding is necessary to normalize URLs. @see crbug.com/759388
648 // The method will throw if the URL is malformed. In this case,
649 // consider URL to be normalized as-is.
650 normalizedURL = decodeURI(request.url);
651 } catch (e) {
652 }
653 const hash = {
654 url: normalizedURL,
655 method: request.method,
656 postData: request.postData,
657 headers: {},
658 };
659
660 if (!normalizedURL.startsWith('data:')) {
661 const headers = Object.keys(request.headers);
662 headers.sort();
663 for (let header of headers) {
664 const headerValue = request.headers[header];
665 header = header.toLowerCase();
666 if (header === 'accept' || header === 'referer' || header === 'x-devtools-emulate-network-conditions-client-id' || header === 'cookie')
667 continue;
668 hash.headers[header] = headerValue;
669 }
670 }
671 return JSON.stringify(hash);
672}
673
674class SecurityDetails {
675 /**
676 * @param {!Protocol.Network.SecurityDetails} securityPayload
677 */
678 constructor(securityPayload) {
679 this._subjectName = securityPayload['subjectName'];
680 this._issuer = securityPayload['issuer'];
681 this._validFrom = securityPayload['validFrom'];
682 this._validTo = securityPayload['validTo'];
683 this._protocol = securityPayload['protocol'];
684 }
685
686 /**
687 * @return {string}
688 */
689 subjectName() {
690 return this._subjectName;
691 }
692
693 /**
694 * @return {string}
695 */
696 issuer() {
697 return this._issuer;
698 }
699
700 /**
701 * @return {number}
702 */
703 validFrom() {
704 return this._validFrom;
705 }
706
707 /**
708 * @return {number}
709 */
710 validTo() {
711 return this._validTo;
712 }
713
714 /**
715 * @return {string}
716 */
717 protocol() {
718 return this._protocol;
719 }
720}
721
722NetworkManager.Events = {
723 Request: 'request',
724 Response: 'response',
725 RequestFailed: 'requestfailed',
726 RequestFinished: 'requestfinished',
727};
728
729const statusTexts = {
730 '100': 'Continue',
731 '101': 'Switching Protocols',
732 '102': 'Processing',
733 '200': 'OK',
734 '201': 'Created',
735 '202': 'Accepted',
736 '203': 'Non-Authoritative Information',
737 '204': 'No Content',
738 '206': 'Partial Content',
739 '207': 'Multi-Status',
740 '208': 'Already Reported',
741 '209': 'IM Used',
742 '300': 'Multiple Choices',
743 '301': 'Moved Permanently',
744 '302': 'Found',
745 '303': 'See Other',
746 '304': 'Not Modified',
747 '305': 'Use Proxy',
748 '306': 'Switch Proxy',
749 '307': 'Temporary Redirect',
750 '308': 'Permanent Redirect',
751 '400': 'Bad Request',
752 '401': 'Unauthorized',
753 '402': 'Payment Required',
754 '403': 'Forbidden',
755 '404': 'Not Found',
756 '405': 'Method Not Allowed',
757 '406': 'Not Acceptable',
758 '407': 'Proxy Authentication Required',
759 '408': 'Request Timeout',
760 '409': 'Conflict',
761 '410': 'Gone',
762 '411': 'Length Required',
763 '412': 'Precondition Failed',
764 '413': 'Payload Too Large',
765 '414': 'URI Too Long',
766 '415': 'Unsupported Media Type',
767 '416': 'Range Not Satisfiable',
768 '417': 'Expectation Failed',
769 '418': 'I\'m a teapot',
770 '421': 'Misdirected Request',
771 '422': 'Unprocessable Entity',
772 '423': 'Locked',
773 '424': 'Failed Dependency',
774 '426': 'Upgrade Required',
775 '428': 'Precondition Required',
776 '429': 'Too Many Requests',
777 '431': 'Request Header Fields Too Large',
778 '451': 'Unavailable For Legal Reasons',
779 '500': 'Internal Server Error',
780 '501': 'Not Implemented',
781 '502': 'Bad Gateway',
782 '503': 'Service Unavailable',
783 '504': 'Gateway Timeout',
784 '505': 'HTTP Version Not Supported',
785 '506': 'Variant Also Negotiates',
786 '507': 'Insufficient Storage',
787 '508': 'Loop Detected',
788 '510': 'Not Extended',
789 '511': 'Network Authentication Required',
790};
791
792module.exports = {Request, Response, NetworkManager};