2020-01-06 18:22:35 -08: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 .
* /
2019-11-26 11:23:13 -08:00
2019-11-27 12:44:12 -08:00
import * as frames from './frames' ;
2019-12-02 16:36:46 -08:00
import { assert } from './helper' ;
2019-11-27 12:44:12 -08:00
2019-11-26 11:23:13 -08:00
export type NetworkCookie = {
name : string ,
value : string ,
domain : string ,
path : string ,
expires : number ,
httpOnly : boolean ,
secure : boolean ,
session : boolean ,
sameSite : 'Strict' | 'Lax' | 'None'
} ;
export type SetNetworkCookieParam = {
name : string ,
value : string ,
url? : string ,
domain? : string ,
path? : string ,
expires? : number ,
httpOnly? : boolean ,
secure? : boolean ,
sameSite ? : 'Strict' | 'Lax' | 'None'
} ;
2019-12-02 16:48:38 -08:00
export function filterCookies ( cookies : NetworkCookie [ ] , urls : string [ ] ) : NetworkCookie [ ] {
2019-11-26 11:23:13 -08:00
const parsedURLs = urls . map ( s = > new URL ( s ) ) ;
// Chromiums's cookies are missing sameSite when it is 'None'
return cookies . filter ( c = > {
if ( ! parsedURLs . length )
return true ;
for ( const parsedURL of parsedURLs ) {
if ( parsedURL . hostname !== c . domain )
continue ;
if ( ! parsedURL . pathname . startsWith ( c . path ) )
continue ;
if ( ( parsedURL . protocol === 'https:' ) !== c . secure )
continue ;
return true ;
}
return false ;
} ) ;
}
2019-11-27 12:44:12 -08:00
2019-12-02 16:36:46 -08:00
export function rewriteCookies ( cookies : SetNetworkCookieParam [ ] ) : SetNetworkCookieParam [ ] {
return cookies . map ( c = > {
2019-12-03 10:51:41 -08:00
assert ( c . name , 'Cookie should have a name' ) ;
assert ( c . value , 'Cookie should have a value' ) ;
assert ( c . url || ( c . domain && c . path ) , 'Cookie should have a url or a domain/path pair' ) ;
assert ( ! ( c . url && c . domain ) , 'Cookie should have either url or domain' ) ;
assert ( ! ( c . url && c . path ) , 'Cookie should have either url or domain' ) ;
2019-12-02 16:36:46 -08:00
const copy = { . . . c } ;
if ( copy . url ) {
assert ( copy . url !== 'about:blank' , ` Blank page can not have cookie " ${ c . name } " ` ) ;
assert ( ! copy . url . startsWith ( 'data:' ) , ` Data URL page can not have cookie " ${ c . name } " ` ) ;
const url = new URL ( copy . url ) ;
copy . domain = url . hostname ;
copy . path = url . pathname . substring ( 0 , url . pathname . lastIndexOf ( '/' ) + 1 ) ;
copy . secure = url . protocol === 'https:' ;
}
return copy ;
} ) ;
}
2019-12-19 15:07:57 -08:00
function stripFragmentFromUrl ( url : string ) : string {
2019-12-18 17:27:20 -07:00
if ( ! url . indexOf ( '#' ) )
return url ;
const parsed = new URL ( url ) ;
parsed . hash = '' ;
return parsed . href ;
}
2019-11-27 12:44:12 -08:00
export type Headers = { [ key : string ] : string } ;
2019-11-27 16:02:31 -08:00
export class Request {
2019-12-30 14:05:28 -08:00
private _delegate : RequestDelegate | null ;
2019-12-11 16:19:37 -08:00
private _response : Response | null = null ;
2019-11-27 16:02:31 -08:00
_redirectChain : Request [ ] ;
2019-12-14 08:20:51 -08:00
_finalRequest : Request ;
readonly _documentId? : string ;
2019-11-27 12:44:12 -08:00
private _failureText : string | null = null ;
private _url : string ;
private _resourceType : string ;
private _method : string ;
private _postData : string ;
private _headers : Headers ;
2019-11-27 16:02:31 -08:00
private _frame : frames.Frame ;
2019-12-11 16:19:37 -08:00
private _waitForResponsePromise : Promise < Response > ;
private _waitForResponsePromiseCallback : ( value? : Response ) = > void ;
2019-12-11 17:46:26 -08:00
private _waitForFinishedPromise : Promise < Response | undefined > ;
private _waitForFinishedPromiseCallback : ( value? : Response | undefined ) = > void ;
2019-12-30 14:05:28 -08:00
private _interceptionHandled = false ;
2019-11-27 12:44:12 -08:00
2019-12-30 14:05:28 -08:00
constructor ( delegate : RequestDelegate | null , frame : frames.Frame | null , redirectChain : Request [ ] , documentId : string ,
2019-11-27 12:44:12 -08:00
url : string , resourceType : string , method : string , postData : string , headers : Headers ) {
2019-12-30 14:05:28 -08:00
this . _delegate = delegate ;
2019-11-27 12:44:12 -08:00
this . _frame = frame ;
this . _redirectChain = redirectChain ;
2019-12-14 08:20:51 -08:00
this . _finalRequest = this ;
for ( const request of redirectChain )
request . _finalRequest = this ;
this . _documentId = documentId ;
2019-12-18 17:27:20 -07:00
this . _url = stripFragmentFromUrl ( url ) ;
2019-11-27 12:44:12 -08:00
this . _resourceType = resourceType ;
this . _method = method ;
this . _postData = postData ;
this . _headers = headers ;
2019-12-11 16:19:37 -08:00
this . _waitForResponsePromise = new Promise ( f = > this . _waitForResponsePromiseCallback = f ) ;
2019-12-11 17:46:26 -08:00
this . _waitForFinishedPromise = new Promise ( f = > this . _waitForFinishedPromiseCallback = f ) ;
2019-11-27 12:44:12 -08:00
}
2019-12-16 16:32:04 -08:00
_setFailureText ( failureText : string ) {
2019-11-27 12:44:12 -08:00
this . _failureText = failureText ;
2019-12-11 17:46:26 -08:00
this . _waitForFinishedPromiseCallback ( ) ;
2019-11-27 12:44:12 -08:00
}
url ( ) : string {
return this . _url ;
}
resourceType ( ) : string {
return this . _resourceType ;
}
method ( ) : string {
return this . _method ;
}
postData ( ) : string | undefined {
return this . _postData ;
}
headers ( ) : { [ key : string ] : string } {
return this . _headers ;
}
2019-11-27 16:02:31 -08:00
response ( ) : Response | null {
2019-11-27 12:44:12 -08:00
return this . _response ;
}
2019-12-11 17:46:26 -08:00
async _waitForFinished ( ) : Promise < Response | undefined > {
return this . _waitForFinishedPromise ;
}
async _waitForResponse ( ) : Promise < Response > {
2019-12-11 16:19:37 -08:00
const response = await this . _waitForResponsePromise ;
2019-12-11 17:46:26 -08:00
await response . _finishedPromise ;
2019-12-11 16:19:37 -08:00
return response ;
}
_setResponse ( response : Response ) {
this . _response = response ;
this . _waitForResponsePromiseCallback ( response ) ;
2019-12-11 17:46:26 -08:00
response . _finishedPromise . then ( ( ) = > this . _waitForFinishedPromiseCallback ( response ) ) ;
2019-12-11 16:19:37 -08:00
}
2019-11-27 16:02:31 -08:00
frame ( ) : frames . Frame | null {
2019-11-27 12:44:12 -08:00
return this . _frame ;
}
isNavigationRequest ( ) : boolean {
2019-12-14 08:20:51 -08:00
return ! ! this . _documentId ;
2019-11-27 12:44:12 -08:00
}
2019-11-27 16:02:31 -08:00
redirectChain ( ) : Request [ ] {
2019-11-27 12:44:12 -08:00
return this . _redirectChain . slice ( ) ;
}
failure ( ) : { errorText : string ; } | null {
2019-12-30 14:05:28 -08:00
if ( this . _failureText === null )
2019-11-27 12:44:12 -08:00
return null ;
return {
errorText : this._failureText
} ;
}
2019-12-30 14:05:28 -08:00
async abort ( errorCode : string = 'failed' ) {
// Request interception is not supported for data: urls.
if ( this . url ( ) . startsWith ( 'data:' ) )
return ;
assert ( this . _delegate , 'Request Interception is not enabled!' ) ;
assert ( ! this . _interceptionHandled , 'Request is already handled!' ) ;
this . _interceptionHandled = true ;
await this . _delegate . abort ( errorCode ) ;
}
async fulfill ( response : { status : number ; headers : { [ key : string ] : string } ; contentType : string ; body : ( string | Buffer ) ; } ) { // Mocking responses for dataURL requests is not currently supported.
if ( this . url ( ) . startsWith ( 'data:' ) )
return ;
assert ( this . _delegate , 'Request Interception is not enabled!' ) ;
assert ( ! this . _interceptionHandled , 'Request is already handled!' ) ;
this . _interceptionHandled = true ;
await this . _delegate . fulfill ( response ) ;
}
async continue ( overrides : { headers ? : { [ key : string ] : string } } = { } ) {
// Request interception is not supported for data: urls.
if ( this . url ( ) . startsWith ( 'data:' ) )
return ;
assert ( this . _delegate , 'Request Interception is not enabled!' ) ;
assert ( ! this . _interceptionHandled , 'Request is already handled!' ) ;
await this . _delegate . continue ( overrides ) ;
}
2019-11-27 12:44:12 -08:00
}
export type RemoteAddress = {
ip : string ,
port : number ,
} ;
type GetResponseBodyCallback = ( ) = > Promise < Buffer > ;
2019-11-27 16:02:31 -08:00
export class Response {
private _request : Request ;
2019-11-27 12:44:12 -08:00
private _contentPromise : Promise < Buffer > | null = null ;
2019-12-11 17:46:26 -08:00
_finishedPromise : Promise < Error | null > ;
private _finishedPromiseCallback : any ;
2019-11-27 12:44:12 -08:00
private _remoteAddress : RemoteAddress ;
private _status : number ;
private _statusText : string ;
private _url : string ;
private _headers : Headers ;
private _getResponseBodyCallback : GetResponseBodyCallback ;
2019-11-27 16:02:31 -08:00
constructor ( request : Request , status : number , statusText : string , headers : Headers , remoteAddress : RemoteAddress , getResponseBodyCallback : GetResponseBodyCallback ) {
2019-11-27 12:44:12 -08:00
this . _request = request ;
this . _status = status ;
this . _statusText = statusText ;
this . _url = request . url ( ) ;
this . _headers = headers ;
this . _remoteAddress = remoteAddress ;
this . _getResponseBodyCallback = getResponseBodyCallback ;
2019-12-11 17:46:26 -08:00
this . _finishedPromise = new Promise ( f = > {
this . _finishedPromiseCallback = f ;
2019-11-27 12:44:12 -08:00
} ) ;
2019-12-11 17:46:26 -08:00
this . _request . _setResponse ( this ) ;
2019-11-27 12:44:12 -08:00
}
2019-12-11 16:19:37 -08:00
_requestFinished ( error? : Error ) {
2019-12-11 17:46:26 -08:00
this . _finishedPromiseCallback . call ( null , error ) ;
2019-11-27 12:44:12 -08:00
}
remoteAddress ( ) : RemoteAddress {
return this . _remoteAddress ;
}
url ( ) : string {
return this . _url ;
}
ok ( ) : boolean {
return this . _status === 0 || ( this . _status >= 200 && this . _status <= 299 ) ;
}
status ( ) : number {
return this . _status ;
}
statusText ( ) : string {
return this . _statusText ;
}
headers ( ) : object {
return this . _headers ;
}
buffer ( ) : Promise < Buffer > {
if ( ! this . _contentPromise ) {
2019-12-11 17:46:26 -08:00
this . _contentPromise = this . _finishedPromise . then ( async error = > {
2019-11-27 12:44:12 -08:00
if ( error )
throw error ;
return this . _getResponseBodyCallback ( ) ;
} ) ;
}
return this . _contentPromise ;
}
async text ( ) : Promise < string > {
const content = await this . buffer ( ) ;
return content . toString ( 'utf8' ) ;
}
async json ( ) : Promise < object > {
const content = await this . text ( ) ;
return JSON . parse ( content ) ;
}
2019-11-27 16:02:31 -08:00
request ( ) : Request {
2019-11-27 12:44:12 -08:00
return this . _request ;
}
2019-11-27 16:02:31 -08:00
frame ( ) : frames . Frame | null {
2019-11-27 12:44:12 -08:00
return this . _request . frame ( ) ;
}
}
2019-12-30 14:05:28 -08:00
export interface RequestDelegate {
abort ( errorCode : string ) : Promise < void > ;
fulfill ( response : { status : number ; headers : { [ key : string ] : string } ; contentType : string ; body : ( string | Buffer ) ; } ) : Promise < void > ;
continue ( overrides : { url? : string ; method? : string ; postData? : string ; headers ? : { [ key : string ] : string ; } ; } ) : Promise < void > ;
}
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
export const STATUS_TEXTS : { [ status : string ] : string } = {
'100' : 'Continue' ,
'101' : 'Switching Protocols' ,
'102' : 'Processing' ,
'103' : 'Early Hints' ,
'200' : 'OK' ,
'201' : 'Created' ,
'202' : 'Accepted' ,
'203' : 'Non-Authoritative Information' ,
'204' : 'No Content' ,
'205' : 'Reset Content' ,
'206' : 'Partial Content' ,
'207' : 'Multi-Status' ,
'208' : 'Already Reported' ,
'226' : 'IM Used' ,
'300' : 'Multiple Choices' ,
'301' : 'Moved Permanently' ,
'302' : 'Found' ,
'303' : 'See Other' ,
'304' : 'Not Modified' ,
'305' : 'Use Proxy' ,
'306' : 'Switch Proxy' ,
'307' : 'Temporary Redirect' ,
'308' : 'Permanent Redirect' ,
'400' : 'Bad Request' ,
'401' : 'Unauthorized' ,
'402' : 'Payment Required' ,
'403' : 'Forbidden' ,
'404' : 'Not Found' ,
'405' : 'Method Not Allowed' ,
'406' : 'Not Acceptable' ,
'407' : 'Proxy Authentication Required' ,
'408' : 'Request Timeout' ,
'409' : 'Conflict' ,
'410' : 'Gone' ,
'411' : 'Length Required' ,
'412' : 'Precondition Failed' ,
'413' : 'Payload Too Large' ,
'414' : 'URI Too Long' ,
'415' : 'Unsupported Media Type' ,
'416' : 'Range Not Satisfiable' ,
'417' : 'Expectation Failed' ,
'418' : 'I\'m a teapot' ,
'421' : 'Misdirected Request' ,
'422' : 'Unprocessable Entity' ,
'423' : 'Locked' ,
'424' : 'Failed Dependency' ,
'425' : 'Too Early' ,
'426' : 'Upgrade Required' ,
'428' : 'Precondition Required' ,
'429' : 'Too Many Requests' ,
'431' : 'Request Header Fields Too Large' ,
'451' : 'Unavailable For Legal Reasons' ,
'500' : 'Internal Server Error' ,
'501' : 'Not Implemented' ,
'502' : 'Bad Gateway' ,
'503' : 'Service Unavailable' ,
'504' : 'Gateway Timeout' ,
'505' : 'HTTP Version Not Supported' ,
'506' : 'Variant Also Negotiates' ,
'507' : 'Insufficient Storage' ,
'508' : 'Loop Detected' ,
'510' : 'Not Extended' ,
'511' : 'Network Authentication Required' ,
} ;