import { CookieParser } from './CookieParser' ; import { NetworkCookie } from './NetworkCookie' ; import { RequestExtraInfo , ResponseExtraInfo } from './ExtraInfoBuilder' ; import { Header , Param , QueryString } from 'har-format' ; import Protocol from 'devtools-protocol' ; import { parse as parseUrl , UrlWithStringQuery } from 'url' ; export interface ContentData { error ? : string ; text ? : string ; encoding ? : string ; } export enum WebSocketFrameType { REQUEST = 'request' , RESPONSE = 'response' , ERROR = 'error' } export interface WebSocket { type : WebSocketFrameType ; data : string ; time : Protocol . Network . MonotonicTime ; opcode : number ; mask : boolean ; } export class NetworkRequest { private _contentData ? : Promise < ContentData > ; private _wallIssueTime : Protocol . Network . TimeSinceEpoch = - 1 ; private _requestHeaderValues : Map < string , string > = new Map < string , string > ( ) ; private _responseHeaderValues : Map < string , string > = new Map < string , string > ( ) ; private _parsedQueryParameters ? : QueryString [ ] ; private _currentPriority ? : Protocol . Network . ResourcePriority ; private _requestFormData : Promise < string | undefined > = Promise . resolve ( undefined ) ; private _formParametersPromise ? : Promise < Param [ ] > ; private _signedExchangeInfo ? : Protocol . Network . SignedExchangeInfo ; set signedExchangeInfo ( info ) { this . _signedExchangeInfo = info ; } private _hasExtraResponseInfo : boolean = false ; get hasExtraResponseInfo ( ) : boolean { return this . _hasExtraResponseInfo ; } set hasExtraResponseInfo ( value ) { this . _hasExtraResponseInfo = value ; } private _hasExtraRequestInfo : boolean = false ; get hasExtraRequestInfo ( ) : boolean { return this . _hasExtraRequestInfo ; } set hasExtraRequestInfo ( value ) { this . _hasExtraRequestInfo = value ; } private _connectionId ? : string = '0' ; get connectionId ( ) : string | undefined { return this . _connectionId ; } set connectionId ( value ) { this . _connectionId = value ; } private _protocol : string = '' ; get protocol ( ) : string { return this . _protocol ; } set protocol ( value ) { this . _protocol = value ?? '' ; } private _requestTime : number = 0 ; get requestTime ( ) : number { return this . _requestTime ; } set requestTime ( value ) { this . _requestTime = value ?? 0 ; } private _requestMethod : string = '' ; get requestMethod ( ) : string { return this . _requestMethod ; } set requestMethod ( value ) { this . _requestMethod = value ?? '' ; } private _statusText : string = '' ; get statusText ( ) : string { return this . _statusText ; } set statusText ( value ) { this . _statusText = value ?? '' ; } private _parsedURL ? : UrlWithStringQuery ; get parsedURL ( ) : UrlWithStringQuery { return this . _parsedURL ; } private _url ? : string ; get url ( ) : string | undefined { return this . _url ; } private _remoteAddress : string = '' ; get remoteAddress ( ) : string { return this . _remoteAddress ; } private _startTime : Protocol . Network . MonotonicTime = - 1 ; get startTime ( ) : Protocol . Network . MonotonicTime { return this . _startTime || - 1 ; } private _issueTime : Protocol . Network . MonotonicTime = - 1 ; get issueTime ( ) : Protocol . Network . MonotonicTime { return this . _issueTime ; } private _endTime : number = - 1 ; get endTime ( ) : number { return this . _endTime || - 1 ; } set endTime ( x ) { if ( this . timing && this . timing . requestTime ) { this . _endTime = Math . max ( x , this . responseReceivedTime ) ; } else { this . _endTime = x ; if ( this . _responseReceivedTime > x ) { this . _responseReceivedTime = x ; } } } private _responseReceivedTime : number = - 1 ; get responseReceivedTime ( ) : number { return this . _responseReceivedTime || - 1 ; } set responseReceivedTime ( value ) { this . _responseReceivedTime = value ; } private _resourceSize : number = 0 ; get resourceSize ( ) : number { return this . _resourceSize || 0 ; } set resourceSize ( value ) { this . _resourceSize = value ?? 0 ; } private _transferSize : number = 0 ; get transferSize ( ) : number { return this . _transferSize || 0 ; } set transferSize ( value ) { this . _transferSize = value ?? 0 ; } private _timing ? : Protocol . Network . ResourceTiming ; get timing ( ) : Protocol . Network . ResourceTiming | undefined { return this . _timing ; } set timing ( timingInfo ) { if ( ! timingInfo ) { return ; } this . _startTime = timingInfo . requestTime ; const headersReceivedTime = timingInfo . requestTime + timingInfo . receiveHeadersEnd / 1000.0 ; if ( ( this . _responseReceivedTime || - 1 ) < 0 || this . _responseReceivedTime > headersReceivedTime ) { this . _responseReceivedTime = headersReceivedTime ; } if ( this . _startTime > this . _responseReceivedTime ) { this . _responseReceivedTime = this . _startTime ; } this . _timing = timingInfo ; } private _mimeType ? : string ; get mimeType ( ) : string | undefined { return this . _mimeType ; } set mimeType ( value ) { this . _mimeType = value ; } private _resourceType : Protocol . Network . ResourceType = 'Other' ; get resourceType ( ) : Protocol . Network . ResourceType { return this . _resourceType ; } set resourceType ( resourceType ) { this . _resourceType = resourceType ?? 'Other' ; } private _redirectSource ? : NetworkRequest ; get redirectSource ( ) : NetworkRequest | undefined { return this . _redirectSource ; } set redirectSource ( originatingRequest ) { this . _redirectSource = originatingRequest ; } private _requestHeaders : Header [ ] = [ ] ; get requestHeaders ( ) : Header [ ] { return this . _requestHeaders ; } set requestHeaders ( headers ) { this . _requestHeaders = headers ; this . _requestHeaderValues . clear ( ) ; delete this . _requestCookies ; } private _requestCookies ? : NetworkCookie [ ] ; get requestCookies ( ) : NetworkCookie [ ] | undefined { if ( ! this . _requestCookies ) { const cookie = this . requestHeaderValue ( 'Cookie' ) ; this . _requestCookies = new CookieParser ( ) . parseCookie ( cookie ) ; } return this . _requestCookies ; } get contentLength ( ) : number { const contentLength = this . requestHeaderValue ( 'Content-Length' ) ; return isNaN ( + contentLength ) ? 0 : parseInt ( contentLength , 10 ) ; } private _requestHeadersText : string = '' ; get requestHeadersText ( ) : string { return this . _requestHeadersText ; } set requestHeadersText ( text ) { this . _requestHeadersText = text ; } private _connectionReused : boolean = false ; get connectionReused ( ) : boolean { return this . _connectionReused ; } set connectionReused ( value ) { this . _connectionReused = value ; } private _responseHeaders : Header [ ] = [ ] ; get responseHeaders ( ) : Header [ ] { return this . _responseHeaders || [ ] ; } set responseHeaders ( value ) { this . _responseHeaders = value ; delete this . _responseCookies ; this . _responseHeaderValues . clear ( ) ; } private _responseHeadersText : string = '' ; get responseHeadersText ( ) : string { return this . _responseHeadersText ; } set responseHeadersText ( value ) { this . _responseHeadersText = value ; } private _responseCookies ? : NetworkCookie [ ] ; get responseCookies ( ) : NetworkCookie [ ] | undefined { if ( ! this . _responseCookies ) { const cookie = this . responseHeaderValue ( 'Set-Cookie' ) ; this . _requestCookies = new CookieParser ( ) . parseSetCookie ( cookie ) ; } return this . _responseCookies ; } private _queryString ? : string ; get queryString ( ) : string { if ( this . _queryString !== undefined ) { return this . _queryString ; } let queryString = null ; const questionMarkPosition = this . url . indexOf ( '?' ) ; if ( questionMarkPosition !== - 1 ) { queryString = this . url . substring ( questionMarkPosition + 1 ) ; const hashSignPosition = queryString . indexOf ( '#' ) ; if ( hashSignPosition !== - 1 ) { queryString = queryString . substring ( 0 , hashSignPosition ) ; } } this . _queryString = queryString ; return this . _queryString ; } private _initialPriority ? : Protocol . Network . ResourcePriority ; get initialPriority ( ) : Protocol . Network . ResourcePriority | undefined { return this . _initialPriority ; } set initialPriority ( priority ) { this . _initialPriority = priority ; } private _frames : WebSocket [ ] = [ ] ; get frames ( ) : WebSocket [ ] { return this . _frames ; } private _statusCode : number = 0 ; get statusCode ( ) : number { return this . _statusCode ; } set statusCode ( value ) { this . _statusCode = value ; } get requestId ( ) : Protocol . Network . RequestId { return this . _requestId ; } get requestHttpVersion ( ) : string { if ( this . requestHeadersText ) { const firstLine = this . requestHeadersText . split ( /\r\n/ ) [ 0 ] ; const match = firstLine . match ( /(HTTP\/\d+\.\d+)$/ ) ; return match ? match [ 1 ] : 'HTTP/0.9' ; } const version = this . requestHeaderValue ( 'version' ) || this . requestHeaderValue ( ':version' ) ; if ( version ) { return version ; } return this . getFilteredProtocolName ( ) ; } get queryParameters ( ) : QueryString [ ] | undefined { if ( this . _parsedQueryParameters ) { return this . _parsedQueryParameters ; } if ( ! this . queryString ) { return null ; } this . _parsedQueryParameters = this . parseParameters ( this . queryString ) ; return this . _parsedQueryParameters ; } get requestContentType ( ) : string | undefined { return this . requestHeaderValue ( 'Content-Type' ) ; } get priority ( ) : Protocol . Network . ResourcePriority | undefined { return this . _currentPriority ?? this . _initialPriority ?? undefined ; } set priority ( priority ) { this . _currentPriority = priority ; } constructor ( private _requestId , url , public readonly documentURL , public readonly frameId = '' , public readonly loaderId , public readonly initiator ? ) { this . setUrl ( url ) ; } private static escapeCharacters ( str , chars = '^[]{}()\\\\.$*+?|' ) { let foundChar = false ; const length = chars . length ; for ( let i = 0 ; i < length ; ++ i ) { if ( str . indexOf ( chars . charAt ( i ) ) !== - 1 ) { foundChar = true ; break ; } } if ( ! foundChar ) { return str ; } let result = '' ; for ( let j = 0 ; j < str . length ; ++ j ) { if ( chars . indexOf ( str . charAt ( j ) ) !== - 1 ) { result += '\\' ; } result += str . charAt ( j ) ; } return result ; } public setUrl ( value ) { if ( this . _url === value ) { return ; } this . _url = value ; this . _parsedURL = parseUrl ( value ) ; delete this . _queryString ; delete this . _parsedQueryParameters ; } public setRemoteAddress ( ip , port ) { this . _remoteAddress = `${ ip }:${ port }` ; } public setIssueTime ( monotonicTime , wallTime ) { this . _issueTime = monotonicTime ; this . _wallIssueTime = wallTime ; this . _startTime = monotonicTime ; } public increaseTransferSize ( value ) { this . _transferSize = ( this . _transferSize || 0 ) + value ; } public requestFormData ( ) { return this . _requestFormData ; } public setRequestFormData ( data ) { this . _requestFormData = typeof data === 'string' ? Promise . resolve ( data ) : data ; this . _formParametersPromise = null ; } public getWallTime ( monotonicTime ) { return this . _wallIssueTime ? this . _wallIssueTime - this . _issueTime + monotonicTime : monotonicTime ; } public formParameters ( ) { if ( ! this . _formParametersPromise ) { this . _formParametersPromise = this . parseFormParameters ( ) ; } return this . _formParametersPromise ; } public responseHttpVersion ( ) { if ( this . _responseHeadersText ) { const firstLine = this . _responseHeadersText . split ( /\r\n/ ) [ 0 ] ; const match = firstLine . match ( /^(HTTP\/\d+\.\d+)/ ) ; return match ? match [ 1 ] : 'HTTP/0.9' ; } const version = this . responseHeaderValue ( 'version' ) || this . responseHeaderValue ( ':version' ) ; if ( version ) { return version ; } return this . getFilteredProtocolName ( ) ; } public setContentData ( data ) { if ( this . resourceType === 'WebSocket' ) { this . _contentData = Promise . resolve ( { error : 'Content for WebSockets is currently not supported' } ) ; } this . _contentData = data . then ( ( { body : text , base64Encoded } ) : ContentData => ( { text , encoding : base64Encoded ? 'base64' : undefined } ) ) . catch ( ( e ) : ContentData => ( { error : e . message } ) ) ; } public contentData ( ) { return this . _contentData ; } public addProtocolFrameError ( errorMessage , time ) { this . addFrame ( { type : WebSocketFrameType . ERROR , data : errorMessage , time , opcode : - 1 , mask : false } ) ; } public addProtocolFrame ( response , time , sent ) { const type = sent ? WebSocketFrameType . REQUEST : WebSocketFrameType . RESPONSE ; this . addFrame ( { type , data : response . payloadData , time , opcode : response . opcode , mask : response . mask } ) ; } public markAsRedirect ( redirectCount ) { this . _requestId = `${ this . requestId }:redirected.${ redirectCount }` ; } public addExtraRequestInfo ( extraRequestInfo ) { this . requestHeaders = extraRequestInfo . requestHeaders ; this . _hasExtraRequestInfo = true ; this . requestHeadersText = '' ; } public addExtraResponseInfo ( extraResponseInfo ) { this . responseHeaders = extraResponseInfo . responseHeaders ; if ( extraResponseInfo . responseHeadersText ) { this . responseHeadersText = extraResponseInfo . responseHeadersText ; if ( this . requestHeadersText ) { let requestHeadersText = `${ this . _requestMethod } ${ this . parsedURL . path }` ; if ( this . parsedURL . query ) { requestHeadersText += `?${ this . parsedURL . query }` ; } requestHeadersText += ` HTTP/1.1\r\n` ; for ( const { name , value } of this . requestHeaders ) { requestHeadersText += `${ name }: ${ value }\r\n` ; } this . requestHeadersText = requestHeadersText ; } } this . _hasExtraResponseInfo = true ; } public responseHeaderValue ( headerName ) { if ( ! this . _responseHeaderValues . has ( headerName ) ) { this . _responseHeaderValues . set ( headerName , this . computeHeaderValue ( this . responseHeaders , headerName ) ) ; } return this . _responseHeaderValues . get ( headerName ) ; } private async parseFormParameters ( ) { if ( this . requestContentType ?. match ( /^application\/x-www-form-urlencoded\s*(;.*)?$/i ) ) { const formUrlencoded = await this . requestFormData ( ) ; if ( ! formUrlencoded ) { return ; } return this . parseParameters ( formUrlencoded ) ; } const multipartDetails = this . requestContentType ?. match ( /^multipart\/form-data\s*;\s*boundary\s*=\s*(\S+)\s*$/ ) ; if ( ! multipartDetails ) { return ; } const boundary = multipartDetails [ 1 ] ; if ( ! boundary ) { return ; } const formData = await this . requestFormData ( ) ; if ( ! formData ) { return ; } return this . parseMultipartFormDataParameters ( formData , boundary ) ; } private parseMultipartFormDataParameters ( data , boundary ) { const sanitizedBoundary = NetworkRequest . escapeCharacters ( boundary ) ; const keyValuePattern = new RegExp ( '^\\r\\ncontent-disposition\\s*:\\s*form-data\\s*;\\s*name="([^"]*)"(?:\\s*;\\s*filename="([^"]*)")?' + '(?:\\r\\ncontent-type\\s*:\\s*([^\\r\\n]*))?' + '\\r\\n\\r\\n' + '(.*)' + '\\r\\n$' , 'is' ) ; const fields = data . split ( new RegExp ( `--${ sanitizedBoundary }(?:--\s*$)?` , 'g' ) ) ; return fields . reduce ( ( result , field ) : Param [ ] => { const [ match , name , value ] = field . match ( keyValuePattern ) || [ ] ; if ( ! match ) { return result ; } result . push ( { name , value } ) ; return result ; } , [ ] ) ; } private addFrame ( frame ) { this . _frames . push ( frame ) ; } private requestHeaderValue ( headerName ) { if ( ! this . _requestHeaderValues . has ( headerName ) ) { this . _requestHeaderValues . set ( headerName , this . computeHeaderValue ( this . requestHeaders , headerName ) ) ; } return this . _requestHeaderValues . get ( headerName ) ; } private getFilteredProtocolName ( ) { const protocol = this . _protocol . toLowerCase ( ) ; if ( protocol === 'h2' ) { return 'http/2.0' ; } return protocol . replace ( /^http\/2(\.0)?\+/ , 'http/2.0+' ) ; } private parseParameters ( queryString ) { return queryString . split ( '&' ) . map ( ( pair ) : QueryString => { const position = pair . indexOf ( '=' ) ; if ( position === - 1 ) { return { name : pair , value : '' } ; } else { return { name : pair . substring ( 0 , position ) , value : pair . substring ( position + 1 ) } ; } } ) ; } private computeHeaderValue ( headers , headerName ) { headerName = headerName . toLowerCase ( ) ; const values = headers . filter ( ( { name } ) : boolean => name . toLowerCase ( ) === headerName ) . map ( ( { value } ) : string => value ) ; if ( ! values . length ) { return ; } if ( headerName === 'set-cookie' ) { return values . join ( '\n' ) ; } return values . join ( ', ' ) ; } }