2021-08-24 14:29:04 -07:00
/ * *
* Copyright ( c ) Microsoft Corporation .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2023-01-05 14:39:49 -08:00
import type * as channels from '@protocol/channels' ;
import type { LookupAddress } from 'dns' ;
2024-07-12 11:42:24 +02:00
import http from 'http' ;
import https from 'https' ;
2022-04-06 13:57:14 -08:00
import type { Readable , TransformCallback } from 'stream' ;
import { pipeline , Transform } from 'stream' ;
2021-09-22 12:44:22 -07:00
import zlib from 'zlib' ;
2022-04-06 13:57:14 -08:00
import type { HTTPCredentials } from '../../types/types' ;
2022-04-07 13:36:13 -08:00
import { TimeoutSettings } from '../common/timeoutSettings' ;
2023-01-13 13:50:38 -08:00
import { getUserAgent } from '../utils/userAgent' ;
2024-10-07 18:43:25 +02:00
import { assert , constructURLBasedOnBaseURL , createGuid , eventsHelper , monotonicTime , type RegisteredListener } from '../utils' ;
2023-01-05 14:39:49 -08:00
import { HttpsProxyAgent , SocksProxyAgent } from '../utilsBundle' ;
2024-07-12 11:42:24 +02:00
import { BrowserContext , verifyClientCertificates } from './browserContext' ;
2024-09-13 18:29:35 -07:00
import { CookieStore , domainMatches , parseRawCookie } from './cookieStore' ;
2021-09-22 12:44:22 -07:00
import { MultipartFormData } from './formData' ;
2024-09-17 16:11:21 +02:00
import { httpHappyEyeballsAgent , httpsHappyEyeballsAgent , timingForSocket } from '../utils/happy-eyeballs' ;
2022-04-06 13:57:14 -08:00
import type { CallMetadata } from './instrumentation' ;
import { SdkObject } from './instrumentation' ;
import type { Playwright } from './playwright' ;
import type { Progress } from './progress' ;
import { ProgressController } from './progress' ;
2022-01-22 11:25:13 -08:00
import { Tracing } from './trace/recorder/tracing' ;
2022-04-06 13:57:14 -08:00
import type * as types from './types' ;
import type { HeadersArray , ProxySettings } from './types' ;
2024-08-19 09:02:14 +02:00
import { getMatchingTLSOptionsForOrigin , rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor' ;
2024-09-17 16:11:21 +02:00
import type * as har from '@trace/har' ;
2024-09-18 08:18:47 +02:00
import { TLSSocket } from 'tls' ;
2022-04-18 12:47:23 -08:00
2021-10-01 12:11:33 -07:00
type FetchRequestOptions = {
2021-09-14 18:31:35 -07:00
userAgent : string ;
extraHTTPHeaders? : HeadersArray ;
httpCredentials? : HTTPCredentials ;
proxy? : ProxySettings ;
timeoutSettings : TimeoutSettings ;
ignoreHTTPSErrors? : boolean ;
baseURL? : string ;
2024-09-13 17:34:34 +02:00
clientCertificates? : types.BrowserContextOptions [ 'clientCertificates' ] ;
2021-09-14 18:31:35 -07:00
} ;
2021-08-24 14:29:04 -07:00
2023-03-10 08:58:12 -08:00
type HeadersObject = Readonly < { [ name : string ] : string } > ;
2021-12-02 15:53:47 -08:00
export type APIRequestEvent = {
url : URL ,
method : string ,
2023-03-10 08:58:12 -08:00
headers : HeadersObject ,
2022-06-14 22:02:15 -07:00
cookies : channels.NameValue [ ] ,
2021-12-02 15:53:47 -08:00
postData? : Buffer
} ;
export type APIRequestFinishedEvent = {
requestEvent : APIRequestEvent ,
httpVersion : string ;
headers : http.IncomingHttpHeaders ;
2022-06-14 22:02:15 -07:00
cookies : channels.NetworkCookie [ ] ;
2021-12-02 15:53:47 -08:00
rawHeaders : string [ ] ;
statusCode : number ;
statusMessage : string ;
body? : Buffer ;
2024-09-17 16:11:21 +02:00
timings : har.Timings ;
2024-09-18 14:51:42 +02:00
serverIPAddress? : string ;
serverPort? : number ;
2024-09-18 08:18:47 +02:00
securityDetails? : har.SecurityDetails ;
2021-12-02 15:53:47 -08:00
} ;
2023-02-22 17:09:56 +01:00
type SendRequestOptions = https . RequestOptions & {
2023-01-05 14:39:49 -08:00
maxRedirects : number ,
deadline : number ,
2023-03-10 08:58:12 -08:00
headers : HeadersObject ,
2023-01-05 14:39:49 -08:00
__testHookLookup ? : ( hostname : string ) = > LookupAddress [ ]
} ;
2021-11-05 16:27:49 +01:00
export abstract class APIRequestContext extends SdkObject {
2021-09-15 14:02:55 -07:00
static Events = {
Dispose : 'dispose' ,
2021-12-02 15:53:47 -08:00
Request : 'request' ,
RequestFinished : 'requestfinished' ,
2021-09-15 14:02:55 -07:00
} ;
2021-09-14 18:31:35 -07:00
readonly fetchResponses : Map < string , Buffer > = new Map ( ) ;
2021-11-30 18:12:19 -08:00
readonly fetchLog : Map < string , string [ ] > = new Map ( ) ;
2021-11-05 16:27:49 +01:00
protected static allInstances : Set < APIRequestContext > = new Set ( ) ;
2022-11-22 15:21:20 -08:00
readonly _activeProgressControllers = new Set < ProgressController > ( ) ;
2023-10-16 20:32:13 -07:00
_closeReason : string | undefined ;
2021-09-15 14:02:55 -07:00
static findResponseBody ( guid : string ) : Buffer | undefined {
2021-11-05 16:27:49 +01:00
for ( const request of APIRequestContext . allInstances ) {
2021-09-15 14:02:55 -07:00
const body = request . fetchResponses . get ( guid ) ;
if ( body )
return body ;
}
return undefined ;
}
2021-09-08 10:01:40 -07:00
2021-09-14 18:31:35 -07:00
constructor ( parent : SdkObject ) {
2022-07-15 07:56:47 -08:00
super ( parent , 'request-context' ) ;
2021-11-05 16:27:49 +01:00
APIRequestContext . allInstances . add ( this ) ;
2021-09-14 18:31:35 -07:00
}
2021-09-13 14:29:44 -07:00
2021-09-15 14:02:55 -07:00
protected _disposeImpl() {
2021-11-05 16:27:49 +01:00
APIRequestContext . allInstances . delete ( this ) ;
2021-09-14 18:31:35 -07:00
this . fetchResponses . clear ( ) ;
2021-11-30 18:12:19 -08:00
this . fetchLog . clear ( ) ;
2021-11-05 16:27:49 +01:00
this . emit ( APIRequestContext . Events . Dispose ) ;
2021-08-24 14:29:04 -07:00
}
2021-11-30 18:12:19 -08:00
disposeResponse ( fetchUid : string ) {
this . fetchResponses . delete ( fetchUid ) ;
this . fetchLog . delete ( fetchUid ) ;
}
2022-01-22 11:25:13 -08:00
abstract tracing ( ) : Tracing ;
2024-05-13 18:51:30 -07:00
abstract dispose ( options : { reason? : string } ) : Promise < void > ;
2021-09-15 14:02:55 -07:00
2021-09-14 18:31:35 -07:00
abstract _defaultOptions ( ) : FetchRequestOptions ;
2022-06-14 22:02:15 -07:00
abstract _addCookies ( cookies : channels.NetworkCookie [ ] ) : Promise < void > ;
abstract _cookies ( url : URL ) : Promise < channels.NetworkCookie [ ] > ;
2021-11-05 16:27:49 +01:00
abstract storageState ( ) : Promise < channels.APIRequestContextStorageStateResult > ;
2021-09-14 18:31:35 -07:00
private _storeResponseBody ( body : Buffer ) : string {
const uid = createGuid ( ) ;
this . fetchResponses . set ( uid , body ) ;
return uid ;
2021-08-26 16:18:54 -07:00
}
2022-06-14 22:02:15 -07:00
async fetch ( params : channels.APIRequestContextFetchParams , metadata : CallMetadata ) : Promise < channels.APIResponse > {
2021-11-19 20:32:29 -08:00
const defaults = this . _defaultOptions ( ) ;
2023-03-10 08:58:12 -08:00
const headers : HeadersObject = {
'user-agent' : defaults . userAgent ,
'accept' : '*/*' ,
'accept-encoding' : 'gzip,deflate,br' ,
} ;
2021-11-19 20:32:29 -08:00
if ( defaults . extraHTTPHeaders ) {
for ( const { name , value } of defaults . extraHTTPHeaders )
2023-03-10 08:58:12 -08:00
setHeader ( headers , name , value ) ;
2021-11-19 20:32:29 -08:00
}
2021-09-14 18:31:35 -07:00
2021-11-19 20:32:29 -08:00
if ( params . headers ) {
for ( const { name , value } of params . headers )
2023-03-10 08:58:12 -08:00
setHeader ( headers , name , value ) ;
2021-11-19 20:32:29 -08:00
}
2021-09-14 18:31:35 -07:00
2024-10-01 22:43:32 +02:00
const requestUrl = new URL ( constructURLBasedOnBaseURL ( defaults . baseURL , params . url ) ) ;
2024-09-09 22:28:08 +02:00
if ( params . encodedParams ) {
requestUrl . search = params . encodedParams ;
} else if ( params . params ) {
2023-05-09 14:51:49 -07:00
for ( const { name , value } of params . params )
2024-08-12 23:22:03 +02:00
requestUrl . searchParams . append ( name , value ) ;
2023-05-09 14:51:49 -07:00
}
2024-05-02 16:30:12 -07:00
const credentials = this . _getHttpCredentials ( requestUrl ) ;
2024-05-30 10:19:56 -07:00
if ( credentials ? . send === 'always' )
2024-05-02 16:30:12 -07:00
setBasicAuthorizationHeader ( headers , credentials ) ;
2021-11-19 20:32:29 -08:00
const method = params . method ? . toUpperCase ( ) || 'GET' ;
const proxy = defaults . proxy ;
let agent ;
2024-09-16 17:57:33 +02:00
// We skip 'per-context' in order to not break existing users. 'per-context' was previously used to
// workaround an upstream Chromium bug. Can be removed in the future.
if ( proxy && proxy . server !== 'per-context' && ! shouldBypassProxy ( requestUrl , proxy . bypass ) )
agent = createProxyAgent ( proxy ) ;
2021-09-14 18:31:35 -07:00
2021-11-19 20:32:29 -08:00
const timeout = defaults . timeoutSettings . timeout ( params ) ;
const deadline = timeout && ( monotonicTime ( ) + timeout ) ;
2023-01-05 14:39:49 -08:00
const options : SendRequestOptions = {
2021-11-19 20:32:29 -08:00
method ,
headers ,
agent ,
2022-09-09 21:14:42 +02:00
maxRedirects : params.maxRedirects === 0 ? - 1 : params.maxRedirects === undefined ? 20 : params.maxRedirects ,
2021-11-19 20:32:29 -08:00
timeout ,
2023-01-05 14:39:49 -08:00
deadline ,
2024-08-19 09:02:14 +02:00
. . . getMatchingTLSOptionsForOrigin ( this . _defaultOptions ( ) . clientCertificates , requestUrl . origin ) ,
2023-01-05 14:39:49 -08:00
__testHookLookup : ( params as any ) . __testHookLookup ,
2021-11-19 20:32:29 -08:00
} ;
2024-07-12 11:42:24 +02:00
// rejectUnauthorized = undefined is treated as true in Node.js 12.
2021-11-19 20:32:29 -08:00
if ( params . ignoreHTTPSErrors || defaults . ignoreHTTPSErrors )
options . rejectUnauthorized = false ;
2022-09-01 08:36:08 -07:00
const postData = serializePostData ( params , headers ) ;
2021-11-19 20:32:29 -08:00
if ( postData )
2023-03-10 08:58:12 -08:00
setHeader ( headers , 'content-length' , String ( postData . byteLength ) ) ;
2021-11-30 18:12:19 -08:00
const controller = new ProgressController ( metadata , this ) ;
const fetchResponse = await controller . run ( progress = > {
2024-06-19 18:10:14 -07:00
return this . _sendRequestWithRetries ( progress , requestUrl , options , postData , params . maxRetries ) ;
2021-11-30 18:12:19 -08:00
} ) ;
2021-11-19 20:32:29 -08:00
const fetchUid = this . _storeResponseBody ( fetchResponse . body ) ;
2021-11-30 18:12:19 -08:00
this . fetchLog . set ( fetchUid , controller . metadata . log ) ;
2024-08-01 17:53:43 -07:00
if ( params . failOnStatusCode && ( fetchResponse . status < 200 || fetchResponse . status >= 400 ) ) {
let responseText = '' ;
if ( fetchResponse . body . byteLength ) {
let text = fetchResponse . body . toString ( 'utf8' ) ;
if ( text . length > 1000 )
text = text . substring ( 0 , 997 ) + '...' ;
responseText = ` \ nResponse text: \ n ${ text } ` ;
}
throw new Error ( ` ${ fetchResponse . status } ${ fetchResponse . statusText } ${ responseText } ` ) ;
}
2021-11-19 20:32:29 -08:00
return { . . . fetchResponse , fetchUid } ;
2021-08-27 15:28:36 -07:00
}
2021-08-26 16:18:54 -07:00
2022-06-14 22:02:15 -07:00
private _parseSetCookieHeader ( responseUrl : string , setCookie : string [ ] | undefined ) : channels . NetworkCookie [ ] {
2021-12-02 15:53:47 -08:00
if ( ! setCookie )
return [ ] ;
2021-09-14 18:31:35 -07:00
const url = new URL ( responseUrl ) ;
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
const defaultPath = '/' + url . pathname . substr ( 1 ) . split ( '/' ) . slice ( 0 , - 1 ) . join ( '/' ) ;
2022-06-14 22:02:15 -07:00
const cookies : channels.NetworkCookie [ ] = [ ] ;
2021-09-14 18:31:35 -07:00
for ( const header of setCookie ) {
// Decode cookie value?
2022-06-14 22:02:15 -07:00
const cookie : channels.NetworkCookie | null = parseCookie ( header ) ;
2021-09-14 18:31:35 -07:00
if ( ! cookie )
continue ;
2021-09-29 17:15:32 -07:00
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3
2021-09-14 18:31:35 -07:00
if ( ! cookie . domain )
cookie . domain = url . hostname ;
2021-09-29 17:15:32 -07:00
else
2022-11-23 09:22:49 -08:00
assert ( cookie . domain . startsWith ( '.' ) || ! cookie . domain . includes ( '.' ) ) ;
2021-09-29 17:15:32 -07:00
if ( ! domainMatches ( url . hostname , cookie . domain ! ) )
2021-09-14 18:31:35 -07:00
continue ;
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4
if ( ! cookie . path || ! cookie . path . startsWith ( '/' ) )
cookie . path = defaultPath ;
cookies . push ( cookie ) ;
}
2021-12-02 15:53:47 -08:00
return cookies ;
2021-09-14 18:31:35 -07:00
}
2023-03-10 08:58:12 -08:00
private async _updateRequestCookieHeader ( url : URL , headers : HeadersObject ) {
if ( getHeader ( headers , 'cookie' ) !== undefined )
2021-09-14 18:31:35 -07:00
return ;
2021-09-29 17:15:32 -07:00
const cookies = await this . _cookies ( url ) ;
2021-09-14 18:31:35 -07:00
if ( cookies . length ) {
const valueArray = cookies . map ( c = > ` ${ c . name } = ${ c . value } ` ) ;
2023-03-10 08:58:12 -08:00
setHeader ( headers , 'cookie' , valueArray . join ( '; ' ) ) ;
2021-09-14 18:31:35 -07:00
}
}
2024-06-19 18:10:14 -07:00
private async _sendRequestWithRetries ( progress : Progress , url : URL , options : SendRequestOptions , postData? : Buffer , maxRetries? : number ) : Promise < Omit < channels.APIResponse , 'fetchUid' > & { body : Buffer } > {
maxRetries ? ? = 0 ;
let backoff = 250 ;
for ( let i = 0 ; i <= maxRetries ; i ++ ) {
try {
return await this . _sendRequest ( progress , url , options , postData ) ;
} catch ( e ) {
2024-09-18 17:09:08 +02:00
e = rewriteOpenSSLErrorIfNeeded ( e ) ;
2024-06-19 18:10:14 -07:00
if ( maxRetries === 0 )
throw e ;
if ( i === maxRetries || ( options . deadline && monotonicTime ( ) + backoff > options . deadline ) )
throw new Error ( ` Failed after ${ i + 1 } attempt(s): ${ e } ` ) ;
// Retry on connection reset only.
if ( e . code !== 'ECONNRESET' )
throw e ;
progress . log ( ` Received ECONNRESET, will retry after ${ backoff } ms. ` ) ;
await new Promise ( f = > setTimeout ( f , backoff ) ) ;
backoff *= 2 ;
}
}
throw new Error ( 'Unreachable' ) ;
}
2023-01-05 14:39:49 -08:00
private async _sendRequest ( progress : Progress , url : URL , options : SendRequestOptions , postData? : Buffer ) : Promise < Omit < channels.APIResponse , 'fetchUid' > & { body : Buffer } > {
2023-03-10 08:58:12 -08:00
await this . _updateRequestCookieHeader ( url , options . headers ) ;
2021-12-02 15:53:47 -08:00
2023-03-10 08:58:12 -08:00
const requestCookies = getHeader ( options . headers , 'cookie' ) ? . split ( ';' ) . map ( p = > {
2021-12-02 15:53:47 -08:00
const [ name , value ] = p . split ( '=' ) . map ( v = > v . trim ( ) ) ;
return { name , value } ;
} ) || [ ] ;
const requestEvent : APIRequestEvent = {
url ,
method : options.method ! ,
2023-03-10 08:58:12 -08:00
headers : options.headers ,
2021-12-02 15:53:47 -08:00
cookies : requestCookies ,
postData
} ;
this . emit ( APIRequestContext . Events . Request , requestEvent ) ;
2022-06-14 22:02:15 -07:00
return new Promise ( ( fulfill , reject ) = > {
2021-09-14 18:31:35 -07:00
const requestConstructor : ( ( url : URL , options : http.RequestOptions , callback ? : ( res : http.IncomingMessage ) = > void ) = > http . ClientRequest )
= ( url . protocol === 'https:' ? https : http ) . request ;
2023-01-05 14:39:49 -08:00
// If we have a proxy agent already, do not override it.
const agent = options . agent || ( url . protocol === 'https:' ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent ) ;
const requestOptions = { . . . options , agent } ;
2024-09-17 16:11:21 +02:00
const startAt = monotonicTime ( ) ;
2024-10-14 17:22:29 +02:00
let reusedSocketAt : number | undefined ;
2024-09-17 16:11:21 +02:00
let dnsLookupAt : number | undefined ;
let tcpConnectionAt : number | undefined ;
let tlsHandshakeAt : number | undefined ;
let requestFinishAt : number | undefined ;
2024-09-18 14:51:42 +02:00
let serverIPAddress : string | undefined ;
let serverPort : number | undefined ;
2024-09-17 16:11:21 +02:00
2024-09-18 08:18:47 +02:00
let securityDetails : har.SecurityDetails | undefined ;
2024-10-07 18:43:25 +02:00
const listeners : RegisteredListener [ ] = [ ] ;
2023-01-05 14:39:49 -08:00
const request = requestConstructor ( url , requestOptions as any , async response = > {
2024-09-17 16:11:21 +02:00
const responseAt = monotonicTime ( ) ;
2024-10-07 18:43:25 +02:00
2021-12-02 15:53:47 -08:00
const notifyRequestFinished = ( body? : Buffer ) = > {
2024-09-17 16:11:21 +02:00
const endAt = monotonicTime ( ) ;
// spec: http://www.softwareishard.com/blog/har-12-spec/#timings
2024-10-14 17:22:29 +02:00
const connectEnd = tlsHandshakeAt ? ? tcpConnectionAt ;
2024-09-17 16:11:21 +02:00
const timings : har.Timings = {
send : requestFinishAt ! - startAt ,
wait : responseAt - requestFinishAt ! ,
receive : endAt - responseAt ,
dns : dnsLookupAt ? dnsLookupAt - startAt : - 1 ,
2024-10-14 17:22:29 +02:00
connect : connectEnd ? connectEnd - startAt : - 1 , // "If [ssl] is defined then the time is also included in the connect field "
2024-09-17 16:11:21 +02:00
ssl : tlsHandshakeAt ? tlsHandshakeAt - tcpConnectionAt ! : - 1 ,
2024-10-14 17:22:29 +02:00
blocked : reusedSocketAt ? reusedSocketAt - startAt : - 1 ,
2024-09-17 16:11:21 +02:00
} ;
2021-12-02 15:53:47 -08:00
const requestFinishedEvent : APIRequestFinishedEvent = {
requestEvent ,
2021-12-03 15:46:57 -08:00
httpVersion : response.httpVersion ,
statusCode : response.statusCode || 0 ,
statusMessage : response.statusMessage || '' ,
headers : response.headers ,
rawHeaders : response.rawHeaders ,
2021-12-02 15:53:47 -08:00
cookies ,
2024-09-17 16:11:21 +02:00
body ,
timings ,
2024-09-18 14:51:42 +02:00
serverIPAddress ,
serverPort ,
2024-09-18 08:18:47 +02:00
securityDetails ,
2021-12-02 15:53:47 -08:00
} ;
this . emit ( APIRequestContext . Events . RequestFinished , requestFinishedEvent ) ;
} ;
2021-11-30 18:12:19 -08:00
progress . log ( ` ← ${ response . statusCode } ${ response . statusMessage } ` ) ;
for ( const [ name , value ] of Object . entries ( response . headers ) )
progress . log ( ` ${ name } : ${ value } ` ) ;
2021-12-02 15:53:47 -08:00
const cookies = this . _parseSetCookieHeader ( response . url || url . toString ( ) , response . headers [ 'set-cookie' ] ) ;
2023-09-19 16:18:16 -07:00
if ( cookies . length ) {
try {
await this . _addCookies ( cookies ) ;
} catch ( e ) {
// Cookie value is limited by 4096 characters in the browsers. If setCookies failed,
// we try setting each cookie individually just in case only some of them are bad.
await Promise . all ( cookies . map ( c = > this . _addCookies ( [ c ] ) . catch ( ( ) = > { } ) ) ) ;
}
}
2021-12-02 15:53:47 -08:00
2022-09-09 21:14:42 +02:00
if ( redirectStatus . includes ( response . statusCode ! ) && options . maxRedirects >= 0 ) {
2021-09-14 18:31:35 -07:00
if ( ! options . maxRedirects ) {
reject ( new Error ( 'Max redirect count exceeded' ) ) ;
2021-10-18 19:41:56 -07:00
request . destroy ( ) ;
2021-09-14 18:31:35 -07:00
return ;
}
2023-08-28 12:42:50 -07:00
const headers = { . . . options . headers } ;
2023-03-10 08:58:12 -08:00
removeHeader ( headers , ` cookie ` ) ;
2021-09-14 18:31:35 -07:00
// HTTP-redirect fetch step 13 (https://fetch.spec.whatwg.org/#http-redirect-fetch)
const status = response . statusCode ! ;
let method = options . method ! ;
if ( ( status === 301 || status === 302 ) && method === 'POST' ||
status === 303 && ! [ 'GET' , 'HEAD' ] . includes ( method ) ) {
method = 'GET' ;
postData = undefined ;
2023-03-10 08:58:12 -08:00
removeHeader ( headers , ` content-encoding ` ) ;
removeHeader ( headers , ` content-language ` ) ;
removeHeader ( headers , ` content-length ` ) ;
removeHeader ( headers , ` content-location ` ) ;
removeHeader ( headers , ` content-type ` ) ;
2021-09-14 18:31:35 -07:00
}
2023-08-28 12:42:50 -07:00
2023-01-05 14:39:49 -08:00
const redirectOptions : SendRequestOptions = {
2021-09-14 18:31:35 -07:00
method ,
headers ,
agent : options.agent ,
maxRedirects : options.maxRedirects - 1 ,
timeout : options.timeout ,
2023-01-05 14:39:49 -08:00
deadline : options.deadline ,
2024-08-19 09:02:14 +02:00
. . . getMatchingTLSOptionsForOrigin ( this . _defaultOptions ( ) . clientCertificates , url . origin ) ,
2023-01-05 14:39:49 -08:00
__testHookLookup : options.__testHookLookup ,
2021-09-14 18:31:35 -07:00
} ;
2021-10-26 23:20:52 -07:00
// rejectUnauthorized = undefined is treated as true in node 12.
if ( options . rejectUnauthorized === false )
redirectOptions . rejectUnauthorized = false ;
2021-09-14 18:31:35 -07:00
// HTTP-redirect fetch step 4: If locationURL is null, then return response.
2024-05-21 09:15:33 +02:00
// Best-effort UTF-8 decoding, per spec it's US-ASCII only, but browsers are more lenient.
// Node.js parses it as Latin1 via std::v8::String, so we convert it to UTF-8.
const locationHeaderValue = Buffer . from ( response . headers . location ? ? '' , 'latin1' ) . toString ( 'utf8' ) ;
if ( locationHeaderValue ) {
2023-01-06 19:22:17 +01:00
let locationURL ;
try {
2024-05-21 09:15:33 +02:00
locationURL = new URL ( locationHeaderValue , url ) ;
2023-01-06 19:22:17 +01:00
} catch ( error ) {
2024-05-21 09:15:33 +02:00
reject ( new Error ( ` uri requested responds with an invalid redirect URL: ${ locationHeaderValue } ` ) ) ;
2023-01-06 19:22:17 +01:00
request . destroy ( ) ;
return ;
}
2023-08-28 12:42:50 -07:00
if ( headers [ 'host' ] )
headers [ 'host' ] = locationURL . host ;
2021-12-02 15:53:47 -08:00
notifyRequestFinished ( ) ;
2021-11-30 18:12:19 -08:00
fulfill ( this . _sendRequest ( progress , locationURL , redirectOptions , postData ) ) ;
2021-10-18 19:41:56 -07:00
request . destroy ( ) ;
2021-09-14 18:31:35 -07:00
return ;
}
2021-08-26 16:18:54 -07:00
}
2023-03-10 08:58:12 -08:00
if ( response . statusCode === 401 && ! getHeader ( options . headers , 'authorization' ) ) {
2021-09-14 18:31:35 -07:00
const auth = response . headers [ 'www-authenticate' ] ;
2023-03-27 11:52:00 -04:00
const credentials = this . _getHttpCredentials ( url ) ;
2021-12-16 13:40:52 -08:00
if ( auth ? . trim ( ) . startsWith ( 'Basic' ) && credentials ) {
2024-05-02 16:30:12 -07:00
setBasicAuthorizationHeader ( options . headers , credentials ) ;
2021-12-02 15:53:47 -08:00
notifyRequestFinished ( ) ;
2021-11-30 18:12:19 -08:00
fulfill ( this . _sendRequest ( progress , url , options , postData ) ) ;
2021-10-18 19:41:56 -07:00
request . destroy ( ) ;
2021-09-14 18:31:35 -07:00
return ;
}
2021-08-26 16:18:54 -07:00
}
2021-09-14 18:31:35 -07:00
response . on ( 'aborted' , ( ) = > reject ( new Error ( 'aborted' ) ) ) ;
2021-08-26 16:18:54 -07:00
2022-01-28 12:58:58 -08:00
const chunks : Buffer [ ] = [ ] ;
const notifyBodyFinished = ( ) = > {
const body = Buffer . concat ( chunks ) ;
notifyRequestFinished ( body ) ;
fulfill ( {
url : response.url || url . toString ( ) ,
status : response.statusCode || 0 ,
statusText : response.statusMessage || '' ,
headers : toHeadersArray ( response . rawHeaders ) ,
body
} ) ;
} ;
2021-09-14 18:31:35 -07:00
let body : Readable = response ;
let transform : Transform | undefined ;
const encoding = response . headers [ 'content-encoding' ] ;
if ( encoding === 'gzip' || encoding === 'x-gzip' ) {
transform = zlib . createGunzip ( {
flush : zlib.constants.Z_SYNC_FLUSH ,
finishFlush : zlib.constants.Z_SYNC_FLUSH
} ) ;
} else if ( encoding === 'br' ) {
2024-07-23 18:28:34 -07:00
transform = zlib . createBrotliDecompress ( {
flush : zlib.constants.BROTLI_OPERATION_FLUSH ,
finishFlush : zlib.constants.BROTLI_OPERATION_FLUSH
} ) ;
2021-09-14 18:31:35 -07:00
} else if ( encoding === 'deflate' ) {
transform = zlib . createInflate ( ) ;
2021-08-26 16:18:54 -07:00
}
2021-09-14 18:31:35 -07:00
if ( transform ) {
2022-02-15 09:06:21 -08:00
// Brotli and deflate decompressors throw if the input stream is empty.
const emptyStreamTransform = new SafeEmptyStreamTransform ( notifyBodyFinished ) ;
body = pipeline ( response , emptyStreamTransform , transform , e = > {
2021-09-14 18:31:35 -07:00
if ( e )
2023-05-19 15:17:43 -07:00
reject ( new Error ( ` failed to decompress ' ${ encoding } ' encoding: ${ e . message } ` ) ) ;
2021-09-14 18:31:35 -07:00
} ) ;
2022-10-24 12:51:45 -07:00
body . on ( 'error' , e = > reject ( new Error ( ` failed to decompress ' ${ encoding } ' encoding: ${ e } ` ) ) ) ;
2022-04-22 13:42:52 +02:00
} else {
body . on ( 'error' , reject ) ;
2021-08-27 23:47:33 -07:00
}
2021-08-31 09:34:58 -07:00
2021-09-14 18:31:35 -07:00
body . on ( 'data' , chunk = > chunks . push ( chunk ) ) ;
2022-01-28 12:58:58 -08:00
body . on ( 'end' , notifyBodyFinished ) ;
2021-08-26 16:18:54 -07:00
} ) ;
2024-09-18 17:09:08 +02:00
request . on ( 'error' , reject ) ;
2021-09-22 19:30:56 +02:00
2024-10-07 18:43:25 +02:00
listeners . push (
eventsHelper . addEventListener ( this , APIRequestContext . Events . Dispose , ( ) = > {
reject ( new Error ( 'Request context disposed.' ) ) ;
request . destroy ( ) ;
} )
) ;
request . on ( 'close' , ( ) = > eventsHelper . removeEventListeners ( listeners ) ) ;
2021-10-18 19:41:56 -07:00
2024-09-17 16:11:21 +02:00
request . on ( 'socket' , socket = > {
2024-10-14 17:22:29 +02:00
if ( request . reusedSocket ) {
reusedSocketAt = monotonicTime ( ) ;
return ;
}
2024-09-17 16:11:21 +02:00
// happy eyeballs don't emit lookup and connect events, so we use our custom ones
const happyEyeBallsTimings = timingForSocket ( socket ) ;
dnsLookupAt = happyEyeBallsTimings . dnsLookupAt ;
2024-10-08 14:17:50 +02:00
tcpConnectionAt ? ? = happyEyeBallsTimings . tcpConnectionAt ;
2024-09-17 16:11:21 +02:00
// non-happy-eyeballs sockets
2024-10-07 18:43:25 +02:00
listeners . push (
eventsHelper . addEventListener ( socket , 'lookup' , ( ) = > { dnsLookupAt = monotonicTime ( ) ; } ) ,
2024-10-08 14:17:50 +02:00
eventsHelper . addEventListener ( socket , 'connect' , ( ) = > { tcpConnectionAt ? ? = monotonicTime ( ) ; } ) ,
2024-10-07 18:43:25 +02:00
eventsHelper . addEventListener ( socket , 'secureConnect' , ( ) = > {
tlsHandshakeAt = monotonicTime ( ) ;
if ( socket instanceof TLSSocket ) {
const peerCertificate = socket . getPeerCertificate ( ) ;
securityDetails = {
protocol : socket.getProtocol ( ) ? ? undefined ,
subjectName : peerCertificate.subject.CN ,
validFrom : new Date ( peerCertificate . valid_from ) . getTime ( ) / 1000 ,
validTo : new Date ( peerCertificate . valid_to ) . getTime ( ) / 1000 ,
issuer : peerCertificate.issuer.CN
} ;
}
} ) ,
) ;
2024-09-18 14:51:42 +02:00
2024-10-08 14:17:50 +02:00
// when using socks proxy, having the socket means the connection got established
if ( agent instanceof SocksProxyAgent )
tcpConnectionAt ? ? = monotonicTime ( ) ;
2024-09-18 14:51:42 +02:00
serverIPAddress = socket . remoteAddress ;
serverPort = socket . remotePort ;
2024-09-17 16:11:21 +02:00
} ) ;
request . on ( 'finish' , ( ) = > { requestFinishAt = monotonicTime ( ) ; } ) ;
2024-10-08 14:17:50 +02:00
// http proxy
request . on ( 'proxyConnect' , ( ) = > {
tcpConnectionAt ? ? = monotonicTime ( ) ;
} ) ;
2021-11-30 18:12:19 -08:00
progress . log ( ` → ${ options . method } ${ url . toString ( ) } ` ) ;
if ( options . headers ) {
for ( const [ name , value ] of Object . entries ( options . headers ) )
progress . log ( ` ${ name } : ${ value } ` ) ;
2021-10-08 14:11:40 -07:00
}
2021-09-22 19:30:56 +02:00
if ( options . deadline ) {
const rejectOnTimeout = ( ) = > {
reject ( new Error ( ` Request timed out after ${ options . timeout } ms ` ) ) ;
2021-10-18 19:41:56 -07:00
request . destroy ( ) ;
2021-09-22 19:30:56 +02:00
} ;
const remaining = options . deadline - monotonicTime ( ) ;
if ( remaining <= 0 ) {
rejectOnTimeout ( ) ;
return ;
}
request . setTimeout ( remaining , rejectOnTimeout ) ;
2021-09-14 18:31:35 -07:00
}
2021-09-22 19:30:56 +02:00
2021-09-14 18:31:35 -07:00
if ( postData )
request . write ( postData ) ;
request . end ( ) ;
2021-08-26 16:18:54 -07:00
} ) ;
2021-09-14 18:31:35 -07:00
}
2023-03-27 11:52:00 -04:00
private _getHttpCredentials ( url : URL ) {
if ( ! this . _defaultOptions ( ) . httpCredentials ? . origin || url . origin . toLowerCase ( ) === this . _defaultOptions ( ) . httpCredentials ? . origin ? . toLowerCase ( ) )
return this . _defaultOptions ( ) . httpCredentials ;
return undefined ;
}
2021-09-14 18:31:35 -07:00
}
2022-02-15 09:06:21 -08:00
class SafeEmptyStreamTransform extends Transform {
private _receivedSomeData : boolean = false ;
private _onEmptyStreamCallback : ( ) = > void ;
constructor ( onEmptyStreamCallback : ( ) = > void ) {
super ( ) ;
this . _onEmptyStreamCallback = onEmptyStreamCallback ;
}
override _transform ( chunk : any , encoding : BufferEncoding , callback : TransformCallback ) : void {
this . _receivedSomeData = true ;
callback ( null , chunk ) ;
}
override _flush ( callback : TransformCallback ) : void {
if ( this . _receivedSomeData )
callback ( null ) ;
else
this . _onEmptyStreamCallback ( ) ;
}
}
2021-11-05 16:27:49 +01:00
export class BrowserContextAPIRequestContext extends APIRequestContext {
2021-09-14 18:31:35 -07:00
private readonly _context : BrowserContext ;
constructor ( context : BrowserContext ) {
super ( context ) ;
this . _context = context ;
2021-09-15 14:02:55 -07:00
context . once ( BrowserContext . Events . Close , ( ) = > this . _disposeImpl ( ) ) ;
}
2022-01-22 11:25:13 -08:00
override tracing() {
return this . _context . tracing ;
}
2024-05-13 18:51:30 -07:00
override async dispose ( options : { reason? : string } ) {
this . _closeReason = options . reason ;
2021-09-15 14:02:55 -07:00
this . fetchResponses . clear ( ) ;
2021-09-14 18:31:35 -07:00
}
_defaultOptions ( ) : FetchRequestOptions {
return {
userAgent : this._context._options.userAgent || this . _context . _browser . userAgent ( ) ,
extraHTTPHeaders : this._context._options.extraHTTPHeaders ,
httpCredentials : this._context._options.httpCredentials ,
proxy : this._context._options.proxy || this . _context . _browser . options . proxy ,
timeoutSettings : this._context._timeoutSettings ,
ignoreHTTPSErrors : this._context._options.ignoreHTTPSErrors ,
baseURL : this._context._options.baseURL ,
2024-07-12 11:42:24 +02:00
clientCertificates : this._context._options.clientCertificates ,
2021-09-08 10:01:40 -07:00
} ;
2021-09-14 18:31:35 -07:00
}
2022-06-14 22:02:15 -07:00
async _addCookies ( cookies : channels.NetworkCookie [ ] ) : Promise < void > {
2021-09-14 18:31:35 -07:00
await this . _context . addCookies ( cookies ) ;
}
2022-06-14 22:02:15 -07:00
async _cookies ( url : URL ) : Promise < channels.NetworkCookie [ ] > {
2021-09-29 17:15:32 -07:00
return await this . _context . cookies ( url . toString ( ) ) ;
2021-09-14 18:31:35 -07:00
}
2021-09-30 14:14:29 -07:00
2021-11-05 16:27:49 +01:00
override async storageState ( ) : Promise < channels.APIRequestContextStorageStateResult > {
2021-09-30 14:14:29 -07:00
return this . _context . storageState ( ) ;
}
2021-09-14 18:31:35 -07:00
}
2021-11-05 16:27:49 +01:00
export class GlobalAPIRequestContext extends APIRequestContext {
2021-09-29 17:15:32 -07:00
private readonly _cookieStore : CookieStore = new CookieStore ( ) ;
2021-09-22 12:44:22 -07:00
private readonly _options : FetchRequestOptions ;
2021-09-30 14:14:29 -07:00
private readonly _origins : channels.OriginStorage [ ] | undefined ;
2022-01-22 11:25:13 -08:00
private readonly _tracing : Tracing ;
2021-09-30 14:14:29 -07:00
constructor ( playwright : Playwright , options : channels.PlaywrightNewRequestOptions ) {
2021-09-14 18:31:35 -07:00
super ( playwright ) ;
2022-01-22 11:25:13 -08:00
this . attribution . context = this ;
2021-09-22 12:44:22 -07:00
const timeoutSettings = new TimeoutSettings ( ) ;
if ( options . timeout !== undefined )
timeoutSettings . setDefaultTimeout ( options . timeout ) ;
const proxy = options . proxy ;
if ( proxy ? . server ) {
let url = proxy ? . server . trim ( ) ;
if ( ! /^\w+:\/\// . test ( url ) )
url = 'http://' + url ;
proxy . server = url ;
}
2021-09-30 14:14:29 -07:00
if ( options . storageState ) {
this . _origins = options . storageState . origins ;
2023-09-12 13:11:18 -07:00
this . _cookieStore . addCookies ( options . storageState . cookies || [ ] ) ;
2021-09-30 14:14:29 -07:00
}
2024-07-12 11:42:24 +02:00
verifyClientCertificates ( options . clientCertificates ) ;
2021-09-22 12:44:22 -07:00
this . _options = {
baseURL : options.baseURL ,
2022-01-14 11:46:17 +01:00
userAgent : options.userAgent || getUserAgent ( ) ,
2021-09-22 12:44:22 -07:00
extraHTTPHeaders : options.extraHTTPHeaders ,
ignoreHTTPSErrors : ! ! options . ignoreHTTPSErrors ,
httpCredentials : options.httpCredentials ,
2024-07-12 11:42:24 +02:00
clientCertificates : options.clientCertificates ,
2021-09-22 12:44:22 -07:00
proxy ,
timeoutSettings ,
} ;
2022-01-22 11:25:13 -08:00
this . _tracing = new Tracing ( this , options . tracesDir ) ;
}
2021-09-22 12:44:22 -07:00
2022-01-22 11:25:13 -08:00
override tracing() {
return this . _tracing ;
2021-09-14 18:31:35 -07:00
}
2024-05-13 18:51:30 -07:00
override async dispose ( options : { reason? : string } ) {
this . _closeReason = options . reason ;
2023-11-01 20:17:10 -07:00
await this . _tracing . flush ( ) ;
2022-01-22 11:25:13 -08:00
await this . _tracing . deleteTmpTracesDir ( ) ;
2021-09-15 14:02:55 -07:00
this . _disposeImpl ( ) ;
}
2021-09-14 18:31:35 -07:00
_defaultOptions ( ) : FetchRequestOptions {
2021-09-22 12:44:22 -07:00
return this . _options ;
2021-09-14 18:31:35 -07:00
}
2022-06-14 22:02:15 -07:00
async _addCookies ( cookies : channels.NetworkCookie [ ] ) : Promise < void > {
2021-09-29 17:15:32 -07:00
this . _cookieStore . addCookies ( cookies ) ;
2021-09-14 18:31:35 -07:00
}
2022-06-14 22:02:15 -07:00
async _cookies ( url : URL ) : Promise < channels.NetworkCookie [ ] > {
2021-09-29 17:15:32 -07:00
return this . _cookieStore . cookies ( url ) ;
2021-09-14 18:31:35 -07:00
}
2021-09-30 14:14:29 -07:00
2021-11-05 16:27:49 +01:00
override async storageState ( ) : Promise < channels.APIRequestContextStorageStateResult > {
2021-09-30 14:14:29 -07:00
return {
cookies : this._cookieStore.allCookies ( ) ,
origins : this._origins || [ ]
} ;
}
2021-08-26 16:18:54 -07:00
}
2024-09-16 17:57:33 +02:00
export function createProxyAgent ( proxy : types.ProxySettings ) {
2024-10-08 14:17:50 +02:00
const proxyURL = new URL ( proxy . server ) ;
if ( proxyURL . protocol ? . startsWith ( 'socks' ) )
return new SocksProxyAgent ( proxyURL ) ;
2024-09-16 17:57:33 +02:00
if ( proxy . username )
2024-10-08 14:17:50 +02:00
proxyURL . username = proxy . username ;
if ( proxy . password )
proxyURL . password = proxy . password ;
// TODO: We should use HttpProxyAgent conditional on proxyURL.protocol instead of always using CONNECT method.
return new HttpsProxyAgent ( proxyURL ) ;
2024-09-16 17:57:33 +02:00
}
2021-09-10 14:03:56 -07:00
function toHeadersArray ( rawHeaders : string [ ] ) : types . HeadersArray {
2021-08-26 16:18:54 -07:00
const result : types.HeadersArray = [ ] ;
2021-09-10 14:03:56 -07:00
for ( let i = 0 ; i < rawHeaders . length ; i += 2 )
result . push ( { name : rawHeaders [ i ] , value : rawHeaders [ i + 1 ] } ) ;
2021-08-26 16:18:54 -07:00
return result ;
}
const redirectStatus = [ 301 , 302 , 303 , 307 , 308 ] ;
2022-06-14 22:02:15 -07:00
function parseCookie ( header : string ) : channels . NetworkCookie | null {
2024-09-13 18:29:35 -07:00
const raw = parseRawCookie ( header ) ;
if ( ! raw )
2021-08-24 14:29:04 -07:00
return null ;
2022-06-14 22:02:15 -07:00
const cookie : channels.NetworkCookie = {
2021-08-24 14:29:04 -07:00
domain : '' ,
path : '' ,
expires : - 1 ,
httpOnly : false ,
secure : false ,
2022-09-30 15:01:59 -07:00
// From https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
// The cookie-sending behavior if SameSite is not specified is SameSite=Lax.
2024-09-13 18:29:35 -07:00
sameSite : 'Lax' ,
. . . raw
2021-08-24 14:29:04 -07:00
} ;
return cookie ;
}
2021-09-16 17:48:43 -07:00
2023-03-10 08:58:12 -08:00
function serializePostData ( params : channels.APIRequestContextFetchParams , headers : HeadersObject ) : Buffer | undefined {
2021-10-01 12:11:33 -07:00
assert ( ( params . postData ? 1 : 0 ) + ( params . jsonData ? 1 : 0 ) + ( params . formData ? 1 : 0 ) + ( params . multipartData ? 1 : 0 ) <= 1 , ` Only one of 'data', 'form' or 'multipart' can be specified ` ) ;
2022-01-24 16:06:36 -07:00
if ( params . jsonData !== undefined ) {
2023-03-10 08:58:12 -08:00
setHeader ( headers , 'content-type' , 'application/json' , true ) ;
2023-10-16 16:33:49 -07:00
return Buffer . from ( params . jsonData , 'utf8' ) ;
2021-10-01 12:11:33 -07:00
} else if ( params . formData ) {
2021-09-16 17:48:43 -07:00
const searchParams = new URLSearchParams ( ) ;
2021-10-01 12:11:33 -07:00
for ( const { name , value } of params . formData )
searchParams . append ( name , value ) ;
2023-03-10 08:58:12 -08:00
setHeader ( headers , 'content-type' , 'application/x-www-form-urlencoded' , true ) ;
2021-09-16 17:48:43 -07:00
return Buffer . from ( searchParams . toString ( ) , 'utf8' ) ;
2021-10-01 12:11:33 -07:00
} else if ( params . multipartData ) {
2021-09-16 17:48:43 -07:00
const formData = new MultipartFormData ( ) ;
2021-10-01 12:11:33 -07:00
for ( const field of params . multipartData ) {
if ( field . file )
formData . addFileField ( field . name , field . file ) ;
else if ( field . value )
formData . addField ( field . name , field . value ) ;
2021-09-16 17:48:43 -07:00
}
2023-03-10 08:58:12 -08:00
setHeader ( headers , 'content-type' , formData . contentTypeHeader ( ) , true ) ;
2021-09-16 17:48:43 -07:00
return formData . finish ( ) ;
2022-01-24 16:06:36 -07:00
} else if ( params . postData !== undefined ) {
2023-03-10 08:58:12 -08:00
setHeader ( headers , 'content-type' , 'application/octet-stream' , true ) ;
2022-07-05 08:58:34 -07:00
return params . postData ;
2021-09-16 17:48:43 -07:00
}
2021-10-01 12:11:33 -07:00
return undefined ;
2021-09-16 17:48:43 -07:00
}
2023-03-10 08:58:12 -08:00
function setHeader ( headers : { [ name : string ] : string } , name : string , value : string , keepExisting = false ) {
const existing = Object . entries ( headers ) . find ( pair = > pair [ 0 ] . toLowerCase ( ) === name . toLowerCase ( ) ) ;
if ( ! existing )
headers [ name ] = value ;
else if ( ! keepExisting )
headers [ existing [ 0 ] ] = value ;
}
function getHeader ( headers : HeadersObject , name : string ) {
const existing = Object . entries ( headers ) . find ( pair = > pair [ 0 ] . toLowerCase ( ) === name . toLowerCase ( ) ) ;
return existing ? existing [ 1 ] : undefined ;
}
function removeHeader ( headers : { [ name : string ] : string } , name : string ) {
delete headers [ name ] ;
}
2023-05-09 14:51:49 -07:00
function shouldBypassProxy ( url : URL , bypass? : string ) : boolean {
if ( ! bypass )
return false ;
const domains = bypass . split ( ',' ) . map ( s = > {
s = s . trim ( ) ;
if ( ! s . startsWith ( '.' ) )
s = '.' + s ;
return s ;
} ) ;
const domain = '.' + url . hostname ;
return domains . some ( d = > domain . endsWith ( d ) ) ;
2024-05-02 16:30:12 -07:00
}
function setBasicAuthorizationHeader ( headers : { [ name : string ] : string } , credentials : HTTPCredentials ) {
const { username , password } = credentials ;
const encoded = Buffer . from ( ` ${ username || '' } : ${ password || '' } ` ) . toString ( 'base64' ) ;
setHeader ( headers , 'authorization' , ` Basic ${ encoded } ` ) ;
}