diff --git a/package.json b/package.json index 4bb5989c4..f45e049fd 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/client/sandbox/cookie/index.ts b/src/client/sandbox/cookie/index.ts index a6a5ac866..0035ba480 100644 --- a/src/client/sandbox/cookie/index.ts +++ b/src/client/sandbox/cookie/index.ts @@ -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 { @@ -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; @@ -206,25 +221,53 @@ 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 { @@ -232,7 +275,30 @@ export default class CookieSandbox extends SandboxBase { 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); } } diff --git a/src/client/sandbox/node/document/index.ts b/src/client/sandbox/node/document/index.ts index 8a10c350d..32527a88c 100644 --- a/src/client/sandbox/node/document/index.ts +++ b/src/client/sandbox/node/document/index.ts @@ -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) { diff --git a/src/session/index.ts b/src/session/index.ts index 0835d39fd..ecc22df3b 100644 --- a/src/session/index.ts +++ b/src/session/index.ts @@ -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; @@ -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 { @@ -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);