import { Logger } from '../utils' ; import { NetworkRequest } from './NetworkRequest' ; import { ChromeRemoteInterfaceEvent , CRIConnection } from '../cdp' ; import { ExtraInfoBuilder } from './ExtraInfoBuilder' ; import { RecordOptions } from '../Plugin' ; import { Header } from 'har-format' ; import { Network , Security } from 'chrome-remote-interface' ; import Protocol from 'devtools-protocol' ; export class NetworkObserver { private readonly _entries : Map < Protocol . Network . RequestId , NetworkRequest > ; private readonly _extraInfoBuilders : Map < Protocol . Network . RequestId , ExtraInfoBuilder > ; private readonly network : Network ; private destination : ( chromeEntry : NetworkRequest ) => void ; private readonly security : Security ; constructor ( private readonly options , private readonly connection , private readonly logger ) { this . _entries = new Map < Protocol . Network . RequestId , NetworkRequest > ( ) ; this . _extraInfoBuilders = new Map < Protocol . Network . RequestId , ExtraInfoBuilder > ( ) ; const { network : network , security : security } = this . connection ; this . network = network ; this . security = security ; } public async subscribe ( callback ) { this . destination = ( entry ) : void => callback ( entry ) ; await this . connection . subscribe ( ( event ) : void => this . handleEvent ( event ) ) ; this . security . certificateError ( ( { eventId } ) : Promise < void > => this . security . handleCertificateError ( { eventId , action : 'continue' } ) ) ; await Promise . all ( [ this . security . setOverrideCertificateErrors ( { override : true } ) , this . network . setCacheDisabled ( { cacheDisabled : true } ) , this . network . setBypassServiceWorker ( { bypass : true } ) ] ) ; } public signedExchangeReceived ( params ) { const entry = this . _entries . get ( params . requestId ) ; if ( ! entry ) { return ; } entry . signedExchangeInfo = params . info ; entry . resourceType = 'SignedExchange' ; this . updateNetworkRequestWithResponse ( entry , params . info . outerResponse ) ; } public requestWillBeSent ( { type , loaderId , initiator , redirectResponse , documentURL , frameId , timestamp , requestId , request , wallTime } ) { let entry = this . _entries . get ( requestId ) ; if ( entry ) { if ( ! redirectResponse ) { return ; } if ( ! entry . signedExchangeInfo ) { this . responseReceived ( { requestId , loaderId , timestamp , type : 'Other' , response : redirectResponse , frameId } ) ; } entry = this . _appendRedirect ( requestId , timestamp , request . url ) ; } else { entry = this . createRequest ( requestId , frameId , loaderId , request . url , documentURL , initiator ) ; } this . updateNetworkRequestWithRequest ( entry , request ) ; entry . setIssueTime ( timestamp , wallTime ) ; entry . resourceType = type ?? 'Other' ; this . getExtraInfoBuilder ( requestId ) . addRequest ( entry ) ; this . startRequest ( entry ) ; } public dataReceived ( { requestId , dataLength , encodedDataLength , timestamp } ) { const entry = this . _entries . get ( requestId ) ; if ( ! entry ) { return ; } entry . resourceSize += dataLength ; if ( encodedDataLength !== - 1 ) { entry . increaseTransferSize ( encodedDataLength ) ; } entry . endTime = timestamp ; } public responseReceived ( { requestId , response , timestamp , type } ) { const entry = this . _entries . get ( requestId ) ; if ( ! entry ) { return ; } entry . responseReceivedTime = timestamp ; entry . resourceType = type ; this . updateNetworkRequestWithResponse ( entry , response ) ; } public resourceChangedPriority ( { requestId , newPriority } ) { const entry = this . _entries . get ( requestId ) ; if ( ! entry ) { return ; } entry . priority = newPriority ; } public async loadingFinished ( { requestId , timestamp , encodedDataLength } ) { const entry = this . _entries . get ( requestId ) ; if ( ! entry ) { return ; } this . finishRequest ( entry , timestamp , encodedDataLength ) ; } public loadingFailed ( { requestId , errorText , canceled , type , timestamp } ) { const entry = this . _entries . get ( requestId ) ; if ( ! entry ) { return ; } entry . resourceType = type ; this . finishRequest ( entry , timestamp , - 1 ) ; const message = errorText || ( canceled && 'Canceled' ) ; this . logger . debug ( `Failed request: ${ requestId }. Reason: ${ message }` ) ; } public webSocketCreated ( { initiator , requestId , url } ) { const entry = this . createRequest ( requestId , '' , '' , url , '' , initiator ) ; this . startRequest ( entry ) ; } public webSocketWillSendHandshakeRequest ( { request , requestId , timestamp , wallTime } ) { const entry = this . _entries . get ( requestId ) ; if ( ! entry ) { return ; } entry . requestMethod = 'GET' ; entry . requestHeaders = this . headersMapToHeadersArray ( request . headers ) ; entry . setIssueTime ( timestamp , wallTime ) ; } public webSocketHandshakeResponseReceived ( { timestamp , response , requestId } ) { const entry = this . _entries . get ( requestId ) ; if ( ! entry ) { return ; } entry . statusCode = response . status ; entry . statusText = response . statusText ; entry . responseHeaders = this . headersMapToHeadersArray ( response . headers ) ; entry . responseHeadersText = response . headersText || '' ; if ( response . requestHeaders ) { entry . requestHeaders = this . headersMapToHeadersArray ( response . requestHeaders ) ; } if ( response . requestHeadersText ) { entry . requestHeadersText = response . requestHeadersText ; } entry . responseReceivedTime = timestamp ; entry . protocol = 'websocket' ; } public webSocketFrameSent ( { requestId , timestamp , response } ) { const entry = this . _entries . get ( requestId ) ; if ( ! entry ) { return ; } entry . addProtocolFrame ( response , timestamp , true ) ; entry . responseReceivedTime = timestamp ; } public webSocketFrameReceived ( { requestId , timestamp , response } ) { const entry = this . _entries . get ( requestId ) ; if ( ! entry ) { return ; } entry . addProtocolFrame ( response , timestamp , false ) ; entry . responseReceivedTime = timestamp ; } public webSocketFrameError ( { errorMessage , requestId , timestamp } ) { const entry = this . _entries . get ( requestId ) ; if ( ! entry ) { return ; } entry . addProtocolFrameError ( errorMessage , timestamp ) ; entry . responseReceivedTime = timestamp ; } public webSocketClosed ( { requestId , timestamp } ) { const entry = this . _entries . get ( requestId ) ; if ( ! entry ) { return ; } this . finishRequest ( entry , timestamp , - 1 ) ; } public requestWillBeSentExtraInfo ( { requestId , headers } ) { this . getExtraInfoBuilder ( requestId ) . addRequestExtraInfo ( { requestHeaders : this . headersMapToHeadersArray ( headers ) } ) ; } public responseReceivedExtraInfo ( { requestId , headers , headersText } ) { this . getExtraInfoBuilder ( requestId ) . addResponseExtraInfo ( { responseHeaders : this . headersMapToHeadersArray ( headers ) , responseHeadersText : headersText } ) ; } private getExtraInfoBuilder ( requestId ) { if ( ! this . _extraInfoBuilders . has ( requestId ) ) { this . _extraInfoBuilders . set ( requestId , new ExtraInfoBuilder ( ( ) : void => { this . _extraInfoBuilders . delete ( requestId ) ; } ) ) ; } return this . _extraInfoBuilders . get ( requestId ) ; } private _appendRedirect ( requestId , time , redirectURL ) { const originalNetworkRequest = this . _entries . get ( requestId ) as NetworkRequest ; let redirectCount = 0 ; let redirect = originalNetworkRequest . redirectSource ; while ( redirect ) { redirectCount ++ ; redirect = redirect . redirectSource ; } originalNetworkRequest . markAsRedirect ( redirectCount ) ; this . finishRequest ( originalNetworkRequest , time , - 1 ) ; const newNetworkRequest = this . createRequest ( requestId , originalNetworkRequest . frameId , originalNetworkRequest . loaderId , redirectURL , originalNetworkRequest . documentURL , originalNetworkRequest . initiator ) ; newNetworkRequest . redirectSource = originalNetworkRequest ; return newNetworkRequest ; } private finishRequest ( networkRequest , finishTime , encodedDataLength ) { networkRequest . endTime = finishTime ; if ( encodedDataLength >= 0 ) { const redirectSource = networkRequest . redirectSource ; if ( redirectSource ?. signedExchangeInfo ) { networkRequest . transferSize = 0 ; redirectSource . transferSize = encodedDataLength ; } else { networkRequest . transferSize = encodedDataLength ; } } if ( this . options . content ) { networkRequest . setContentData ( this . network . getResponseBody ( { requestId : networkRequest . requestId } ) ) ; } this . _entries . delete ( networkRequest . requestId ) ; this . getExtraInfoBuilder ( networkRequest . requestId ) . finished ( ) ; if ( ! this . excludeRequest ( networkRequest ) ) { this . destination ( networkRequest ) ; } } private startRequest ( networkRequest ) { this . _entries . set ( networkRequest . requestId , networkRequest ) ; } private updateNetworkRequestWithRequest ( chromeRequest , request ) { chromeRequest . requestMethod = request . method ; chromeRequest . requestHeaders = this . headersMapToHeadersArray ( request . headers ) ; chromeRequest . setRequestFormData ( this . getRequestPostData ( chromeRequest . requestId , request ) ) ; chromeRequest . initialPriority = request . initialPriority ; } private getRequestPostData ( requestId , request ) { return request . hasPostData && ! ! request . postData ? request . postData : this . network . getRequestPostData ( { requestId } ) . then ( ( { postData } ) : string => postData ) . catch ( ( ) : string => undefined ) ; } private createRequest ( requestId , frameId , loaderId , url , documentURL , initiator ? ) { return new NetworkRequest ( requestId , url , documentURL , frameId , loaderId , initiator ) ; } private updateNetworkRequestWithResponse ( networkRequest , response ) { if ( response . url && networkRequest . url !== response . url ) { networkRequest . setUrl ( response . url ) ; } networkRequest . mimeType = response . mimeType ; networkRequest . statusCode = response . status ; networkRequest . statusText = response . statusText ; if ( ! networkRequest . hasExtraResponseInfo ) { networkRequest . responseHeaders = this . headersMapToHeadersArray ( response . headers ) ; } if ( response . encodedDataLength >= 0 ) { networkRequest . transferSize = response . encodedDataLength ; } if ( response . requestHeaders && ! networkRequest . hasExtraRequestInfo ) { networkRequest . requestHeaders = this . headersMapToHeadersArray ( response . requestHeaders ) ; networkRequest . requestHeadersText = response . requestHeadersText ?? '' ; } networkRequest . connectionReused = response . connectionReused ; networkRequest . connectionId = String ( response . connectionId ) ; if ( response . remoteIPAddress ) { networkRequest . setRemoteAddress ( response . remoteIPAddress , response . remotePort || - 1 ) ; } networkRequest . timing = response . timing ; networkRequest . protocol = response . protocol ?? '' ; } private headersMapToHeadersArray ( headersMap ) { return Object . keys ( headersMap ) . reduce ( ( acc , name ) : Header [ ] => { const values = headersMap [ name ] . split ( '\n' ) ; acc . push ( ... values . map ( ( value ) : Header => ( { name , value } ) ) ) ; return acc ; } , [ ] ) ; } private excludeRequest ( request ) { const { path = '/' } = request . parsedURL ; return ! ! this . options . excludePaths ?. some ( ( excludedPath ) : boolean => new RegExp ( excludedPath ) . test ( path ) ) ; } private handleEvent ( { method , params } ) { const methodName = method . substring ( method . indexOf ( '.' ) + 1 ) ; const handler = this [ methodName ] ; if ( handler ) { handler . call ( this , params ) ; } } }