Skip to content

Commit

Permalink
add stub cookie strategy for proxyless mode (#2797)
Browse files Browse the repository at this point in the history
* refactor

* lint
  • Loading branch information
AlexKamaev authored Oct 6, 2022
1 parent 2d339a8 commit 76d571e
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 82 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "testcafe-hammerhead",
"description": "A powerful web-proxy used as a core for the TestCafe testing framework (https://github.com/DevExpress/testcafe).",
"version": "25.0.0",
"version": "25.0.1",
"homepage": "https://github.com/DevExpress/testcafe-hammerhead",
"bugs": {
"url": "https://github.com/DevExpress/testcafe-hammerhead/issues"
Expand Down
218 changes: 142 additions & 76 deletions src/client/sandbox/cookie/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,80 +21,49 @@ import getTopOpenerWindow from '../../utils/get-top-opener-window';

const MIN_DATE_VALUE = new nativeMethods.date(0).toUTCString(); // eslint-disable-line new-cap

export default class CookieSandbox extends SandboxBase {
private readonly _windowSync: WindowSync;
interface CookieSandboxStrategy {
getCookie: () => string;

constructor (messageSandbox: MessageSandbox,
private readonly _unloadSandbox: UnloadSandbox,
childWindowSandbox: ChildWindowSandbox) {
super();
setCookie: (cookie: CookieRecord | string) => void;

this._windowSync = new WindowSync(this, messageSandbox, childWindowSandbox);
}

private static _removeAllSyncCookie (): void {
const cookies = nativeMethods.documentCookieGetter.call(document);
const parsedCookies = parseClientSyncCookieStr(cookies);
const sessionId = settings.get().sessionId;
syncCookie: () => void;

for (const outdatedCookie of parsedCookies.outdated)
nativeMethods.documentCookieSetter.call(document, generateDeleteSyncCookieStr(outdatedCookie));
syncWindowCookie: (parsedCookies: CookieRecord[]) => void;

for (const parsedCookie of parsedCookies.actual) {
if (parsedCookie.sid === sessionId && (parsedCookie.isWindowSync || parsedCookie.isServerSync)) {
nativeMethods.documentCookieSetter.call(document, generateDeleteSyncCookieStr(parsedCookie));
removeAllSyncCookie: () => void;
}

if (parsedCookie.isClientSync) {
changeSyncType(parsedCookie, { window: false });
nativeMethods.documentCookieSetter.call(document, formatSyncCookie(parsedCookie));
}
}
}
class CookieSandboxStrategyFactory {
static create (proxyless: boolean, document: Document | null, windowSync: WindowSync) {
return proxyless ? new CookieSandboxProxylessStrategy() : new CookieSandboxProxyStrategy(document, windowSync);
}
}

_canSetCookie (cookie, setByClient: boolean): boolean {
// eslint-disable-next-line no-restricted-properties
if (setByClient && (cookie.length > BYTES_PER_COOKIE_LIMIT || destLocation.getParsed().protocol === 'file:'))
return false;

const clientCookie = `key${nativeMethods.mathRandom.call(nativeMethods.math)}=value`;

nativeMethods.documentCookieSetter.call(this.document, clientCookie);

const documentCookieIsEmpty = !nativeMethods.documentCookieGetter.call(this.document);

if (!documentCookieIsEmpty)
nativeMethods.documentCookieSetter.call(this.document, `${clientCookie};expires=${MIN_DATE_VALUE}`);

return !documentCookieIsEmpty;
class CookieSandboxProxylessStrategy implements CookieSandboxStrategy {
getCookie (): string {
return '';
}
setCookie (): void {
return void 0;
}
syncCookie (): void {
return void 0;
}
syncWindowCookie (): void {
return void 0;
}
removeAllSyncCookie (): void {
return void 0;
}
}

static _updateClientCookieStr (cookieKey, newCookieStr: string): void {
const cookieStr = settings.get().cookie; // eslint-disable-line no-restricted-properties
const cookies = cookieStr ? cookieStr.split(';') : [];
const changedCookies = [];
let replaced = false;
const searchStr = cookieKey === '' ? null : cookieKey + '=';

// NOTE: Replace a cookie if it already exists.
for (let cookie of cookies) {
cookie = trim(cookie);

const isCookieExists = searchStr ? cookie.indexOf(searchStr) === 0 : cookie.indexOf('=') === -1;

if (!isCookieExists)
changedCookies.push(cookie);
else if (newCookieStr !== null) {
changedCookies.push(newCookieStr);

replaced = true;
}
}

if (!replaced && newCookieStr !== null)
changedCookies.push(newCookieStr);
class CookieSandboxProxyStrategy implements CookieSandboxStrategy {
document: Document | null = null;
private readonly _windowSync: WindowSync;

settings.get().cookie = changedCookies.join('; '); // eslint-disable-line no-restricted-properties
constructor (document: Document | null, _windowSync: WindowSync) {
this.document = document;
this._windowSync = _windowSync;
}

getCookie (): string {
Expand Down Expand Up @@ -194,6 +163,52 @@ export default class CookieSandbox extends SandboxBase {
this._windowSync.syncBetweenWindows(parsedCookies);
}

syncWindowCookie (parsedCookies: CookieRecord[]): void {
const clientCookie = nativeMethods.documentCookieGetter.call(this.document);

for (const parsedCookie of parsedCookies) {
if (CookieSandbox.isSyncCookieExists(parsedCookie, clientCookie))
this.setCookie(parsedCookie);
}
}

removeAllSyncCookie (): void {
const cookies = nativeMethods.documentCookieGetter.call(this.document);
const parsedCookies = parseClientSyncCookieStr(cookies);
const sessionId = settings.get().sessionId;

for (const outdatedCookie of parsedCookies.outdated)
nativeMethods.documentCookieSetter.call(this.document, generateDeleteSyncCookieStr(outdatedCookie));

for (const parsedCookie of parsedCookies.actual) {
if (parsedCookie.sid === sessionId && (parsedCookie.isWindowSync || parsedCookie.isServerSync)) {
nativeMethods.documentCookieSetter.call(this.document, generateDeleteSyncCookieStr(parsedCookie));

if (parsedCookie.isClientSync) {
changeSyncType(parsedCookie, { window: false });
nativeMethods.documentCookieSetter.call(this.document, formatSyncCookie(parsedCookie));
}
}
}
}

_canSetCookie (cookie, setByClient: boolean): boolean {
// eslint-disable-next-line no-restricted-properties
if (setByClient && (cookie.length > BYTES_PER_COOKIE_LIMIT || destLocation.getParsed().protocol === 'file:'))
return false;

const clientCookie = `key${nativeMethods.mathRandom.call(nativeMethods.math)}=value`;

nativeMethods.documentCookieSetter.call(this.document, clientCookie);

const documentCookieIsEmpty = !nativeMethods.documentCookieGetter.call(this.document);

if (!documentCookieIsEmpty)
nativeMethods.documentCookieSetter.call(this.document, `${clientCookie};expires=${MIN_DATE_VALUE}`);

return !documentCookieIsEmpty;
}

_syncClientCookie (parsedCookie: CookieRecord): void {
parsedCookie.isClientSync = true;
parsedCookie.isWindowSync = true;
Expand All @@ -206,33 +221,84 @@ export default class CookieSandbox extends SandboxBase {

this._windowSync.syncBetweenWindows([parsedCookie]);
}
}

static isSyncCookieExists (parsedCookie: CookieRecord, clientCookieStr: string): boolean {
const startIndex = clientCookieStr.indexOf(parsedCookie.cookieStr);
const endIndex = startIndex + parsedCookie.cookieStr.length;
export default class CookieSandbox extends SandboxBase {
private readonly _windowSync: WindowSync;
private _cookieStrategy: CookieSandboxStrategy;

return startIndex > -1 && (clientCookieStr.length === endIndex || clientCookieStr.charAt(endIndex) === ';');
constructor (messageSandbox: MessageSandbox,
private readonly _unloadSandbox: UnloadSandbox,
childWindowSandbox: ChildWindowSandbox) {
super();

this._windowSync = new WindowSync(this, messageSandbox, childWindowSandbox);
}

syncWindowCookie (parsedCookies: CookieRecord[]): void {
const clientCookie = nativeMethods.documentCookieGetter.call(this.document);
static _updateClientCookieStr (cookieKey, newCookieStr: string): void {
const cookieStr = settings.get().cookie; // eslint-disable-line no-restricted-properties
const cookies = cookieStr ? cookieStr.split(';') : [];
const changedCookies = [];
let replaced = false;
const searchStr = cookieKey === '' ? null : cookieKey + '=';

for (const parsedCookie of parsedCookies) {
if (CookieSandbox.isSyncCookieExists(parsedCookie, clientCookie))
this.setCookie(parsedCookie);
// NOTE: Replace a cookie if it already exists.
for (let cookie of cookies) {
cookie = trim(cookie);

const isCookieExists = searchStr ? cookie.indexOf(searchStr) === 0 : cookie.indexOf('=') === -1;

if (!isCookieExists)
changedCookies.push(cookie);
else if (newCookieStr !== null) {
changedCookies.push(newCookieStr);

replaced = true;
}
}

if (!replaced && newCookieStr !== null)
changedCookies.push(newCookieStr);

settings.get().cookie = changedCookies.join('; '); // eslint-disable-line no-restricted-properties
}

getWindowSync (): WindowSync {
return this._windowSync;
static isSyncCookieExists (parsedCookie: CookieRecord, clientCookieStr: string): boolean {
const startIndex = clientCookieStr.indexOf(parsedCookie.cookieStr);
const endIndex = startIndex + parsedCookie.cookieStr.length;

return startIndex > -1 && (clientCookieStr.length === endIndex || clientCookieStr.charAt(endIndex) === ';');
}

attach (window: Window & typeof globalThis): void {
super.attach(window);

this._windowSync.attach(window);

this._cookieStrategy = CookieSandboxStrategyFactory.create(
this.proxyless,
this.document,
this._windowSync);

if (window === getTopOpenerWindow())
this._unloadSandbox.on(this._unloadSandbox.UNLOAD_EVENT, CookieSandbox._removeAllSyncCookie);
this._unloadSandbox.on(this._unloadSandbox.UNLOAD_EVENT, this._cookieStrategy.removeAllSyncCookie);
}

getWindowSync (): WindowSync {
return this._windowSync;
}

// Strategy methods
getCookie (): string {
return this._cookieStrategy.getCookie();
}
setCookie (cookie: CookieRecord | string): void {
this._cookieStrategy.setCookie(cookie);
}
syncCookie (): void {
this._cookieStrategy.syncCookie();
}
syncWindowCookie (parsedCookies: CookieRecord[]): void {
this._cookieStrategy.syncWindowCookie(parsedCookies);
}
}
10 changes: 6 additions & 4 deletions src/client/sandbox/node/document/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,12 @@ export default class DocumentSandbox extends SandboxBase {

const documentCookiePropOwnerPrototype = window[nativeMethods.documentCookiePropOwnerName].prototype;

overrideDescriptor(documentCookiePropOwnerPrototype, 'cookie', {
getter: () => documentSandbox._cookieSandbox.getCookie(),
setter: value => documentSandbox._cookieSandbox.setCookie(String(value)),
});
if (!this.proxyless) {
overrideDescriptor(documentCookiePropOwnerPrototype, 'cookie', {
getter: () => documentSandbox._cookieSandbox.getCookie(),
setter: value => documentSandbox._cookieSandbox.setCookie(String(value)),
});
}

overrideDescriptor(docPrototype, 'activeElement', {
getter: function (this: Document) {
Expand Down
7 changes: 6 additions & 1 deletion src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ interface SessionOptions {
export default abstract class Session extends EventEmitter {
uploadStorage: UploadStorage;
id: string = generateUniqueId();
cookies: Cookies = new Cookies();
cookies: Cookies;
proxy: Proxy | null = null;
externalProxySettings: ExternalProxySettings | null = null;
pageLoadCount = 0;
Expand All @@ -127,6 +127,7 @@ export default abstract class Session extends EventEmitter {
this.uploadStorage = new UploadStorage(uploadRoots);
this.options = this._getOptions(options);
this._requestHookEventData = this._initRequestHookEventData();
this.cookies = this.createCookies();
}

private _initRequestHookEventData (): RequestHookEventData {
Expand All @@ -144,6 +145,10 @@ export default abstract class Session extends EventEmitter {
}, options);
}

protected createCookies (): Cookies {
return new Cookies();
}

// State
getStateSnapshot (): StateSnapshot {
return new StateSnapshot(this.cookies.serializeJar(), null);
Expand Down

0 comments on commit 76d571e

Please sign in to comment.