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' ;
2021-08-26 16:18:54 -07:00
import * as http from 'http' ;
import * as 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 url from 'url' ;
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' ;
import { getUserAgent } from '../common/userAgent' ;
import { assert , createGuid , monotonicTime } from '../utils' ;
2023-01-05 14:39:49 -08:00
import { HttpsProxyAgent , SocksProxyAgent } from '../utilsBundle' ;
2021-09-22 12:44:22 -07:00
import { BrowserContext } from './browserContext' ;
2021-09-29 17:15:32 -07:00
import { CookieStore , domainMatches } from './cookieStore' ;
2021-09-22 12:44:22 -07:00
import { MultipartFormData } from './formData' ;
2023-01-05 14:39:49 -08:00
import { httpHappyEyeballsAgent , httpsHappyEyeballsAgent } from './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' ;
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 ;
} ;
2021-08-24 14:29:04 -07:00
2021-12-02 15:53:47 -08:00
export type APIRequestEvent = {
url : URL ,
method : string ,
headers : { [ name : string ] : string } ,
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 ;
} ;
2023-01-05 14:39:49 -08:00
export type SendRequestOptions = https . RequestOptions & {
maxRedirects : number ,
deadline : number ,
__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 > ( ) ;
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 ;
abstract dispose ( ) : 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 headers : { [ name : string ] : string } = { } ;
const defaults = this . _defaultOptions ( ) ;
headers [ 'user-agent' ] = defaults . userAgent ;
headers [ 'accept' ] = '*/*' ;
headers [ 'accept-encoding' ] = 'gzip,deflate,br' ;
if ( defaults . extraHTTPHeaders ) {
for ( const { name , value } of defaults . extraHTTPHeaders )
headers [ name . toLowerCase ( ) ] = value ;
}
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 )
headers [ name . toLowerCase ( ) ] = value ;
}
2021-09-14 18:31:35 -07:00
2021-11-19 20:32:29 -08:00
const method = params . method ? . toUpperCase ( ) || 'GET' ;
const proxy = defaults . proxy ;
let agent ;
2022-04-14 12:53:49 +02:00
if ( proxy && proxy . server !== 'per-context' ) {
2021-11-19 20:32:29 -08:00
// TODO: support bypass proxy
const proxyOpts = url . parse ( proxy . server ) ;
if ( proxyOpts . protocol ? . startsWith ( 'socks' ) ) {
agent = new SocksProxyAgent ( {
host : proxyOpts.hostname ,
port : proxyOpts.port || undefined ,
} ) ;
} else {
if ( proxy . username )
proxyOpts . auth = ` ${ proxy . username } : ${ proxy . password || '' } ` ;
agent = new HttpsProxyAgent ( proxyOpts ) ;
2021-09-14 18:31:35 -07:00
}
2021-11-19 20:32:29 -08:00
}
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 ,
__testHookLookup : ( params as any ) . __testHookLookup ,
2021-11-19 20:32:29 -08:00
} ;
// rejectUnauthorized = undefined is treated as true in node 12.
if ( params . ignoreHTTPSErrors || defaults . ignoreHTTPSErrors )
options . rejectUnauthorized = false ;
const requestUrl = new URL ( params . url , defaults . baseURL ) ;
if ( params . params ) {
for ( const { name , value } of params . params )
requestUrl . searchParams . set ( name , value ) ;
2021-09-14 18:31:35 -07:00
}
2021-11-19 20:32:29 -08:00
2022-09-01 08:36:08 -07:00
const postData = serializePostData ( params , headers ) ;
2021-11-19 20:32:29 -08:00
if ( postData )
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 = > {
return this . _sendRequest ( progress , requestUrl , options , postData ) ;
} ) ;
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 ) ;
2021-11-19 20:32:29 -08:00
if ( params . failOnStatusCode && ( fetchResponse . status < 200 || fetchResponse . status >= 400 ) )
throw new Error ( ` ${ fetchResponse . status } ${ fetchResponse . statusText } ` ) ;
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
}
private async _updateRequestCookieHeader ( url : URL , options : http.RequestOptions ) {
if ( options . headers ! [ 'cookie' ] !== undefined )
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 } ` ) ;
options . headers ! [ 'cookie' ] = valueArray . join ( '; ' ) ;
}
}
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 } > {
2021-09-14 18:31:35 -07:00
await this . _updateRequestCookieHeader ( url , options ) ;
2021-12-02 15:53:47 -08:00
const requestCookies = ( options . headers ! [ 'cookie' ] as ( string | undefined ) ) ? . split ( ';' ) . map ( p = > {
const [ name , value ] = p . split ( '=' ) . map ( v = > v . trim ( ) ) ;
return { name , value } ;
} ) || [ ] ;
const requestEvent : APIRequestEvent = {
url ,
method : options.method ! ,
headers : options.headers as { [ name : string ] : string } ,
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 } ;
const request = requestConstructor ( url , requestOptions as any , async response = > {
2021-12-02 15:53:47 -08:00
const notifyRequestFinished = ( body? : Buffer ) = > {
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 ,
body
} ;
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' ] ) ;
if ( cookies . length )
await this . _addCookies ( cookies ) ;
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 ;
}
const headers = { . . . options . headers } ;
delete headers [ ` cookie ` ] ;
// 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 ;
delete headers [ ` content-encoding ` ] ;
delete headers [ ` content-language ` ] ;
2021-10-22 16:52:49 -07:00
delete headers [ ` content-length ` ] ;
2021-09-14 18:31:35 -07:00
delete headers [ ` content-location ` ] ;
delete headers [ ` content-type ` ] ;
}
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 ,
__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.
if ( response . headers . location ) {
const locationURL = new URL ( response . headers . location , url ) ;
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
}
2021-09-14 18:31:35 -07:00
if ( response . statusCode === 401 && ! options . headers ! [ 'authorization' ] ) {
const auth = response . headers [ 'www-authenticate' ] ;
const credentials = this . _defaultOptions ( ) . httpCredentials ;
2021-12-16 13:40:52 -08:00
if ( auth ? . trim ( ) . startsWith ( 'Basic' ) && credentials ) {
2021-09-27 18:58:08 +02:00
const { username , password } = credentials ;
2021-09-14 18:31:35 -07:00
const encoded = Buffer . from ( ` ${ username || '' } : ${ password || '' } ` ) . toString ( 'base64' ) ;
options . headers ! [ 'authorization' ] = ` Basic ${ encoded } ` ;
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' ) {
transform = zlib . createBrotliDecompress ( ) ;
} 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 )
reject ( new Error ( ` failed to decompress ' ${ encoding } ' encoding: ${ e } ` ) ) ;
} ) ;
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
} ) ;
2021-09-14 18:31:35 -07:00
request . on ( 'error' , reject ) ;
2021-09-22 19:30:56 +02:00
2021-10-18 19:41:56 -07:00
const disposeListener = ( ) = > {
reject ( new Error ( 'Request context disposed.' ) ) ;
request . destroy ( ) ;
} ;
2021-11-05 16:27:49 +01:00
this . on ( APIRequestContext . Events . Dispose , disposeListener ) ;
request . on ( 'close' , ( ) = > this . off ( APIRequestContext . Events . Dispose , disposeListener ) ) ;
2021-10-18 19:41:56 -07:00
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
}
}
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 ;
}
override async dispose() {
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 ,
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 ;
this . _cookieStore . addCookies ( options . storageState . cookies ) ;
}
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 ,
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
}
2022-01-22 11:25:13 -08:00
override async dispose() {
2022-08-31 21:51:38 -07:00
await this . _tracing . dispose ( ) ;
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
}
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 {
2022-01-26 20:27:43 +01:00
const pairs = header . split ( ';' ) . filter ( s = > s . trim ( ) . length > 0 ) . map ( p = > {
let key = '' ;
let value = '' ;
const separatorPos = p . indexOf ( '=' ) ;
if ( separatorPos === - 1 ) {
// If only a key is specified, the value is left undefined.
key = p . trim ( ) ;
} else {
// Otherwise we assume that the key is the element before the first `=`
key = p . slice ( 0 , separatorPos ) . trim ( ) ;
// And the value is the rest of the string.
value = p . slice ( separatorPos + 1 ) . trim ( ) ;
}
return [ key , value ] ;
} ) ;
2021-08-24 14:29:04 -07:00
if ( ! pairs . length )
return null ;
const [ name , value ] = pairs [ 0 ] ;
2022-06-14 22:02:15 -07:00
const cookie : channels.NetworkCookie = {
2021-08-24 14:29:04 -07:00
name ,
value ,
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.
sameSite : 'Lax'
2021-08-24 14:29:04 -07:00
} ;
for ( let i = 1 ; i < pairs . length ; i ++ ) {
const [ name , value ] = pairs [ i ] ;
switch ( name . toLowerCase ( ) ) {
case 'expires' :
const expiresMs = ( + new Date ( value ) ) ;
if ( isFinite ( expiresMs ) )
cookie . expires = expiresMs / 1000 ;
break ;
case 'max-age' :
const maxAgeSec = parseInt ( value , 10 ) ;
if ( isFinite ( maxAgeSec ) )
cookie . expires = Date . now ( ) / 1000 + maxAgeSec ;
break ;
case 'domain' :
2021-09-29 17:15:32 -07:00
cookie . domain = value . toLocaleLowerCase ( ) || '' ;
2022-11-23 09:22:49 -08:00
if ( cookie . domain && ! cookie . domain . startsWith ( '.' ) && cookie . domain . includes ( '.' ) )
2021-09-29 17:15:32 -07:00
cookie . domain = '.' + cookie . domain ;
2021-08-24 14:29:04 -07:00
break ;
case 'path' :
cookie . path = value || '' ;
break ;
case 'secure' :
cookie . secure = true ;
break ;
case 'httponly' :
cookie . httpOnly = true ;
break ;
2022-09-30 15:01:59 -07:00
case 'samesite' :
switch ( value . toLowerCase ( ) ) {
case 'none' :
cookie . sameSite = 'None' ;
break ;
case 'lax' :
cookie . sameSite = 'Lax' ;
break ;
case 'strict' :
cookie . sameSite = 'Strict' ;
break ;
}
break ;
2021-08-24 14:29:04 -07:00
}
}
return cookie ;
}
2021-09-16 17:48:43 -07:00
2021-11-11 11:12:24 -08:00
function isJsonParsable ( value : any ) {
if ( typeof value !== 'string' )
return false ;
try {
JSON . parse ( value ) ;
return true ;
} catch ( e ) {
if ( e instanceof SyntaxError )
return false ;
else
throw e ;
}
}
2021-11-05 16:27:49 +01:00
function serializePostData ( params : channels.APIRequestContextFetchParams , headers : { [ name : string ] : string } ) : 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 ) {
2021-11-11 11:12:24 -08:00
const json = isJsonParsable ( params . jsonData ) ? params.jsonData : JSON.stringify ( params . jsonData ) ;
2021-10-01 12:11:33 -07:00
headers [ 'content-type' ] ? ? = 'application/json' ;
2021-09-16 17:48:43 -07:00
return Buffer . from ( json , '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 ) ;
headers [ 'content-type' ] ? ? = 'application/x-www-form-urlencoded' ;
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
}
2021-10-01 12:11:33 -07:00
headers [ 'content-type' ] ? ? = formData . contentTypeHeader ( ) ;
2021-09-16 17:48:43 -07:00
return formData . finish ( ) ;
2022-01-24 16:06:36 -07:00
} else if ( params . postData !== undefined ) {
2021-10-01 12:11:33 -07:00
headers [ 'content-type' ] ? ? = 'application/octet-stream' ;
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
}