2019-11-26 11:23:13 -08:00
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
|
|
// Licensed under the MIT license.
|
|
|
|
|
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-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-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-11-27 12:44:12 -08:00
|
|
|
|
2019-12-14 08:20:51 -08:00
|
|
|
constructor(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) {
|
|
|
|
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-11-27 12:44:12 -08:00
|
|
|
this._url = url;
|
|
|
|
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 {
|
|
|
|
if (!this._failureText)
|
|
|
|
return null;
|
|
|
|
return {
|
|
|
|
errorText: this._failureText
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|